diff --git a/docs/chapter_array_and_linkedlist/array.md b/docs/chapter_array_and_linkedlist/array.md index 05ed7bf0e..e19302c5a 100755 --- a/docs/chapter_array_and_linkedlist/array.md +++ b/docs/chapter_array_and_linkedlist/array.md @@ -132,14 +132,6 @@ comments: true nums = [1, 3, 2, 5, 4] ``` -=== "Zig" - - ```zig title="array.zig" - // 初始化数组 - const arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } - const nums = [_]i32{ 1, 3, 2, 5, 4 }; - ``` - ??? pythontutor "可视化运行"
@@ -326,19 +318,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 随机访问元素 - fn randomAccess(nums: []const i32) i32 { - // 在区间 [0, nums.len) 中随机抽取一个整数 - const random_index = std.crypto.random.intRangeLessThan(usize, 0, nums.len); - // 获取并返回随机元素 - const randomNum = nums[random_index]; - return randomNum; - } - ``` - ??? pythontutor "可视化运行"
@@ -535,21 +514,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 在数组的索引 index 处插入元素 num - fn insert(nums: []i32, num: i32, index: usize) void { - // 把索引 index 以及之后的所有元素向后移动一位 - var i = nums.len - 1; - while (i > index) : (i -= 1) { - nums[i] = nums[i - 1]; - } - // 将 num 赋给 index 处的元素 - nums[index] = num; - } - ``` - ??? pythontutor "可视化运行"
@@ -720,19 +684,6 @@ comments: true end ``` -=== "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]; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -980,33 +931,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 遍历数组 - fn traverse(nums: []const i32) void { - var count: i32 = 0; - - // 通过索引遍历数组 - var i: usize = 0; - while (i < nums.len) : (i += 1) { - count += nums[i]; - } - - // 直接遍历数组元素 - count = 0; - for (nums) |num| { - count += num; - } - - // 同时遍历数据索引和元素 - for (nums, 0..) |num, index| { - count += nums[index]; - count += num; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -1189,18 +1113,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 在数组中查找指定元素 - fn find(nums: []i32, target: i32) i32 { - for (nums, 0..) |num, i| { - if (num == target) return @intCast(i); - } - return -1; - } - ``` - ??? pythontutor "可视化运行"
@@ -1431,23 +1343,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 扩展数组长度 - fn extend(allocator: std.mem.Allocator, nums: []const i32, enlarge: usize) ![]i32 { - // 初始化一个扩展长度后的数组 - const res = try allocator.alloc(i32, nums.len + enlarge); - @memset(res, 0); - - // 将原数组中的所有元素复制到新数组 - std.mem.copyForwards(i32, res, nums); - - // 返回扩展后的新数组 - return res; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_array_and_linkedlist/linked_list.md b/docs/chapter_array_and_linkedlist/linked_list.md index d2c40164f..4b3782538 100755 --- a/docs/chapter_array_and_linkedlist/linked_list.md +++ b/docs/chapter_array_and_linkedlist/linked_list.md @@ -191,26 +191,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 链表节点类 - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // 节点值 - next: ?*Self = null, // 指向下一节点的指针 - - // 构造函数 - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - } - }; - } - ``` - ## 4.2.1   链表常用操作 ### 1.   初始化链表 @@ -439,23 +419,6 @@ comments: true n3.next = n4 ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 初始化链表 - // 初始化各个节点 - var n0 = inc.ListNode(i32){.val = 1}; - var n1 = inc.ListNode(i32){.val = 3}; - var n2 = inc.ListNode(i32){.val = 2}; - var n3 = inc.ListNode(i32){.val = 5}; - var n4 = inc.ListNode(i32){.val = 4}; - // 构建节点之间的引用 - n0.next = &n1; - n1.next = &n2; - n2.next = &n3; - n3.next = &n4; - ``` - ??? pythontutor "可视化运行"
@@ -617,17 +580,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 在链表的节点 n0 之后插入节点 P - fn insert(comptime T: type, n0: *ListNode(T), P: *ListNode(T)) void { - const n1 = n0.next; - P.next = n1; - n0.next = P; - } - ``` - ??? pythontutor "可视化运行"
@@ -831,18 +783,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 删除链表的节点 n0 之后的首个节点 - fn remove(comptime T: type, n0: *ListNode(T)) void { - // n0 -> P -> n1 => n0 -> n1 - const P = n0.next; - const n1 = P.?.next; - n0.next = n1; - } - ``` - ??? pythontutor "可视化运行"
@@ -1047,24 +987,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 访问链表中索引为 index 的节点 - fn access(comptime T: type, node: *ListNode(T), index: i32) ?*ListNode(T) { - var head: ?*ListNode(T) = node; - var i: i32 = 0; - while (i < index) : (i += 1) { - if (head) |cur| { - head = cur.next; - } else { - return null; - } - } - return head; - } - ``` - ??? pythontutor "可视化运行"
@@ -1291,22 +1213,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 在链表中查找值为 target 的首个节点 - fn find(comptime T: type, node: *ListNode(T), target: T) i32 { - var head: ?*ListNode(T) = node; - var index: i32 = 0; - while (head) |cur| { - if (cur.val == target) return index; - head = cur.next; - index += 1; - } - return -1; - } - ``` - ??? pythontutor "可视化运行"
@@ -1537,28 +1443,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 双向链表节点类 - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // 节点值 - next: ?*Self = null, // 指向后继节点的指针 - prev: ?*Self = null, // 指向前驱节点的指针 - - // 构造函数 - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - self.prev = null; - } - }; - } - ``` - ![常见链表种类](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" }

图 4-8   常见链表种类

diff --git a/docs/chapter_array_and_linkedlist/list.md b/docs/chapter_array_and_linkedlist/list.md index 4111af26e..fe798e359 100755 --- a/docs/chapter_array_and_linkedlist/list.md +++ b/docs/chapter_array_and_linkedlist/list.md @@ -151,15 +151,6 @@ comments: true nums = [1, 3, 2, 5, 4] ``` -=== "Zig" - - ```zig title="list.zig" - // 初始化列表 - var nums = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums.deinit(); - try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); - ``` - ??? pythontutor "可视化运行"
@@ -292,16 +283,6 @@ comments: true nums[1] = 0 # 将索引 1 处的元素更新为 0 ``` -=== "Zig" - - ```zig title="list.zig" - // 访问元素 - var num = nums.items[1]; // 访问索引 1 处的元素 - - // 更新元素 - nums.items[1] = 0; // 将索引 1 处的元素更新为 0 - ``` - ??? pythontutor "可视化运行"
@@ -557,26 +538,6 @@ comments: true nums.delete_at(3) # 删除索引 3 处的元素 ``` -=== "Zig" - - ```zig title="list.zig" - // 清空列表 - nums.clearRetainingCapacity(); - - // 在尾部添加元素 - try nums.append(1); - try nums.append(3); - try nums.append(2); - try nums.append(5); - try nums.append(4); - - // 在中间插入元素 - try nums.insert(3, 6); // 在索引 3 处插入数字 6 - - // 删除元素 - _ = nums.orderedRemove(3); // 删除索引 3 处的元素 - ``` - ??? pythontutor "可视化运行"
@@ -779,23 +740,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="list.zig" - // 通过索引遍历列表 - var count: i32 = 0; - var i: i32 = 0; - while (i < nums.items.len) : (i += 1) { - count += nums[i]; - } - - // 直接遍历列表元素 - count = 0; - for (nums.items) |num| { - count += num; - } - ``` - ??? pythontutor "可视化运行"
@@ -908,16 +852,6 @@ comments: true nums += nums1 ``` -=== "Zig" - - ```zig title="list.zig" - // 拼接两个列表 - var nums1 = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums1.deinit(); - try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); - try nums.insertSlice(nums.items.len, nums1.items); // 将列表 nums1 拼接到 nums 之后 - ``` - ??? pythontutor "可视化运行"
@@ -1017,13 +951,6 @@ comments: true nums = nums.sort { |a, b| a <=> b } # 排序后,列表元素从小到大排列 ``` -=== "Zig" - - ```zig title="list.zig" - // 排序列表 - std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32)); - ``` - ??? pythontutor "可视化运行"
@@ -2368,163 +2295,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_list.zig" - // 列表类 - const MyList = struct { - const Self = @This(); - - items: []i32, // 数组(存储列表元素) - capacity: usize, // 列表容量 - allocator: std.mem.Allocator, // 内存分配器 - - extend_ratio: usize = 2, // 每次列表扩容的倍数 - - // 构造函数(分配内存+初始化列表) - pub fn init(allocator: std.mem.Allocator) Self { - return Self{ - .items = &[_]i32{}, - .capacity = 0, - .allocator = allocator, - }; - } - - // 析构函数(释放内存) - pub fn deinit(self: Self) void { - self.allocator.free(self.allocatedSlice()); - } - - // 在尾部添加元素 - pub fn add(self: *Self, item: i32) !void { - // 元素数量超出容量时,触发扩容机制 - const newlen = self.items.len + 1; - try self.ensureTotalCapacity(newlen); - - // 更新元素 - self.items.len += 1; - const new_item_ptr = &self.items[self.items.len - 1]; - new_item_ptr.* = item; - } - - // 获取列表长度(当前元素数量) - pub fn getSize(self: *Self) usize { - return self.items.len; - } - - // 获取列表容量 - pub fn getCapacity(self: *Self) usize { - return self.capacity; - } - - // 访问元素 - pub fn get(self: *Self, index: usize) i32 { - // 索引如果越界,则抛出异常,下同 - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - return self.items[index]; - } - - // 更新元素 - pub fn set(self: *Self, index: usize, num: i32) void { - // 索引如果越界,则抛出异常,下同 - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - self.items[index] = num; - } - - // 在中间插入元素 - pub fn insert(self: *Self, index: usize, item: i32) !void { - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - - // 元素数量超出容量时,触发扩容机制 - const newlen = self.items.len + 1; - try self.ensureTotalCapacity(newlen); - - // 将索引 index 以及之后的元素都向后移动一位 - self.items.len += 1; - var i = self.items.len - 1; - while (i >= index) : (i -= 1) { - self.items[i] = self.items[i - 1]; - } - self.items[index] = item; - } - - // 删除元素 - pub fn remove(self: *Self, index: usize) i32 { - if (index < 0 or index >= self.getSize()) { - @panic("索引越界"); - } - // 将索引 index 之后的元素都向前移动一位 - const item = self.items[index]; - var i = index; - while (i < self.items.len - 1) : (i += 1) { - self.items[i] = self.items[i + 1]; - } - self.items.len -= 1; - // 返回被删除的元素 - return item; - } - - // 将列表转换为数组 - pub fn toArraySlice(self: *Self) ![]i32 { - return self.toOwnedSlice(false); - } - - // 返回新的切片并设置是否要重置或清空列表容器 - pub fn toOwnedSlice(self: *Self, clear: bool) ![]i32 { - const allocator = self.allocator; - const old_memory = self.allocatedSlice(); - if (allocator.remap(old_memory, self.items.len)) |new_items| { - if (clear) { - self.* = init(allocator); - } - return new_items; - } - - const new_memory = try allocator.alloc(i32, self.items.len); - @memcpy(new_memory, self.items); - if (clear) { - self.clearAndFree(); - } - return new_memory; - } - - // 列表扩容 - fn ensureTotalCapacity(self: *Self, new_capacity: usize) !void { - if (self.capacity >= new_capacity) return; - const capcacity = if (self.capacity == 0) 10 else self.capacity; - const better_capacity = capcacity * self.extend_ratio; - - const old_memory = self.allocatedSlice(); - if (self.allocator.remap(old_memory, better_capacity)) |new_memory| { - self.items.ptr = new_memory.ptr; - self.capacity = new_memory.len; - } else { - const new_memory = try self.allocator.alloc(i32, better_capacity); - @memcpy(new_memory[0..self.items.len], self.items); - self.allocator.free(old_memory); - self.items.ptr = new_memory.ptr; - self.capacity = new_memory.len; - } - } - - fn clearAndFree(self: *Self, allocator: std.mem.Allocator) void { - allocator.free(self.allocatedSlice()); - self.items.len = 0; - self.capacity = 0; - } - - fn allocatedSlice(self: Self) []i32 { - return self.items.ptr[0..self.capacity]; - } - }; - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_backtracking/backtracking_algorithm.md b/docs/chapter_backtracking/backtracking_algorithm.md index 0dc8c6a79..ea28484de 100644 --- a/docs/chapter_backtracking/backtracking_algorithm.md +++ b/docs/chapter_backtracking/backtracking_algorithm.md @@ -232,12 +232,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_i_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "可视化运行"
@@ -549,12 +543,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_ii_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "可视化运行"
@@ -909,12 +897,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "可视化运行"
@@ -1266,12 +1248,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 接下来,我们基于框架代码来解决例题三。状态 `state` 为节点遍历路径,选择 `choices` 为当前节点的左子节点和右子节点,结果 `res` 是路径列表: === "Python" @@ -1948,22 +1924,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_template.zig" - [class]{}-[func]{isSolution} - - [class]{}-[func]{recordSolution} - - [class]{}-[func]{isValid} - - [class]{}-[func]{makeChoice} - - [class]{}-[func]{undoChoice} - - [class]{}-[func]{backtrack} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_backtracking/n_queens_problem.md b/docs/chapter_backtracking/n_queens_problem.md index 2aed6bd44..8aec7c995 100644 --- a/docs/chapter_backtracking/n_queens_problem.md +++ b/docs/chapter_backtracking/n_queens_problem.md @@ -744,14 +744,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="n_queens.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_backtracking/permutations_problem.md b/docs/chapter_backtracking/permutations_problem.md index e2b4f1fe2..21d0c4783 100644 --- a/docs/chapter_backtracking/permutations_problem.md +++ b/docs/chapter_backtracking/permutations_problem.md @@ -538,14 +538,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="permutations_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsI} - ``` - ??? pythontutor "可视化运行"
@@ -1093,14 +1085,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="permutations_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsII} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_backtracking/subset_sum_problem.md b/docs/chapter_backtracking/subset_sum_problem.md index d667f5e0d..eef470afb 100644 --- a/docs/chapter_backtracking/subset_sum_problem.md +++ b/docs/chapter_backtracking/subset_sum_problem.md @@ -501,14 +501,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_i_naive.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumINaive} - ``` - ??? pythontutor "可视化运行"
@@ -1070,14 +1062,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumI} - ``` - ??? pythontutor "可视化运行"
@@ -1689,14 +1673,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumII} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_computational_complexity/iteration_and_recursion.md b/docs/chapter_computational_complexity/iteration_and_recursion.md index e3d0d280c..699f50d80 100644 --- a/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -198,20 +198,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // for 循环 - fn forLoop(n: usize) i32 { - var res: i32 = 0; - // 循环求和 1, 2, ..., n-1, n - for (1..n + 1) |i| { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "可视化运行"
@@ -442,21 +428,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // while 循环 - fn whileLoop(n: i32) i32 { - var res: i32 = 0; - var i: i32 = 1; // 初始化条件变量 - // 循环求和 1, 2, ..., n-1, n - while (i <= n) : (i += 1) { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "可视化运行"
@@ -702,25 +673,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // while 循环(两次更新) - fn whileLoopII(n: i32) i32 { - var res: i32 = 0; - var i: i32 = 1; // 初始化条件变量 - // 循环求和 1, 4, 10, ... - while (i <= n) : ({ - // 更新条件变量 - i += 1; - i *= 2; - }) { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "可视化运行"
@@ -956,26 +908,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // 双层 for 循环 - fn nestedForLoop(allocator: Allocator, n: usize) ![]const u8 { - var res = std.ArrayList(u8).init(allocator); - defer res.deinit(); - var buffer: [20]u8 = undefined; - // 循环 i = 1, 2, ..., n-1, n - for (1..n + 1) |i| { - // 循环 j = 1, 2, ..., n-1, n - for (1..n + 1) |j| { - const str = try std.fmt.bufPrint(&buffer, "({d}, {d}), ", .{ i, j }); - try res.appendSlice(str); - } - } - return res.toOwnedSlice(); - } - ``` - ??? pythontutor "可视化运行"
@@ -1199,22 +1131,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 递归函数 - fn recur(n: i32) i32 { - // 终止条件 - if (n == 1) { - return 1; - } - // 递:递归调用 - const res = recur(n - 1); - // 归:返回结果 - return n + res; - } - ``` - ??? pythontutor "可视化运行"
@@ -1428,20 +1344,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 尾递归函数 - fn tailRecur(n: i32, res: i32) i32 { - // 终止条件 - if (n == 0) { - return res; - } - // 尾递归调用 - return tailRecur(n - 1, res + n); - } - ``` - ??? pythontutor "可视化运行"
@@ -1668,22 +1570,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 斐波那契数列 - fn fib(n: i32) i32 { - // 终止条件 f(1) = 0, f(2) = 1 - if (n == 1 or n == 2) { - return n - 1; - } - // 递归调用 f(n) = f(n-1) + f(n-2) - const res: i32 = fib(n - 1) + fib(n - 2); - // 返回结果 f(n) - return res; - } - ``` - ??? pythontutor "可视化运行"
@@ -2029,31 +1915,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 使用迭代模拟递归 - fn forLoopRecur(comptime n: i32) i32 { - // 使用一个显式的栈来模拟系统调用栈 - var stack: [n]i32 = undefined; - var res: i32 = 0; - // 递:递归调用 - var i: usize = n; - while (i > 0) { - stack[i - 1] = @intCast(i); - i -= 1; - } - // 归:返回结果 - var index: usize = n; - while (index > 0) { - index -= 1; - res += stack[index]; - } - // res = 1+2+3+...+n - return res; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_computational_complexity/space_complexity.md b/docs/chapter_computational_complexity/space_complexity.md index 94d76842e..c107ddd6b 100755 --- a/docs/chapter_computational_complexity/space_complexity.md +++ b/docs/chapter_computational_complexity/space_complexity.md @@ -367,12 +367,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - ## 2.4.2   推算方法 空间复杂度的推算方法与时间复杂度大致相同,只需将统计对象从“操作数量”转为“使用空间大小”。 @@ -535,12 +529,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - **在递归函数中,需要注意统计栈帧空间**。观察以下代码: === "Python" @@ -813,12 +801,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 函数 `loop()` 和 `recur()` 的时间复杂度都为 $O(n)$ ,但空间复杂度不同。 - 函数 `loop()` 在循环中调用了 $n$ 次 `function()` ,每轮中的 `function()` 都返回并释放了栈帧空间,因此空间复杂度仍为 $O(1)$ 。 @@ -1195,40 +1177,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 函数 - fn function() i32 { - // 执行某些操作 - return 0; - } - - // 常数阶 - fn constant(n: i32) void { - // 常量、变量、对象占用 O(1) 空间 - const a: i32 = 0; - const b: i32 = 0; - const nums = [_]i32{0} ** 10000; - const node = ListNode(i32){ .val = 0 }; - var i: i32 = 0; - // 循环中的变量占用 O(1) 空间 - while (i < n) : (i += 1) { - const c: i32 = 0; - _ = c; - } - // 循环中的函数占用 O(1) 空间 - i = 0; - while (i < n) : (i += 1) { - _ = function(); - } - _ = a; - _ = b; - _ = nums; - _ = node; - } - ``` - ??? pythontutor "可视化运行"
@@ -1507,33 +1455,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 线性阶 - fn linear(comptime n: i32) !void { - // 长度为 n 的数组占用 O(n) 空间 - const 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; - } - ``` - ??? pythontutor "可视化运行"
@@ -1694,17 +1615,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 线性阶(递归实现) - fn linearRecur(comptime n: i32) void { - std.debug.print("递归 n = {}\n", .{n}); - if (n == 1) return; - linearRecur(n - 1); - } - ``` - ??? pythontutor "可视化运行"
@@ -1938,27 +1848,6 @@ $$ end ``` -=== "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); - } - } - ``` - ??? pythontutor "可视化运行"
@@ -2140,18 +2029,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 平方阶(递归实现) - fn quadraticRecur(comptime n: i32) i32 { - if (n <= 0) return 0; - const nums = [_]i32{0} ** n; - std.debug.print("递归 n = {} 中的 nums 长度 = {}\n", .{ n, nums.len }); - return quadraticRecur(n - 1); - } - ``` - ??? pythontutor "可视化运行"
@@ -2346,20 +2223,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 指数阶(建立满二叉树) - fn buildTree(allocator: std.mem.Allocator, n: i32) !?*TreeNode(i32) { - if (n == 0) return null; - const root = try allocator.create(TreeNode(i32)); - root.init(0); - root.left = try buildTree(allocator, n - 1); - root.right = try buildTree(allocator, n - 1); - return root; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_computational_complexity/time_complexity.md b/docs/chapter_computational_complexity/time_complexity.md index 825e1f646..387558fa7 100755 --- a/docs/chapter_computational_complexity/time_complexity.md +++ b/docs/chapter_computational_complexity/time_complexity.md @@ -205,21 +205,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 在某运行平台下 - fn algorithm(n: usize) void { - var a: i32 = 2; // 1 ns - a += 1; // 1 ns - a *= 2; // 10 ns - // 循环 n 次 - for (0..n) |_| { // 1 ns - std.debug.print("{}\n", .{0}); // 5 ns - } - } - ``` - 根据以上方法,可以得到算法的运行时间为 $(6n + 12)$ ns : $$ @@ -503,29 +488,6 @@ $$ end ``` -=== "Zig" - - ```zig title="" - // 算法 A 的时间复杂度:常数阶 - fn algorithm_A(n: usize) void { - _ = n; - std.debug.print("{}\n", .{0}); - } - // 算法 B 的时间复杂度:线性阶 - fn algorithm_B(n: i32) void { - for (0..n) |_| { - std.debug.print("{}\n", .{0}); - } - } - // 算法 C 的时间复杂度:常数阶 - fn algorithm_C(n: i32) void { - _ = n; - for (0..1000000) |_| { - std.debug.print("{}\n", .{0}); - } - } - ``` - 图 2-7 展示了以上三个算法函数的时间复杂度。 - 算法 `A` 只有 $1$ 个打印操作,算法运行时间不随着 $n$ 增大而增长。我们称此算法的时间复杂度为“常数阶”。 @@ -727,20 +689,6 @@ $$ end ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +1 - a += 1; // +1 - a *= 2; // +1 - // 循环 n 次 - for (0..n) |_| { // +1(每轮都执行 i ++) - std.debug.print("{}\n", .{0}); // +1 - } - } - ``` - 设算法的操作数量是一个关于输入数据大小 $n$ 的函数,记为 $T(n)$ ,则以上函数的操作数量为: $$ @@ -1020,27 +968,6 @@ $T(n)$ 是一次函数,说明其运行时间的增长趋势是线性的,因 end ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +0(技巧 1) - a = a + @as(i32, @intCast(n)); // +0(技巧 1) - - // +n(技巧 2) - for(0..(5 * n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - - // +n*n(技巧 3) - for(0..(2 * n)) |_| { - for(0..(n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - } - } - ``` - 以下公式展示了使用上述技巧前后的统计结果,两者推算出的时间复杂度都为 $O(n^2)$ 。 $$ @@ -1266,22 +1193,6 @@ $$ end ``` -=== "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 < size) : (i += 1) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -1448,20 +1359,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "可视化运行"
@@ -1651,20 +1548,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 线性阶(遍历数组) - fn arrayTraversal(nums: []i32) i32 { - var count: i32 = 0; - // 循环次数与数组长度成正比 - for (nums) |_| { - count += 1; - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -1883,24 +1766,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 平方阶 - fn quadratic(n: i32) i32 { - var count: i32 = 0; - var i: i32 = 0; - // 循环次数与数据大小 n 成平方关系 - while (i < n) : (i += 1) { - var j: i32 = 0; - while (j < n) : (j += 1) { - count += 1; - } - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -2210,31 +2075,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 平方阶(冒泡排序) - fn bubbleSort(nums: []i32) i32 { - var count: i32 = 0; // 计数器 - // 外循环:未排序区间为 [0, i] - var i: i32 = @as(i32, @intCast(nums.len)) - 1; - while (i > 0) : (i -= 1) { - var j: usize = 0; - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交换 nums[j] 与 nums[j + 1] - const tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - count += 3; // 元素交换包含 3 个单元操作 - } - } - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -2484,27 +2324,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "可视化运行"
@@ -2657,16 +2476,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 指数阶(递归实现) - fn expRecur(n: i32) i32 { - if (n == 1) return 1; - return expRecur(n - 1) + expRecur(n - 1) + 1; - } - ``` - ??? pythontutor "可视化运行"
@@ -2864,20 +2673,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 对数阶(循环实现) - fn logarithmic(n: i32) i32 { - var count: i32 = 0; - var n_var: i32 = n; - while (n_var > 1) : (n_var = @divTrunc(n_var, 2)) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -3029,16 +2824,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 对数阶(递归实现) - fn logRecur(n: i32) i32 { - if (n <= 1) return 0; - return logRecur(@divTrunc(n, 2)) + 1; - } - ``` - ??? pythontutor "可视化运行"
@@ -3253,21 +3038,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 线性对数阶 - fn linearLogRecur(n: i32) i32 { - if (n <= 1) return 1; - var count: i32 = linearLogRecur(@divTrunc(n, 2)) + linearLogRecur(@divTrunc(n, 2)); - var i: i32 = 0; - while (i < n) : (i += 1) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "可视化运行"
@@ -3494,22 +3264,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "可视化运行"
@@ -3904,33 +3658,6 @@ $$ end ``` -=== "Zig" - - ```zig title="worst_best_time_complexity.zig" - // 生成一个数组,元素为 { 1, 2, ..., n },顺序被打乱 - fn randomNumbers(comptime n: usize) [n]i32 { - var nums: [n]i32 = undefined; - // 生成数组 nums = { 1, 2, 3, ..., n } - for (&nums, 0..) |*num, i| { - num.* = @as(i32, @intCast(i)) + 1; - } - // 随机打乱数组元素 - const rand = std.crypto.random; - rand.shuffle(i32, &nums); - return nums; - } - - // 查找数组 nums 中数字 1 所在索引 - fn findOne(nums: []i32) i32 { - for (nums, 0..) |num, i| { - // 当元素 1 在数组头部时,达到最佳时间复杂度 O(1) - // 当元素 1 在数组尾部时,达到最差时间复杂度 O(n) - if (num == 1) return @intCast(i); - } - return -1; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_data_structure/basic_data_types.md b/docs/chapter_data_structure/basic_data_types.md index 90a2cc49b..1db5460c6 100644 --- a/docs/chapter_data_structure/basic_data_types.md +++ b/docs/chapter_data_structure/basic_data_types.md @@ -178,24 +178,6 @@ comments: true data = [0, 0.0, 'a', false, ListNode(0)] ``` -=== "Zig" - - ```zig title="" - const hello = [5]u8{ 'h', 'e', 'l', 'l', 'o' }; - // 以上代码展示了定义一个字面量数组的方式,其中你可以选择指明数组的大小或者使用 _ 代替。使用 _ 时,Zig 会尝试自动计算数组的长度 - - const matrix_4x4 = [4][4]f32{ - [_]f32{ 1.0, 0.0, 0.0, 0.0 }, - [_]f32{ 0.0, 1.0, 0.0, 1.0 }, - [_]f32{ 0.0, 0.0, 1.0, 0.0 }, - [_]f32{ 0.0, 0.0, 0.0, 1.0 }, - }; - // 多维数组(矩阵)实际上就是嵌套数组,我们很容易就可以创建一个多维数组出来 - - const array = [_:0]u8{ 1, 2, 3, 4 }; - // 定义一个哨兵终止数组,本质上来说,这是为了兼容 C 中的规定的字符串结尾字符\0。我们使用语法 [N:x]T 来描述一个元素为类型 T,长度为 N 的数组,在它对应 N 的索引处的值应该是 x - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_divide_and_conquer/binary_search_recur.md b/docs/chapter_divide_and_conquer/binary_search_recur.md index aad12dd48..f7ce7677d 100644 --- a/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -450,14 +450,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_recur.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index fc99c18b4..15f1cc9c7 100644 --- a/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -512,14 +512,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="build_tree.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_divide_and_conquer/hanota_problem.md b/docs/chapter_divide_and_conquer/hanota_problem.md index 37fe6ba6a..e796cfd8a 100644 --- a/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/docs/chapter_divide_and_conquer/hanota_problem.md @@ -542,16 +542,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hanota.zig" - [class]{}-[func]{move} - - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_divide_and_conquer/summary.md b/docs/chapter_divide_and_conquer/summary.md index 008c51bf0..faec91d0a 100644 --- a/docs/chapter_divide_and_conquer/summary.md +++ b/docs/chapter_divide_and_conquer/summary.md @@ -4,6 +4,8 @@ comments: true # 12.5   小结 +### 1.   重点回顾 + - 分治是一种常见的算法设计策略,包括分(划分)和治(合并)两个阶段,通常基于递归实现。 - 判断是否是分治算法问题的依据包括:问题能否分解、子问题是否独立、子问题能否合并。 - 归并排序是分治策略的典型应用,其递归地将数组划分为等长的两个子数组,直到只剩一个元素时开始逐层合并,从而完成排序。 diff --git a/docs/chapter_dynamic_programming/dp_problem_features.md b/docs/chapter_dynamic_programming/dp_problem_features.md index 5d1ae07a2..db17d2f6a 100644 --- a/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/docs/chapter_dynamic_programming/dp_problem_features.md @@ -318,28 +318,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - // 爬楼梯最小代价:动态规划 - fn minCostClimbingStairsDP(comptime cost: []i32) i32 { - comptime var n = cost.len - 1; - if (n == 1 or n == 2) { - return cost[n]; - } - // 初始化 dp 表,用于存储子问题的解 - var dp = [_]i32{-1} ** (n + 1); - // 初始状态:预设最小子问题的解 - dp[1] = cost[1]; - dp[2] = cost[2]; - // 状态转移:从较小子问题逐步求解较大子问题 - for (3..n + 1) |i| { - dp[i] = @min(dp[i - 1], dp[i - 2]) + cost[i]; - } - return dp[n]; - } - ``` - ??? pythontutor "可视化运行"
@@ -603,27 +581,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - // 爬楼梯最小代价:空间优化后的动态规划 - fn minCostClimbingStairsDPComp(cost: []i32) i32 { - var n = cost.len - 1; - if (n == 1 or n == 2) { - return cost[n]; - } - var a = cost[1]; - var b = cost[2]; - // 状态转移:从较小子问题逐步求解较大子问题 - for (3..n + 1) |i| { - var tmp = b; - b = @min(a, tmp) + cost[i]; - a = tmp; - } - return b; - } - ``` - ??? pythontutor "可视化运行"
@@ -985,30 +942,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_constraint_dp.zig" - // 带约束爬楼梯:动态规划 - fn climbingStairsConstraintDP(comptime n: usize) i32 { - if (n == 1 or n == 2) { - return 1; - } - // 初始化 dp 表,用于存储子问题的解 - var dp = [_][3]i32{ [_]i32{ -1, -1, -1 } } ** (n + 1); - // 初始状态:预设最小子问题的解 - dp[1][1] = 1; - dp[1][2] = 0; - dp[2][1] = 0; - dp[2][2] = 1; - // 状态转移:从较小子问题逐步求解较大子问题 - for (3..n + 1) |i| { - dp[i][1] = dp[i - 1][2]; - dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; - } - return dp[n][1] + dp[n][2]; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 6253b15c5..e1644d7d8 100644 --- a/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -383,27 +383,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路径和:暴力搜索 - fn minPathSumDFS(grid: anytype, i: i32, j: i32) i32 { - // 若为左上角单元格,则终止搜索 - if (i == 0 and j == 0) { - return grid[0][0]; - } - // 若行列索引越界,则返回 +∞ 代价 - if (i < 0 or j < 0) { - return std.math.maxInt(i32); - } - // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 - var up = minPathSumDFS(grid, i - 1, j); - var left = minPathSumDFS(grid, i, j - 1); - // 返回从左上角到 (i, j) 的最小路径代价 - return @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - ``` - ??? pythontutor "可视化运行"
@@ -763,33 +742,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路径和:记忆化搜索 - fn minPathSumDFSMem(grid: anytype, mem: anytype, i: i32, j: i32) i32 { - // 若为左上角单元格,则终止搜索 - if (i == 0 and j == 0) { - return grid[0][0]; - } - // 若行列索引越界,则返回 +∞ 代价 - if (i < 0 or j < 0) { - return std.math.maxInt(i32); - } - // 若已有记录,则直接返回 - if (mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] != -1) { - return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - // 计算从左上角到 (i-1, j) 和 (i, j-1) 的最小路径代价 - var up = minPathSumDFSMem(grid, mem, i - 1, j); - var left = minPathSumDFSMem(grid, mem, i, j - 1); - // 返回从左上角到 (i, j) 的最小路径代价 - // 记录并返回左上角到 (i, j) 的最小路径代价 - mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] = @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1165,34 +1117,6 @@ $$ end ``` -=== "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]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1581,32 +1505,6 @@ $$ end ``` -=== "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]; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_dynamic_programming/edit_distance_problem.md b/docs/chapter_dynamic_programming/edit_distance_problem.md index 154d03f04..aeed93930 100644 --- a/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -477,37 +477,6 @@ $$ end ``` -=== "Zig" - - ```zig title="edit_distance.zig" - // 编辑距离:动态规划 - fn editDistanceDP(comptime s: []const u8, comptime t: []const u8) i32 { - comptime var n = s.len; - comptime var m = t.len; - var dp = [_][m + 1]i32{[_]i32{0} ** (m + 1)} ** (n + 1); - // 状态转移:首行首列 - for (1..n + 1) |i| { - dp[i][0] = @intCast(i); - } - for (1..m + 1) |j| { - dp[0][j] = @intCast(j); - } - // 状态转移:其余行和列 - for (1..n + 1) |i| { - for (1..m + 1) |j| { - if (s[i - 1] == t[j - 1]) { - // 若两字符相等,则直接跳过此两字符 - dp[i][j] = dp[i - 1][j - 1]; - } else { - // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 - dp[i][j] = @min(@min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; - } - } - } - return dp[n][m]; - } - ``` - ??? pythontutor "可视化运行"
@@ -997,40 +966,6 @@ $$ end ``` -=== "Zig" - - ```zig title="edit_distance.zig" - // 编辑距离:空间优化后的动态规划 - fn editDistanceDPComp(comptime s: []const u8, comptime t: []const u8) i32 { - comptime var n = s.len; - comptime var m = t.len; - var dp = [_]i32{0} ** (m + 1); - // 状态转移:首行 - for (1..m + 1) |j| { - dp[j] = @intCast(j); - } - // 状态转移:其余行 - for (1..n + 1) |i| { - // 状态转移:首列 - var leftup = dp[0]; // 暂存 dp[i-1, j-1] - dp[0] = @intCast(i); - // 状态转移:其余列 - for (1..m + 1) |j| { - var temp = dp[j]; - if (s[i - 1] == t[j - 1]) { - // 若两字符相等,则直接跳过此两字符 - dp[j] = leftup; - } else { - // 最少编辑步数 = 插入、删除、替换这三种操作的最少编辑步数 + 1 - dp[j] = @min(@min(dp[j - 1], dp[j]), leftup) + 1; - } - leftup = temp; // 更新为下一轮的 dp[i-1, j-1] - } - } - return dp[m]; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index d2b0c3815..6ae1e0455 100644 --- a/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -420,39 +420,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="climbing_stairs_backtrack.zig" - // 回溯 - fn backtrack(choices: []i32, state: i32, n: i32, res: std.ArrayList(i32)) void { - // 当爬到第 n 阶时,方案数量加 1 - if (state == n) { - res.items[0] = res.items[0] + 1; - } - // 遍历所有选择 - for (choices) |choice| { - // 剪枝:不允许越过第 n 阶 - if (state + choice > n) { - continue; - } - // 尝试:做出选择,更新状态 - backtrack(choices, state + choice, n, res); - // 回退 - } - } - - // 爬楼梯:回溯 - fn climbingStairsBacktrack(n: usize) !i32 { - var choices = [_]i32{ 1, 2 }; // 可选择向上爬 1 阶或 2 阶 - var state: i32 = 0; // 从第 0 阶开始爬 - var res = std.ArrayList(i32).init(std.heap.page_allocator); - defer res.deinit(); - try res.append(0); // 使用 res[0] 记录方案数量 - backtrack(&choices, state, @intCast(n), res); - return res.items[0]; - } - ``` - ??? pythontutor "可视化运行"
@@ -728,26 +695,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs.zig" - // 搜索 - fn dfs(i: usize) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (i == 1 or i == 2) { - return @intCast(i); - } - // dp[i] = dp[i-1] + dp[i-2] - var count = dfs(i - 1) + dfs(i - 2); - return count; - } - - // 爬楼梯:搜索 - fn climbingStairsDFS(comptime n: usize) i32 { - return dfs(n); - } - ``` - ??? pythontutor "可视化运行"
@@ -1115,34 +1062,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs_mem.zig" - // 记忆化搜索 - fn dfs(i: usize, mem: []i32) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (i == 1 or i == 2) { - return @intCast(i); - } - // 若存在记录 dp[i] ,则直接返回之 - if (mem[i] != -1) { - return mem[i]; - } - // dp[i] = dp[i-1] + dp[i-2] - var count = dfs(i - 1, mem) + dfs(i - 2, mem); - // 记录 dp[i] - mem[i] = count; - return count; - } - - // 爬楼梯:记忆化搜索 - fn climbingStairsDFSMem(comptime n: usize) i32 { - // mem[i] 记录爬到第 i 阶的方案总数,-1 代表无记录 - var mem = [_]i32{ -1 } ** (n + 1); - return dfs(n, &mem); - } - ``` - ??? pythontutor "可视化运行"
@@ -1419,28 +1338,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - // 爬楼梯:动态规划 - fn climbingStairsDP(comptime n: usize) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (n == 1 or n == 2) { - return @intCast(n); - } - // 初始化 dp 表,用于存储子问题的解 - var dp = [_]i32{-1} ** (n + 1); - // 初始状态:预设最小子问题的解 - dp[1] = 1; - dp[2] = 2; - // 状态转移:从较小子问题逐步求解较大子问题 - for (3..n + 1) |i| { - dp[i] = dp[i - 1] + dp[i - 2]; - } - return dp[n]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1678,25 +1575,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - // 爬楼梯:空间优化后的动态规划 - fn climbingStairsDPComp(comptime n: usize) i32 { - if (n == 1 or n == 2) { - return @intCast(n); - } - var a: i32 = 1; - var b: i32 = 2; - for (3..n + 1) |_| { - var tmp = b; - b = a + b; - a = tmp; - } - return b; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_dynamic_programming/knapsack_problem.md b/docs/chapter_dynamic_programming/knapsack_problem.md index b5c72f954..137dccaa4 100644 --- a/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/docs/chapter_dynamic_programming/knapsack_problem.md @@ -338,27 +338,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:暴力搜索 - fn knapsackDFS(wgt: []i32, val: []i32, i: usize, c: usize) i32 { - // 若已选完所有物品或背包无剩余容量,则返回价值 0 - if (i == 0 or c == 0) { - return 0; - } - // 若超过背包容量,则只能选择不放入背包 - if (wgt[i - 1] > c) { - return knapsackDFS(wgt, val, i - 1, c); - } - // 计算不放入和放入物品 i 的最大价值 - var no = knapsackDFS(wgt, val, i - 1, c); - var yes = knapsackDFS(wgt, val, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; - // 返回两种方案中价值更大的那一个 - return @max(no, yes); - } - ``` - ??? pythontutor "可视化运行"
@@ -727,32 +706,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:记忆化搜索 - fn knapsackDFSMem(wgt: []i32, val: []i32, mem: anytype, i: usize, c: usize) i32 { - // 若已选完所有物品或背包无剩余容量,则返回价值 0 - if (i == 0 or c == 0) { - return 0; - } - // 若已有记录,则直接返回 - if (mem[i][c] != -1) { - return mem[i][c]; - } - // 若超过背包容量,则只能选择不放入背包 - if (wgt[i - 1] > c) { - return knapsackDFSMem(wgt, val, mem, i - 1, c); - } - // 计算不放入和放入物品 i 的最大价值 - var no = knapsackDFSMem(wgt, val, mem, i - 1, c); - var yes = knapsackDFSMem(wgt, val, mem, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; - // 记录并返回两种方案中价值更大的那一个 - mem[i][c] = @max(no, yes); - return mem[i][c]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1104,30 +1057,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:动态规划 - fn knapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); - // 状态转移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超过背包容量,则不选物品 i - dp[i][c] = dp[i - 1][c]; - } else { - // 不选和选物品 i 这两种方案的较大值 - dp[i][c] = @max(dp[i - 1][c], dp[i - 1][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[n][cap]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1510,29 +1439,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:空间优化后的动态规划 - fn knapsackDPComp(wgt: []i32, val: []i32, comptime cap: usize) i32 { - var n = wgt.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (cap + 1); - // 状态转移 - for (1..n + 1) |i| { - // 倒序遍历 - var c = cap; - while (c > 0) : (c -= 1) { - if (wgt[i - 1] < c) { - // 不选和选物品 i 这两种方案的较大值 - dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[cap]; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_dynamic_programming/summary.md b/docs/chapter_dynamic_programming/summary.md index 6c0453646..b09752f2a 100644 --- a/docs/chapter_dynamic_programming/summary.md +++ b/docs/chapter_dynamic_programming/summary.md @@ -4,6 +4,8 @@ comments: true # 14.7   小结 +### 1.   重点回顾 + - 动态规划对问题进行分解,并通过存储子问题的解来规避重复计算,提高计算效率。 - 不考虑时间的前提下,所有动态规划问题都可以用回溯(暴力搜索)进行求解,但递归树中存在大量的重叠子问题,效率极低。通过引入记忆化列表,可以存储所有计算过的子问题的解,从而保证重叠子问题只被计算一次。 - 记忆化搜索是一种从顶至底的递归式解法,而与之对应的动态规划是一种从底至顶的递推式解法,其如同“填写表格”一样。由于当前状态仅依赖某些局部状态,因此我们可以消除 $dp$ 表的一个维度,从而降低空间复杂度。 diff --git a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index 632158ff9..e1caa811f 100644 --- a/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -371,30 +371,6 @@ $$ end ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - // 完全背包:动态规划 - fn unboundedKnapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); - // 状态转移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超过背包容量,则不选物品 i - dp[i][c] = dp[i - 1][c]; - } else { - // 不选和选物品 i 这两种方案的较大值 - dp[i][c] = @max(dp[i - 1][c], dp[i][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[n][cap]; - } - ``` - ??? pythontutor "可视化运行"
@@ -769,30 +745,6 @@ $$ end ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - // 完全背包:空间优化后的动态规划 - fn unboundedKnapsackDPComp(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (cap + 1); - // 状态转移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超过背包容量,则不选物品 i - dp[c] = dp[c]; - } else { - // 不选和选物品 i 这两种方案的较大值 - dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[cap]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1240,39 +1192,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change.zig" - // 零钱兑换:动态规划 - fn coinChangeDP(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - comptime var max = amt + 1; - // 初始化 dp 表 - var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); - // 状态转移:首行首列 - for (1..amt + 1) |a| { - dp[0][a] = max; - } - // 状态转移:其余行和列 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超过目标金额,则不选硬币 i - dp[i][a] = dp[i - 1][a]; - } else { - // 不选和选硬币 i 这两种方案的较小值 - dp[i][a] = @min(dp[i - 1][a], dp[i][a - @as(usize, @intCast(coins[i - 1]))] + 1); - } - } - } - if (dp[n][amt] != max) { - return @intCast(dp[n][amt]); - } else { - return -1; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -1688,37 +1607,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change.zig" - // 零钱兑换:空间优化后的动态规划 - fn coinChangeDPComp(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - comptime var max = amt + 1; - // 初始化 dp 表 - var dp = [_]i32{0} ** (amt + 1); - @memset(&dp, max); - dp[0] = 0; - // 状态转移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超过目标金额,则不选硬币 i - dp[a] = dp[a]; - } else { - // 不选和选硬币 i 这两种方案的较小值 - dp[a] = @min(dp[a], dp[a - @as(usize, @intCast(coins[i - 1]))] + 1); - } - } - } - if (dp[amt] != max) { - return @intCast(dp[amt]); - } else { - return -1; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -2121,34 +2009,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change_ii.zig" - // 零钱兑换 II:动态规划 - fn coinChangeIIDP(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - // 初始化 dp 表 - var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); - // 初始化首列 - for (0..n + 1) |i| { - dp[i][0] = 1; - } - // 状态转移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超过目标金额,则不选硬币 i - dp[i][a] = dp[i - 1][a]; - } else { - // 不选和选硬币 i 这两种方案的较小值 - dp[i][a] = dp[i - 1][a] + dp[i][a - @as(usize, @intCast(coins[i - 1]))]; - } - } - } - return dp[n][amt]; - } - ``` - ??? pythontutor "可视化运行"
@@ -2485,31 +2345,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change_ii.zig" - // 零钱兑换 II:空间优化后的动态规划 - fn coinChangeIIDPComp(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (amt + 1); - dp[0] = 1; - // 状态转移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超过目标金额,则不选硬币 i - dp[a] = dp[a]; - } else { - // 不选和选硬币 i 这两种方案的较小值 - dp[a] = dp[a] + dp[a - @as(usize, @intCast(coins[i - 1]))]; - } - } - } - return dp[amt]; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_graph/graph_operations.md b/docs/chapter_graph/graph_operations.md index 4b572065f..44ae93b85 100644 --- a/docs/chapter_graph/graph_operations.md +++ b/docs/chapter_graph/graph_operations.md @@ -1206,12 +1206,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="graph_adjacency_matrix.zig" - [class]{GraphAdjMat}-[func]{} - ``` - ??? pythontutor "可视化运行"
@@ -2370,12 +2364,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="graph_adjacency_list.zig" - [class]{GraphAdjList}-[func]{} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_graph/graph_traversal.md b/docs/chapter_graph/graph_traversal.md index 5a0a4b34e..8b90550e3 100644 --- a/docs/chapter_graph/graph_traversal.md +++ b/docs/chapter_graph/graph_traversal.md @@ -475,12 +475,6 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先 end ``` -=== "Zig" - - ```zig title="graph_bfs.zig" - [class]{}-[func]{graphBFS} - ``` - ??? pythontutor "可视化运行"
@@ -941,14 +935,6 @@ BFS 通常借助队列来实现,代码如下所示。队列具有“先入先 end ``` -=== "Zig" - - ```zig title="graph_dfs.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{graphDFS} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_greedy/fractional_knapsack_problem.md b/docs/chapter_greedy/fractional_knapsack_problem.md index 0f324a742..066b890e1 100644 --- a/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/docs/chapter_greedy/fractional_knapsack_problem.md @@ -531,14 +531,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="fractional_knapsack.zig" - [class]{Item}-[func]{} - - [class]{}-[func]{fractionalKnapsack} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_greedy/greedy_algorithm.md b/docs/chapter_greedy/greedy_algorithm.md index 041c9fbaf..4b9f55ef9 100644 --- a/docs/chapter_greedy/greedy_algorithm.md +++ b/docs/chapter_greedy/greedy_algorithm.md @@ -330,12 +330,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="coin_change_greedy.zig" - [class]{}-[func]{coinChangeGreedy} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_greedy/max_capacity_problem.md b/docs/chapter_greedy/max_capacity_problem.md index 546470712..07f37414e 100644 --- a/docs/chapter_greedy/max_capacity_problem.md +++ b/docs/chapter_greedy/max_capacity_problem.md @@ -421,12 +421,6 @@ $$ end ``` -=== "Zig" - - ```zig title="max_capacity.zig" - [class]{}-[func]{maxCapacity} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_greedy/max_product_cutting_problem.md b/docs/chapter_greedy/max_product_cutting_problem.md index 4478fa4b5..086e5c283 100644 --- a/docs/chapter_greedy/max_product_cutting_problem.md +++ b/docs/chapter_greedy/max_product_cutting_problem.md @@ -386,12 +386,6 @@ $$ end ``` -=== "Zig" - - ```zig title="max_product_cutting.zig" - [class]{}-[func]{maxProductCutting} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_greedy/summary.md b/docs/chapter_greedy/summary.md index f857d0305..d3210c3b5 100644 --- a/docs/chapter_greedy/summary.md +++ b/docs/chapter_greedy/summary.md @@ -4,6 +4,8 @@ comments: true # 15.5   小结 +### 1.   重点回顾 + - 贪心算法通常用于解决最优化问题,其原理是在每个决策阶段都做出局部最优的决策,以期获得全局最优解。 - 贪心算法会迭代地做出一个又一个的贪心选择,每轮都将问题转化成一个规模更小的子问题,直到问题被解决。 - 贪心算法不仅实现简单,还具有很高的解题效率。相比于动态规划,贪心算法的时间复杂度通常更低。 diff --git a/docs/chapter_hashing/hash_algorithm.md b/docs/chapter_hashing/hash_algorithm.md index 7d3fff8a0..83ded99b6 100644 --- a/docs/chapter_hashing/hash_algorithm.md +++ b/docs/chapter_hashing/hash_algorithm.md @@ -642,18 +642,6 @@ index = hash(key) % capacity end ``` -=== "Zig" - - ```zig title="simple_hash.zig" - [class]{}-[func]{addHash} - - [class]{}-[func]{mulHash} - - [class]{}-[func]{xorHash} - - [class]{}-[func]{rotHash} - ``` - ??? pythontutor "可视化运行"
@@ -1012,12 +1000,6 @@ $$ # 节点对象 # 的哈希值为 4302940560806366381 ``` -=== "Zig" - - ```zig title="built_in_hash.zig" - - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_hashing/hash_collision.md b/docs/chapter_hashing/hash_collision.md index 9ac350d97..32686c565 100644 --- a/docs/chapter_hashing/hash_collision.md +++ b/docs/chapter_hashing/hash_collision.md @@ -1517,12 +1517,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hash_map_chaining.zig" - [class]{HashMapChaining}-[func]{} - ``` - ??? pythontutor "可视化运行"
@@ -3288,12 +3282,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hash_map_open_addressing.zig" - [class]{HashMapOpenAddressing}-[func]{} - ``` - ### 2.   平方探测 平方探测与线性探测类似,都是开放寻址的常见策略之一。当发生冲突时,平方探测不是简单地跳过一个固定的步数,而是跳过“探测次数的平方”的步数,即 $1, 4, 9, \dots$ 步。 diff --git a/docs/chapter_hashing/hash_map.md b/docs/chapter_hashing/hash_map.md index d5c1b56ef..9b60dbfc0 100755 --- a/docs/chapter_hashing/hash_map.md +++ b/docs/chapter_hashing/hash_map.md @@ -323,12 +323,6 @@ comments: true hmap.delete(10583) ``` -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -551,12 +545,6 @@ comments: true hmap.values.each { |val| puts val } ``` -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -1765,115 +1753,6 @@ index = hash(key) % capacity end ``` -=== "Zig" - - ```zig title="array_hash_map.zig" - // 键值对 - const Pair = struct { - key: usize = undefined, - val: []const u8 = undefined, - - pub fn init(key: usize, val: []const u8) Pair { - return Pair { - .key = key, - .val = val, - }; - } - }; - - // 基于数组实现的哈希表 - fn ArrayHashMap(comptime T: type) type { - return struct { - bucket: ?std.ArrayList(?T) = null, - mem_allocator: std.mem.Allocator = undefined, - - const Self = @This(); - - // 构造函数 - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - self.mem_allocator = allocator; - // 初始化一个长度为 100 的桶(数组) - self.bucket = std.ArrayList(?T).init(self.mem_allocator); - var i: i32 = 0; - while (i < 100) : (i += 1) { - try self.bucket.?.append(null); - } - } - - // 析构函数 - pub fn deinit(self: *Self) void { - if (self.bucket != null) self.bucket.?.deinit(); - } - - // 哈希函数 - fn hashFunc(key: usize) usize { - var index = key % 100; - return index; - } - - // 查询操作 - pub fn get(self: *Self, key: usize) []const u8 { - var index = hashFunc(key); - var pair = self.bucket.?.items[index]; - return pair.?.val; - } - - // 添加操作 - pub fn put(self: *Self, key: usize, val: []const u8) !void { - var pair = Pair.init(key, val); - var index = hashFunc(key); - self.bucket.?.items[index] = pair; - } - - // 删除操作 - pub fn remove(self: *Self, key: usize) !void { - var index = hashFunc(key); - // 置为 null ,代表删除 - self.bucket.?.items[index] = null; - } - - // 获取所有键值对 - pub fn pairSet(self: *Self) !std.ArrayList(T) { - var entry_set = std.ArrayList(T).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try entry_set.append(item.?); - } - return entry_set; - } - - // 获取所有键 - pub fn keySet(self: *Self) !std.ArrayList(usize) { - var key_set = std.ArrayList(usize).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try key_set.append(item.?.key); - } - return key_set; - } - - // 获取所有值 - pub fn valueSet(self: *Self) !std.ArrayList([]const u8) { - var value_set = std.ArrayList([]const u8).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try value_set.append(item.?.val); - } - return value_set; - } - - // 打印哈希表 - pub fn print(self: *Self) !void { - var entry_set = try self.pairSet(); - defer entry_set.deinit(); - for (entry_set.items) |item| { - std.debug.print("{} -> {s}\n", .{item.key, item.val}); - } - } - }; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_heap/build_heap.md b/docs/chapter_heap/build_heap.md index ac3a8134c..d660699a0 100644 --- a/docs/chapter_heap/build_heap.md +++ b/docs/chapter_heap/build_heap.md @@ -322,23 +322,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 构造方法,根据输入列表建堆 - fn init(self: *Self, allocator: std.mem.Allocator, nums: []const T) !void { - if (self.max_heap != null) return; - self.max_heap = std.ArrayList(T).init(allocator); - // 将列表元素原封不动添加进堆 - try self.max_heap.?.appendSlice(nums); - // 堆化除叶节点以外的其他所有节点 - var i: usize = parent(self.size() - 1) + 1; - while (i > 0) : (i -= 1) { - try self.siftDown(i - 1); - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_heap/heap.md b/docs/chapter_heap/heap.md index 31748fbba..cb0f30c23 100644 --- a/docs/chapter_heap/heap.md +++ b/docs/chapter_heap/heap.md @@ -418,12 +418,6 @@ comments: true # Ruby 未提供内置 Heap 类 ``` -=== "Zig" - - ```zig title="heap.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -692,26 +686,6 @@ comments: true end ``` -=== "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.   访问堆顶元素 堆顶元素即为二叉树的根节点,也就是列表的首个元素: @@ -832,15 +806,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 访问堆顶元素 - fn peek(self: *Self) T { - return self.max_heap.?.items[0]; - } - ``` - ??? pythontutor "可视化运行"
@@ -1246,33 +1211,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 元素入堆 - fn push(self: *Self, val: T) !void { - // 添加节点 - try self.max_heap.?.append(val); - // 从底至顶堆化 - try self.siftUp(self.size() - 1); - } - - // 从节点 i 开始,从底至顶堆化 - fn siftUp(self: *Self, i_: usize) !void { - var i = i_; - while (true) { - // 获取节点 i 的父节点 - var p = parent(i); - // 当“越过根节点”或“节点无须修复”时,结束堆化 - if (p < 0 or self.max_heap.?.items[i] <= self.max_heap.?.items[p]) break; - // 交换两节点 - try self.swap(i, p); - // 循环向上堆化 - i = p; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -1830,43 +1768,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 元素出堆 - fn pop(self: *Self) !T { - // 判断处理 - if (self.isEmpty()) unreachable; - // 交换根节点与最右叶节点(交换首元素与尾元素) - try self.swap(0, self.size() - 1); - // 删除节点 - var val = self.max_heap.?.pop(); - // 从顶至底堆化 - try self.siftDown(0); - // 返回堆顶元素 - return val; - } - - // 从节点 i 开始,从顶至底堆化 - fn siftDown(self: *Self, i_: usize) !void { - var i = i_; - while (true) { - // 判断节点 i, l, r 中值最大的节点,记为 ma - var l = left(i); - var r = right(i); - var ma = i; - if (l < self.size() and self.max_heap.?.items[l] > self.max_heap.?.items[ma]) ma = l; - if (r < self.size() and self.max_heap.?.items[r] > self.max_heap.?.items[ma]) ma = r; - // 若节点 i 最大或索引 l, r 越界,则无须继续堆化,跳出 - if (ma == i) break; - // 交换两节点 - try self.swap(i, ma); - // 循环向下堆化 - i = ma; - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_heap/top_k.md b/docs/chapter_heap/top_k.md index 2df17ac33..834ce5d8a 100644 --- a/docs/chapter_heap/top_k.md +++ b/docs/chapter_heap/top_k.md @@ -461,12 +461,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="top_k.zig" - [class]{}-[func]{topKHeap} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_introduction/summary.md b/docs/chapter_introduction/summary.md index d619842cd..6d77588fb 100644 --- a/docs/chapter_introduction/summary.md +++ b/docs/chapter_introduction/summary.md @@ -4,6 +4,8 @@ comments: true # 1.3   小结 +### 1.   重点回顾 + - 算法在日常生活中无处不在,并不是遥不可及的高深知识。实际上,我们已经在不知不觉中学会了许多算法,用以解决生活中的大小问题。 - 查字典的原理与二分查找算法相一致。二分查找算法体现了分而治之的重要算法思想。 - 整理扑克的过程与插入排序算法非常类似。插入排序算法适合排序小型数据集。 @@ -12,7 +14,7 @@ comments: true - 数据结构与算法紧密相连。数据结构是算法的基石,而算法为数据结构注入生命力。 - 我们可以将数据结构与算法类比为拼装积木,积木代表数据,积木的形状和连接方式等代表数据结构,拼装积木的步骤则对应算法。 -### 1.   Q & A +### 2.   Q & A **Q**:作为一名程序员,我在日常工作中从未用算法解决过问题,常用算法都被编程语言封装好了,直接用就可以了;这是否意味着我们工作中的问题还没有到达需要算法的程度? diff --git a/docs/chapter_preface/suggestions.md b/docs/chapter_preface/suggestions.md index 89831b0fb..cdf44cb29 100644 --- a/docs/chapter_preface/suggestions.md +++ b/docs/chapter_preface/suggestions.md @@ -184,17 +184,6 @@ comments: true # 注释 ``` -=== "Zig" - - ```zig title="" - // 标题注释,用于标注函数、类、测试样例等 - - // 内容注释,用于详解代码 - - // 多行 - // 注释 - ``` - ## 0.2.2   在动画图解中高效学习 相较于文字,视频和图片具有更高的信息密度和结构化程度,更易于理解。在本书中,**重点和难点知识将主要通过动画以图解形式展示**,而文字则作为解释与补充。 diff --git a/docs/chapter_preface/summary.md b/docs/chapter_preface/summary.md index e701daac5..afecca771 100644 --- a/docs/chapter_preface/summary.md +++ b/docs/chapter_preface/summary.md @@ -4,6 +4,8 @@ comments: true # 0.3   小结 +### 1.   重点回顾 + - 本书的主要受众是算法初学者。如果你已有一定基础,本书能帮助你系统回顾算法知识,书中源代码也可作为“刷题工具库”使用。 - 书中内容主要包括复杂度分析、数据结构和算法三部分,涵盖了该领域的大部分主题。 - 对于算法新手,在初学阶段阅读一本入门书至关重要,可以少走许多弯路。 diff --git a/docs/chapter_searching/binary_search.md b/docs/chapter_searching/binary_search.md index ed755432b..8b6e64657 100755 --- a/docs/chapter_searching/binary_search.md +++ b/docs/chapter_searching/binary_search.md @@ -362,30 +362,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search.zig" - // 二分查找(双闭区间) - fn binarySearch(comptime T: type, nums: std.ArrayList(T), target: T) T { - // 初始化双闭区间 [0, n-1] ,即 i, j 分别指向数组首元素、尾元素 - var i: usize = 0; - var j: usize = nums.items.len - 1; - // 循环,当搜索区间为空时跳出(当 i > j 时为空) - while (i <= j) { - var m = i + (j - i) / 2; // 计算中点索引 m - if (nums.items[m] < target) { // 此情况说明 target 在区间 [m+1, j] 中 - i = m + 1; - } else if (nums.items[m] > target) { // 此情况说明 target 在区间 [i, m-1] 中 - j = m - 1; - } else { // 找到目标元素,返回其索引 - return @intCast(m); - } - } - // 未找到目标元素,返回 -1 - return -1; - } - ``` - ??? pythontutor "可视化运行"
@@ -710,30 +686,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search.zig" - // 二分查找(左闭右开区间) - fn binarySearchLCRO(comptime T: type, nums: std.ArrayList(T), target: T) T { - // 初始化左闭右开区间 [0, n) ,即 i, j 分别指向数组首元素、尾元素+1 - var i: usize = 0; - var j: usize = nums.items.len; - // 循环,当搜索区间为空时跳出(当 i = j 时为空) - while (i <= j) { - var m = i + (j - i) / 2; // 计算中点索引 m - if (nums.items[m] < target) { // 此情况说明 target 在区间 [m+1, j) 中 - i = m + 1; - } else if (nums.items[m] > target) { // 此情况说明 target 在区间 [i, m) 中 - j = m; - } else { // 找到目标元素,返回其索引 - return @intCast(m); - } - } - // 未找到目标元素,返回 -1 - return -1; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_searching/binary_search_edge.md b/docs/chapter_searching/binary_search_edge.md index 7213e300a..c4a498d2f 100644 --- a/docs/chapter_searching/binary_search_edge.md +++ b/docs/chapter_searching/binary_search_edge.md @@ -224,12 +224,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchLeftEdge} - ``` - ??? pythontutor "可视化运行"
@@ -485,12 +479,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchRightEdge} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_searching/binary_search_insertion.md b/docs/chapter_searching/binary_search_insertion.md index e8f466343..2ab42aae1 100644 --- a/docs/chapter_searching/binary_search_insertion.md +++ b/docs/chapter_searching/binary_search_insertion.md @@ -315,12 +315,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertionSimple} - ``` - ??? pythontutor "可视化运行"
@@ -666,12 +660,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertion} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_searching/replace_linear_by_hashing.md b/docs/chapter_searching/replace_linear_by_hashing.md index 2d80fc7c1..6225bc7d2 100755 --- a/docs/chapter_searching/replace_linear_by_hashing.md +++ b/docs/chapter_searching/replace_linear_by_hashing.md @@ -241,26 +241,6 @@ comments: true end ``` -=== "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; - } - ``` - ??? pythontutor "可视化运行"
@@ -556,27 +536,6 @@ comments: true end ``` -=== "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; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_searching/summary.md b/docs/chapter_searching/summary.md index a5ffa5e3e..d77921efd 100644 --- a/docs/chapter_searching/summary.md +++ b/docs/chapter_searching/summary.md @@ -4,6 +4,8 @@ comments: true # 10.6   小结 +### 1.   重点回顾 + - 二分查找依赖数据的有序性,通过循环逐步缩减一半搜索区间来进行查找。它要求输入数据有序,且仅适用于数组或基于数组实现的数据结构。 - 暴力搜索通过遍历数据结构来定位数据。线性搜索适用于数组和链表,广度优先搜索和深度优先搜索适用于图和树。此类算法通用性好,无须对数据进行预处理,但时间复杂度 $O(n)$ 较高。 - 哈希查找、树查找和二分查找属于高效搜索方法,可在特定数据结构中快速定位目标元素。此类算法效率高,时间复杂度可达 $O(\log n)$ 甚至 $O(1)$ ,但通常需要借助额外数据结构。 diff --git a/docs/chapter_sorting/bubble_sort.md b/docs/chapter_sorting/bubble_sort.md index 02b292974..5888bbd7f 100755 --- a/docs/chapter_sorting/bubble_sort.md +++ b/docs/chapter_sorting/bubble_sort.md @@ -290,28 +290,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - // 冒泡排序 - fn bubbleSort(nums: []i32) void { - // 外循环:未排序区间为 [0, i] - var i: usize = nums.len - 1; - while (i > 0) : (i -= 1) { - var j: usize = 0; - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交换 nums[j] 与 nums[j + 1] - var tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - } - } - } - } - ``` - ??? pythontutor "可视化运行"
@@ -617,31 +595,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - // 冒泡排序(标志优化) - fn bubbleSortWithFlag(nums: []i32) void { - // 外循环:未排序区间为 [0, i] - var i: usize = nums.len - 1; - while (i > 0) : (i -= 1) { - var flag = false; // 初始化标志位 - var j: usize = 0; - // 内循环:将未排序区间 [0, i] 中的最大元素交换至该区间的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交换 nums[j] 与 nums[j + 1] - var tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - flag = true; - } - } - if (!flag) break; // 此轮“冒泡”未交换任何元素,直接跳出 - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/bucket_sort.md b/docs/chapter_sorting/bucket_sort.md index 7230be435..5a18c341a 100644 --- a/docs/chapter_sorting/bucket_sort.md +++ b/docs/chapter_sorting/bucket_sort.md @@ -437,12 +437,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bucket_sort.zig" - [class]{}-[func]{bucketSort} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/counting_sort.md b/docs/chapter_sorting/counting_sort.md index a0ecfdc44..eebe7e919 100644 --- a/docs/chapter_sorting/counting_sort.md +++ b/docs/chapter_sorting/counting_sort.md @@ -361,12 +361,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSortNaive} - ``` - ??? pythontutor "可视化运行"
@@ -883,12 +877,6 @@ $$ end ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSort} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/heap_sort.md b/docs/chapter_sorting/heap_sort.md index 2fdc95637..274fe3310 100644 --- a/docs/chapter_sorting/heap_sort.md +++ b/docs/chapter_sorting/heap_sort.md @@ -612,14 +612,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="heap_sort.zig" - [class]{}-[func]{siftDown} - - [class]{}-[func]{heapSort} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/insertion_sort.md b/docs/chapter_sorting/insertion_sort.md index 1dee1b995..8444051ef 100755 --- a/docs/chapter_sorting/insertion_sort.md +++ b/docs/chapter_sorting/insertion_sort.md @@ -270,25 +270,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="insertion_sort.zig" - // 插入排序 - fn insertionSort(nums: []i32) void { - // 外循环:已排序区间为 [0, i-1] - var i: usize = 1; - while (i < nums.len) : (i += 1) { - var base = nums[i]; - var j: usize = i; - // 内循环:将 base 插入到已排序区间 [0, i-1] 中的正确位置 - while (j >= 1 and nums[j - 1] > base) : (j -= 1) { - nums[j] = nums[j - 1]; // 将 nums[j] 向右移动一位 - } - nums[j] = base; // 将 base 赋值到正确位置 - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/merge_sort.md b/docs/chapter_sorting/merge_sort.md index 2af130e60..2c15f8c18 100755 --- a/docs/chapter_sorting/merge_sort.md +++ b/docs/chapter_sorting/merge_sort.md @@ -679,60 +679,6 @@ comments: true end ``` -=== "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 - left) / 2; // 计算中点 - try mergeSort(nums, left, mid); // 递归左子数组 - try mergeSort(nums, mid + 1, right); // 递归右子数组 - // 合并阶段 - try merge(nums, left, mid, right); - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/quick_sort.md b/docs/chapter_sorting/quick_sort.md index b4e1ea2ad..0d6c486c4 100755 --- a/docs/chapter_sorting/quick_sort.md +++ b/docs/chapter_sorting/quick_sort.md @@ -366,31 +366,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 元素交换 - fn swap(nums: []i32, i: usize, j: usize) void { - var tmp = nums[i]; - nums[i] = nums[j]; - nums[j] = tmp; - } - - // 哨兵划分 - fn partition(nums: []i32, left: usize, right: usize) usize { - // 以 nums[left] 为基准数 - var i = left; - var j = right; - while (i < j) { - while (i < j and nums[j] >= nums[left]) j -= 1; // 从右向左找首个小于基准数的元素 - while (i < j and nums[i] <= nums[left]) i += 1; // 从左向右找首个大于基准数的元素 - swap(nums, i, j); // 交换这两个元素 - } - swap(nums, i, left); // 将基准数交换至两子数组的分界线 - return i; // 返回基准数的索引 - } - ``` - ??? pythontutor "可视化运行"
@@ -618,21 +593,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 快速排序 - fn quickSort(nums: []i32, left: usize, right: usize) void { - // 子数组长度为 1 时终止递归 - if (left >= right) return; - // 哨兵划分 - var pivot = partition(nums, left, right); - // 递归左子数组、右子数组 - quickSort(nums, left, pivot - 1); - quickSort(nums, pivot + 1, right); - } - ``` - ??? pythontutor "可视化运行"
@@ -1122,40 +1082,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 选取三个候选元素的中位数 - fn medianThree(nums: []i32, left: usize, mid: usize, right: usize) usize { - var l = nums[left]; - var m = nums[mid]; - var r = nums[right]; - if ((l <= m && m <= r) || (r <= m && m <= l)) - return mid; // m 在 l 和 r 之间 - if ((m <= l && l <= r) || (r <= l && l <= m)) - return left; // l 在 m 和 r 之间 - return right; - } - - // 哨兵划分(三数取中值) - fn partition(nums: []i32, left: usize, right: usize) usize { - // 选取三个候选元素的中位数 - var med = medianThree(nums, left, (left + right) / 2, right); - // 将中位数交换至数组最左端 - swap(nums, left, med); - // 以 nums[left] 为基准数 - var i = left; - var j = right; - while (i < j) { - while (i < j and nums[j] >= nums[left]) j -= 1; // 从右向左找首个小于基准数的元素 - while (i < j and nums[i] <= nums[left]) i += 1; // 从左向右找首个大于基准数的元素 - swap(nums, i, j); // 交换这两个元素 - } - swap(nums, i, left); // 将基准数交换至两子数组的分界线 - return i; // 返回基准数的索引 - } - ``` - ??? pythontutor "可视化运行"
@@ -1445,29 +1371,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 快速排序(递归深度优化) - fn quickSort(nums: []i32, left_: usize, right_: usize) void { - var left = left_; - var right = right_; - // 子数组长度为 1 时终止递归 - while (left < right) { - // 哨兵划分操作 - var pivot = partition(nums, left, right); - // 对两个子数组中较短的那个执行快速排序 - if (pivot - left < right - pivot) { - quickSort(nums, left, pivot - 1); // 递归排序左子数组 - left = pivot + 1; // 剩余未排序区间为 [pivot + 1, right] - } else { - quickSort(nums, pivot + 1, right); // 递归排序右子数组 - right = pivot - 1; // 剩余未排序区间为 [left, pivot - 1] - } - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/radix_sort.md b/docs/chapter_sorting/radix_sort.md index c0210173c..e6fe97061 100644 --- a/docs/chapter_sorting/radix_sort.md +++ b/docs/chapter_sorting/radix_sort.md @@ -716,70 +716,6 @@ $$ end ``` -=== "Zig" - - ```zig title="radix_sort.zig" - // 获取元素 num 的第 k 位,其中 exp = 10^(k-1) - fn digit(num: i32, exp: i32) i32 { - // 传入 exp 而非 k 可以避免在此重复执行昂贵的次方计算 - return @mod(@divFloor(num, exp), 10); - } - - // 计数排序(根据 nums 第 k 位排序) - fn countingSortDigit(nums: []i32, exp: i32) !void { - // 十进制的位范围为 0~9 ,因此需要长度为 10 的桶数组 - var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - // defer mem_arena.deinit(); - const mem_allocator = mem_arena.allocator(); - var counter = try mem_allocator.alloc(usize, 10); - @memset(counter, 0); - var n = nums.len; - // 统计 0~9 各数字的出现次数 - for (nums) |num| { - var d: u32 = @bitCast(digit(num, exp)); // 获取 nums[i] 第 k 位,记为 d - counter[d] += 1; // 统计数字 d 的出现次数 - } - // 求前缀和,将“出现个数”转换为“数组索引” - var i: usize = 1; - while (i < 10) : (i += 1) { - counter[i] += counter[i - 1]; - } - // 倒序遍历,根据桶内统计结果,将各元素填入 res - var res = try mem_allocator.alloc(i32, n); - i = n - 1; - while (i >= 0) : (i -= 1) { - var d: u32 = @bitCast(digit(nums[i], exp)); - var j = counter[d] - 1; // 获取 d 在数组中的索引 j - res[j] = nums[i]; // 将当前元素填入索引 j - counter[d] -= 1; // 将 d 的数量减 1 - if (i == 0) break; - } - // 使用结果覆盖原数组 nums - i = 0; - while (i < n) : (i += 1) { - nums[i] = res[i]; - } - } - - // 基数排序 - fn radixSort(nums: []i32) !void { - // 获取数组的最大元素,用于判断最大位数 - var m: i32 = std.math.minInt(i32); - for (nums) |num| { - if (num > m) m = num; - } - // 按照从低位到高位的顺序遍历 - var exp: i32 = 1; - while (exp <= m) : (exp *= 10) { - // 对数组元素的第 k 位执行计数排序 - // k = 1 -> exp = 1 - // k = 2 -> exp = 10 - // 即 exp = 10^(k-1) - try countingSortDigit(nums, exp); - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_sorting/selection_sort.md b/docs/chapter_sorting/selection_sort.md index 680bc5809..78fe1d49c 100644 --- a/docs/chapter_sorting/selection_sort.md +++ b/docs/chapter_sorting/selection_sort.md @@ -324,12 +324,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="selection_sort.zig" - [class]{}-[func]{selectionSort} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_stack_and_queue/deque.md b/docs/chapter_stack_and_queue/deque.md index 29a01455c..26cd57cfd 100644 --- a/docs/chapter_stack_and_queue/deque.md +++ b/docs/chapter_stack_and_queue/deque.md @@ -393,12 +393,6 @@ comments: true is_empty = size.zero? ``` -=== "Zig" - - ```zig title="deque.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -2160,166 +2154,6 @@ comments: true end ``` -=== "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 所示,与基于数组实现队列类似,我们也可以使用环形数组来实现双向队列。 @@ -3768,12 +3602,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_deque.zig" - [class]{ArrayDeque}-[func]{} - ``` - ## 5.3.3   双向队列应用 双向队列兼具栈与队列的逻辑,**因此它可以实现这两者的所有应用场景,同时提供更高的自由度**。 diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 4f012b182..6698b9650 100755 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -366,12 +366,6 @@ comments: true is_empty = queue.empty? ``` -=== "Zig" - - ```zig title="queue.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -1317,95 +1311,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linkedlist_queue.zig" - // 基于链表实现的队列 - fn LinkedListQueue(comptime T: type) type { - return struct { - const Self = @This(); - - front: ?*inc.ListNode(T) = null, // 头节点 front - rear: ?*inc.ListNode(T) = null, // 尾节点 rear - que_size: usize = 0, // 队列的长度 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 内存分配器 - - // 构造函数(分配内存+初始化队列) - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.front = null; - self.rear = null; - self.que_size = 0; - } - - // 析构函数(释放内存) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 获取队列的长度 - pub fn size(self: *Self) usize { - return self.que_size; - } - - // 判断队列是否为空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 访问队首元素 - pub fn peek(self: *Self) T { - if (self.size() == 0) @panic("队列为空"); - return self.front.?.val; - } - - // 入队 - pub fn push(self: *Self, num: T) !void { - // 在尾节点后添加 num - var node = try self.mem_allocator.create(inc.ListNode(T)); - node.init(num); - // 如果队列为空,则令头、尾节点都指向该节点 - if (self.front == null) { - self.front = node; - self.rear = node; - // 如果队列不为空,则将该节点添加到尾节点后 - } else { - self.rear.?.next = node; - self.rear = node; - } - self.que_size += 1; - } - - // 出队 - pub fn pop(self: *Self) T { - var num = self.peek(); - // 删除头节点 - self.front = self.front.?.next; - self.que_size -= 1; - return num; - } - - // 将链表转换为数组 - pub fn toArray(self: *Self) ![]T { - var node = self.front; - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - while (i < res.len) : (i += 1) { - res[i] = node.?.val; - node = node.?.next; - } - return res; - } - }; - } - ``` - ??? pythontutor "可视化运行"
@@ -2381,98 +2286,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_queue.zig" - // 基于环形数组实现的队列 - fn ArrayQueue(comptime T: type) type { - return struct { - const Self = @This(); - - nums: []T = undefined, // 用于存储队列元素的数组 - cap: usize = 0, // 队列容量 - front: usize = 0, // 队首指针,指向队首元素 - queSize: usize = 0, // 尾指针,指向队尾 + 1 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 内存分配器 - - // 构造函数(分配内存+初始化数组) - pub fn init(self: *Self, allocator: std.mem.Allocator, cap: usize) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.cap = cap; - self.nums = try self.mem_allocator.alloc(T, self.cap); - @memset(self.nums, @as(T, 0)); - } - - // 析构函数(释放内存) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 获取队列的容量 - pub fn capacity(self: *Self) usize { - return self.cap; - } - - // 获取队列的长度 - pub fn size(self: *Self) usize { - return self.queSize; - } - - // 判断队列是否为空 - pub fn isEmpty(self: *Self) bool { - return self.queSize == 0; - } - - // 入队 - pub fn push(self: *Self, num: T) !void { - if (self.size() == self.capacity()) { - std.debug.print("队列已满\n", .{}); - return; - } - // 计算队尾指针,指向队尾索引 + 1 - // 通过取余操作实现 rear 越过数组尾部后回到头部 - var rear = (self.front + self.queSize) % self.capacity(); - // 在尾节点后添加 num - self.nums[rear] = num; - self.queSize += 1; - } - - // 出队 - pub fn pop(self: *Self) T { - var num = self.peek(); - // 队首指针向后移动一位,若越过尾部,则返回到数组头部 - self.front = (self.front + 1) % self.capacity(); - self.queSize -= 1; - return num; - } - - // 访问队首元素 - pub fn peek(self: *Self) T { - if (self.isEmpty()) @panic("队列为空"); - return self.nums[self.front]; - } - - // 返回数组 - pub fn toArray(self: *Self) ![]T { - // 仅转换有效长度范围内的列表元素 - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - var j: usize = self.front; - while (i < self.size()) : ({ i += 1; j += 1; }) { - res[i] = self.nums[j % self.capacity()]; - } - return res; - } - }; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index 79f1a6a0e..b866b70a3 100755 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -359,12 +359,6 @@ comments: true is_empty = stack.empty? ``` -=== "Zig" - - ```zig title="stack.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -1166,84 +1160,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linkedlist_stack.zig" - // 基于链表实现的栈 - fn LinkedListStack(comptime T: type) type { - return struct { - const Self = @This(); - - stack_top: ?*inc.ListNode(T) = null, // 将头节点作为栈顶 - stk_size: usize = 0, // 栈的长度 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 内存分配器 - - // 构造函数(分配内存+初始化栈) - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.stack_top = null; - self.stk_size = 0; - } - - // 析构函数(释放内存) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 获取栈的长度 - pub fn size(self: *Self) usize { - return self.stk_size; - } - - // 判断栈是否为空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 访问栈顶元素 - pub fn peek(self: *Self) T { - if (self.size() == 0) @panic("栈为空"); - return self.stack_top.?.val; - } - - // 入栈 - pub fn push(self: *Self, num: T) !void { - var node = try self.mem_allocator.create(inc.ListNode(T)); - node.init(num); - node.next = self.stack_top; - self.stack_top = node; - self.stk_size += 1; - } - - // 出栈 - pub fn pop(self: *Self) T { - var num = self.peek(); - self.stack_top = self.stack_top.?.next; - self.stk_size -= 1; - return num; - } - - // 将栈转换为数组 - pub fn toArray(self: *Self) ![]T { - var node = self.stack_top; - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - while (i < res.len) : (i += 1) { - res[res.len - i - 1] = node.?.val; - node = node.?.next; - } - return res; - } - }; - } - ``` - ??? pythontutor "可视化运行"
@@ -1886,64 +1802,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_stack.zig" - // 基于数组实现的栈 - fn ArrayStack(comptime T: type) type { - return struct { - const Self = @This(); - - stack: ?std.ArrayList(T) = null, - - // 构造方法(分配内存+初始化栈) - pub fn init(self: *Self, allocator: std.mem.Allocator) void { - if (self.stack == null) { - self.stack = std.ArrayList(T).init(allocator); - } - } - - // 析构方法(释放内存) - pub fn deinit(self: *Self) void { - if (self.stack == null) return; - self.stack.?.deinit(); - } - - // 获取栈的长度 - pub fn size(self: *Self) usize { - return self.stack.?.items.len; - } - - // 判断栈是否为空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 访问栈顶元素 - pub fn peek(self: *Self) T { - if (self.isEmpty()) @panic("栈为空"); - return self.stack.?.items[self.size() - 1]; - } - - // 入栈 - pub fn push(self: *Self, num: T) !void { - try self.stack.?.append(num); - } - - // 出栈 - pub fn pop(self: *Self) T { - var num = self.stack.?.pop(); - return num; - } - - // 返回 ArrayList - pub fn toList(self: *Self) std.ArrayList(T) { - return self.stack.?; - } - }; - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_tree/array_representation_of_tree.md b/docs/chapter_tree/array_representation_of_tree.md index 47ff9a23c..1c81cad15 100644 --- a/docs/chapter_tree/array_representation_of_tree.md +++ b/docs/chapter_tree/array_representation_of_tree.md @@ -136,12 +136,6 @@ comments: true tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` -=== "Zig" - - ```zig title="" - - ``` - ![任意类型二叉树的数组表示](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }

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

@@ -1334,12 +1328,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_binary_tree.zig" - [class]{ArrayBinaryTree}-[func]{} - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_tree/avl_tree.md b/docs/chapter_tree/avl_tree.md index 3648fe73f..e2ee21221 100644 --- a/docs/chapter_tree/avl_tree.md +++ b/docs/chapter_tree/avl_tree.md @@ -236,12 +236,6 @@ AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二 end ``` -=== "Zig" - - ```zig title="" - - ``` - “节点高度”是指从该节点到它的最远叶节点的距离,即所经过的“边”的数量。需要特别注意的是,叶节点的高度为 $0$ ,而空节点的高度为 $-1$ 。我们将创建两个工具函数,分别用于获取和更新节点的高度: === "Python" @@ -481,23 +475,6 @@ AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二 end ``` -=== "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$ 。我们同样将获取节点平衡因子的功能封装成函数,方便后续使用: @@ -669,18 +646,6 @@ AVL 树既是二叉搜索树,也是平衡二叉树,同时满足这两类二 end ``` -=== "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); - } - ``` - !!! tip 设平衡因子为 $f$ ,则一棵 AVL 树的任意节点的平衡因子皆满足 $-1 \le f \le 1$ 。 @@ -956,24 +921,6 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中 end ``` -=== "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 所示的“左旋”操作。 @@ -1229,24 +1176,6 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中 end ``` -=== "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` 执行“右旋”。 @@ -1730,40 +1659,6 @@ AVL 树的特点在于“旋转”操作,它能够在不影响二叉树的中 end ``` -=== "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.   插入节点 @@ -2142,38 +2037,6 @@ AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区 end ``` -=== "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.   删除节点 类似地,在二叉搜索树的删除节点方法的基础上,需要从底至顶执行旋转操作,使所有失衡节点恢复平衡。代码如下所示: @@ -2775,51 +2638,6 @@ AVL 树的节点插入操作与二叉搜索树在主体上类似。唯一的区 end ``` -=== "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 树的节点查找操作与二叉搜索树一致,在此不再赘述。 diff --git a/docs/chapter_tree/binary_search_tree.md b/docs/chapter_tree/binary_search_tree.md index 0602ab231..1cdd5bcc2 100755 --- a/docs/chapter_tree/binary_search_tree.md +++ b/docs/chapter_tree/binary_search_tree.md @@ -338,30 +338,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 查找节点 - fn search(self: *Self, num: T) ?*inc.TreeNode(T) { - var cur = self.root; - // 循环查找,越过叶节点后跳出 - while (cur != null) { - // 目标节点在 cur 的右子树中 - if (cur.?.val < num) { - cur = cur.?.right; - // 目标节点在 cur 的左子树中 - } else if (cur.?.val > num) { - cur = cur.?.left; - // 找到目标节点,跳出循环 - } else { - break; - } - } - // 返回目标节点 - return cur; - } - ``` - ??? pythontutor "可视化运行"
@@ -826,42 +802,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 插入节点 - fn insert(self: *Self, num: T) !void { - // 若树为空,则初始化根节点 - if (self.root == null) { - self.root = try self.mem_allocator.create(inc.TreeNode(T)); - return; - } - var cur = self.root; - var pre: ?*inc.TreeNode(T) = null; - // 循环查找,越过叶节点后跳出 - while (cur != null) { - // 找到重复节点,直接返回 - if (cur.?.val == num) return; - pre = cur; - // 插入位置在 cur 的右子树中 - if (cur.?.val < num) { - cur = cur.?.right; - // 插入位置在 cur 的左子树中 - } else { - cur = cur.?.left; - } - } - // 插入节点 - var node = try self.mem_allocator.create(inc.TreeNode(T)); - node.init(num); - if (pre.?.val < num) { - pre.?.right = node; - } else { - pre.?.left = node; - } - } - ``` - ??? pythontutor "可视化运行"
@@ -1648,56 +1588,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 删除节点 - fn remove(self: *Self, num: T) void { - // 若树为空,直接提前返回 - if (self.root == null) return; - var cur = self.root; - var pre: ?*inc.TreeNode(T) = null; - // 循环查找,越过叶节点后跳出 - while (cur != null) { - // 找到待删除节点,跳出循环 - if (cur.?.val == num) break; - pre = cur; - // 待删除节点在 cur 的右子树中 - if (cur.?.val < num) { - cur = cur.?.right; - // 待删除节点在 cur 的左子树中 - } else { - cur = cur.?.left; - } - } - // 若无待删除节点,则直接返回 - if (cur == null) return; - // 子节点数量 = 0 or 1 - if (cur.?.left == null or cur.?.right == null) { - // 当子节点数量 = 0 / 1 时, child = null / 该子节点 - var child = if (cur.?.left != null) cur.?.left else cur.?.right; - // 删除节点 cur - if (pre.?.left == cur) { - pre.?.left = child; - } else { - pre.?.right = child; - } - // 子节点数量 = 2 - } else { - // 获取中序遍历中 cur 的下一个节点 - var tmp = cur.?.right; - while (tmp.?.left != null) { - tmp = tmp.?.left; - } - var tmp_val = tmp.?.val; - // 递归删除节点 tmp - self.remove(tmp.?.val); - // 用 tmp 覆盖 cur - cur.?.val = tmp_val; - } - } - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_tree/binary_tree.md b/docs/chapter_tree/binary_tree.md index 84e788d8e..93548fd37 100644 --- a/docs/chapter_tree/binary_tree.md +++ b/docs/chapter_tree/binary_tree.md @@ -205,12 +205,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 每个节点都有两个引用(指针),分别指向左子节点(left-child node)右子节点(right-child node),该节点被称为这两个子节点的父节点(parent node)。当给定一个二叉树的节点时,我们将该节点的左子节点及其以下节点形成的树称为该节点的左子树(left subtree),同理可得右子树(right subtree)。 **在二叉树中,除叶节点外,其他所有节点都包含子节点和非空子树**。如图 7-1 所示,如果将“节点 2”视为父节点,则其左子节点和右子节点分别是“节点 4”和“节点 5”,左子树是“节点 4 及其以下节点形成的树”,右子树是“节点 5 及其以下节点形成的树”。 @@ -463,12 +457,6 @@ comments: true n2.right = n5 ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - ??? pythontutor "可视化运行"
@@ -638,12 +626,6 @@ comments: true n1.left = n2 ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - ??? pythontutor "可视化运行"
diff --git a/docs/chapter_tree/binary_tree_traversal.md b/docs/chapter_tree/binary_tree_traversal.md index d0a90e11c..9dbbc550a 100755 --- a/docs/chapter_tree/binary_tree_traversal.md +++ b/docs/chapter_tree/binary_tree_traversal.md @@ -334,38 +334,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_tree_bfs.zig" - // 层序遍历 - fn levelOrder(comptime T: type, mem_allocator: std.mem.Allocator, root: *inc.TreeNode(T)) !std.ArrayList(T) { - // 初始化队列,加入根节点 - const L = std.TailQueue(*inc.TreeNode(T)); - var queue = L{}; - var root_node = try mem_allocator.create(L.Node); - root_node.data = root; - queue.append(root_node); - // 初始化一个列表,用于保存遍历序列 - var list = std.ArrayList(T).init(std.heap.page_allocator); - while (queue.len > 0) { - var queue_node = queue.popFirst().?; // 队列出队 - var node = queue_node.data; - try list.append(node.val); // 保存节点值 - if (node.left != null) { - var tmp_node = try mem_allocator.create(L.Node); - tmp_node.data = node.left.?; - queue.append(tmp_node); // 左子节点入队 - } - if (node.right != null) { - var tmp_node = try mem_allocator.create(L.Node); - tmp_node.data = node.right.?; - queue.append(tmp_node); // 右子节点入队 - } - } - return list; - } - ``` - ??? pythontutor "可视化运行"
@@ -851,37 +819,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_tree_dfs.zig" - // 前序遍历 - fn preOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 访问优先级:根节点 -> 左子树 -> 右子树 - try list.append(root.?.val); - try preOrder(T, root.?.left); - try preOrder(T, root.?.right); - } - - // 中序遍历 - fn inOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 访问优先级:左子树 -> 根节点 -> 右子树 - try inOrder(T, root.?.left); - try list.append(root.?.val); - try inOrder(T, root.?.right); - } - - // 后序遍历 - fn postOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 访问优先级:左子树 -> 右子树 -> 根节点 - try postOrder(T, root.?.left); - try postOrder(T, root.?.right); - try list.append(root.?.val); - } - ``` - ??? pythontutor "可视化运行"
diff --git a/en/docs/chapter_appendix/contribution.md b/en/docs/chapter_appendix/contribution.md index 924351851..339629089 100644 --- a/en/docs/chapter_appendix/contribution.md +++ b/en/docs/chapter_appendix/contribution.md @@ -2,45 +2,45 @@ comments: true --- -# 16.2   Contributing +# 16.2   Contributing Together -Due to the limited abilities of the author, some omissions and errors are inevitable in this book. Please understand. If you discover any typos, broken links, missing content, textual ambiguities, unclear explanations, or unreasonable text structures, please assist us in making corrections to provide readers with better quality learning resources. +Due to limited capacity, there may be inevitable omissions and errors in this book. We appreciate your understanding and are grateful for your help in correcting them. If you discover typos, broken links, missing content, ambiguous wording, unclear explanations, or structural issues, please help us make corrections to provide readers with higher-quality learning resources. -The GitHub IDs of all [contributors](https://github.com/krahets/hello-algo/graphs/contributors) will be displayed on the repository, web, and PDF versions of the homepage of this book to thank them for their selfless contributions to the open-source community. +The GitHub IDs of all [contributors](https://github.com/krahets/hello-algo/graphs/contributors) will be displayed on the homepage of the book repository, the web version, and the PDF version to acknowledge their selfless contributions to the open source community. -!!! success "The charm of open source" +!!! success "The Charm of Open Source" - The interval between two printings of a paper book is often long, making content updates very inconvenient. - - In this open-source book, however, the content update cycle is shortened to just a few days or even hours. + The interval between two printings of a physical book is often quite long, making content updates very inconvenient. -### 1.   Content fine-tuning + In this open source book, the time for content updates has been shortened to just days or even hours. -As shown in Figure 16-3, there is an "edit icon" in the upper right corner of each page. You can follow these steps to modify text or code. +### 1.   Minor Content Adjustments -1. Click the "edit icon". If prompted to "fork this repository", please agree to do so. -2. Modify the Markdown source file content, check the accuracy of the content, and try to keep the formatting consistent. -3. Fill in the modification description at the bottom of the page, then click the "Propose file change" button. After the page redirects, click the "Create pull request" button to initiate the pull request. +As shown in Figure 16-3, there is an "edit icon" in the top-right corner of each page. You can modify text or code by following these steps. -![Edit page button](contribution.assets/edit_markdown.png){ class="animation-figure" } +1. Click the "edit icon". If you encounter a prompt asking you to "Fork this repository", please approve the operation. +2. Modify the content of the Markdown source file, verify the correctness of the content, and maintain consistent formatting as much as possible. +3. Fill in a description of your changes at the bottom of the page, then click the "Propose file change" button. After the page transitions, click the "Create pull request" button to submit your pull request. -

Figure 16-3   Edit page button

+![Page edit button](contribution.assets/edit_markdown.png){ class="animation-figure" } -Figures cannot be directly modified and require the creation of a new [Issue](https://github.com/krahets/hello-algo/issues) or a comment to describe the problem. We will redraw and replace the figures as soon as possible. +

Figure 16-3   Page edit button

-### 2.   Content creation +Images cannot be directly modified. Please describe the issue by creating a new [Issue](https://github.com/krahets/hello-algo/issues) or leaving a comment. We will promptly redraw and replace the images. -If you are interested in participating in this open-source project, including translating code into other programming languages or expanding article content, then the following Pull Request workflow needs to be implemented. +### 2.   Content Creation -1. Log in to GitHub and Fork the [code repository](https://github.com/krahets/hello-algo) of this book to your personal account. -2. Go to your Forked repository web page and use the `git clone` command to clone the repository to your local machine. -3. Create content locally and perform complete tests to verify the correctness of the code. -4. Commit the changes made locally, then push them to the remote repository. -5. Refresh the repository webpage and click the "Create pull request" button to initiate the pull request. +If you are interested in contributing to this open source project, including translating code into other programming languages or expanding article content, you will need to follow the Pull Request workflow below. -### 3.   Docker deployment +1. Log in to GitHub and Fork the book's [code repository](https://github.com/krahets/hello-algo) to your personal account. +2. Enter your forked repository webpage and use the `git clone` command to clone the repository to your local machine. +3. Create content locally and conduct comprehensive tests to verify code correctness. +4. Commit your local changes and push them to the remote repository. +5. Refresh the repository webpage and click the "Create pull request" button to submit your pull request. -In the `hello-algo` root directory, execute the following Docker script to access the project at `http://localhost:8000`: +### 3.   Docker Deployment + +From the root directory of `hello-algo`, run the following Docker script to access the project at `http://localhost:8000`: ```shell docker-compose up -d diff --git a/en/docs/chapter_appendix/index.md b/en/docs/chapter_appendix/index.md index 807cc622a..fdc1e490c 100644 --- a/en/docs/chapter_appendix/index.md +++ b/en/docs/chapter_appendix/index.md @@ -9,6 +9,6 @@ icon: material/help-circle-outline ## Chapter contents -- [16.1   Installation](installation.md) -- [16.2   Contributing](contribution.md) -- [16.3   Terminology](terminology.md) +- [16.1   Programming Environment Installation](installation.md) +- [16.2   Contributing Together](contribution.md) +- [16.3   Terminology Table](terminology.md) diff --git a/en/docs/chapter_appendix/installation.md b/en/docs/chapter_appendix/installation.md index fc8b3b357..be665333d 100644 --- a/en/docs/chapter_appendix/installation.md +++ b/en/docs/chapter_appendix/installation.md @@ -2,75 +2,75 @@ comments: true --- -# 16.1   Installation +# 16.1   Programming Environment Installation -## 16.1.1   Install IDE +## 16.1.1   Installing Ide -We recommend using the open-source, lightweight VS Code as your local Integrated Development Environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/) and choose the version of VS Code appropriate for your operating system to download and install. +We recommend using the open-source and lightweight VS Code as the local integrated development environment (IDE). Visit the [VS Code official website](https://code.visualstudio.com/), and download and install the appropriate version of VS Code according to your operating system. -![Download VS Code from the official website](installation.assets/vscode_installation.png){ class="animation-figure" } +![Download VS Code from the Official Website](installation.assets/vscode_installation.png){ class="animation-figure" } -

Figure 16-1   Download VS Code from the official website

+

Figure 16-1   Download VS Code from the Official Website

-VS Code has a powerful extension ecosystem, supporting the execution and debugging of most programming languages. For example, after installing the "Python Extension Pack," you can debug Python code. The installation steps are shown in Figure 16-2. +VS Code has a powerful ecosystem of extensions that supports running and debugging most programming languages. For example, after installing the "Python Extension Pack" extension, you can debug Python code. The installation steps are shown in the following figure. -![Install VS Code Extension Pack](installation.assets/vscode_extension_installation.png){ class="animation-figure" } +![Install VS Code Extensions](installation.assets/vscode_extension_installation.png){ class="animation-figure" } -

Figure 16-2   Install VS Code Extension Pack

+

Figure 16-2   Install VS Code Extensions

-## 16.1.2   Install language environments +## 16.1.2   Installing Language Environments -### 1.   Python environment +### 1.   Python Environment -1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), requiring Python 3.10 or newer. -2. In the VS Code extension marketplace, search for `python` and install the Python Extension Pack. -3. (Optional) Enter `pip install black` in the command line to install the code formatting tool. +1. Download and install [Miniconda3](https://docs.conda.io/en/latest/miniconda.html), which requires Python 3.10 or newer. +2. Search for `python` in the VS Code extension marketplace and install the Python Extension Pack. +3. (Optional) Enter `pip install black` on the command line to install the code formatter. -### 2.   C/C++ environment +### 2.   C/c++ Environment -1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([Configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); MacOS comes with Clang, so no installation is necessary. -2. In the VS Code extension marketplace, search for `c++` and install the C/C++ Extension Pack. +1. Windows systems need to install [MinGW](https://sourceforge.net/projects/mingw-w64/files/) ([configuration tutorial](https://blog.csdn.net/qq_33698226/article/details/129031241)); macOS comes with Clang built-in and does not require installation. +2. Search for `c++` in the VS Code extension marketplace and install the C/C++ Extension Pack. 3. (Optional) Open the Settings page, search for the `Clang_format_fallback Style` code formatting option, and set it to `{ BasedOnStyle: Microsoft, BreakBeforeBraces: Attach }`. -### 3.   Java environment +### 3.   Java Environment 1. Download and install [OpenJDK](https://jdk.java.net/18/) (version must be > JDK 9). -2. In the VS Code extension marketplace, search for `java` and install the Extension Pack for Java. +2. Search for `java` in the VS Code extension marketplace and install the Extension Pack for Java. -### 4.   C# environment +### 4.   C# Environment 1. Download and install [.Net 8.0](https://dotnet.microsoft.com/en-us/download). -2. In the VS Code extension marketplace, search for `C# Dev Kit` and install the C# Dev Kit ([Configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)). -3. You can also use Visual Studio ([Installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)). +2. Search for `C# Dev Kit` in the VS Code extension marketplace and install C# Dev Kit ([configuration tutorial](https://code.visualstudio.com/docs/csharp/get-started)). +3. You can also use Visual Studio ([installation tutorial](https://learn.microsoft.com/zh-cn/visualstudio/install/install-visual-studio?view=vs-2022)). -### 5.   Go environment +### 5.   Go Environment -1. Download and install [go](https://go.dev/dl/). -2. In the VS Code extension marketplace, search for `go` and install Go. -3. Press `Ctrl + Shift + P` to call up the command bar, enter go, choose `Go: Install/Update Tools`, select all and install. +1. Download and install [Go](https://go.dev/dl/). +2. Search for `go` in the VS Code extension marketplace and install Go. +3. Press `Ctrl + Shift + P` to open the command palette, type `go`, select `Go: Install/Update Tools`, check all options and install. -### 6.   Swift environment +### 6.   Swift Environment 1. Download and install [Swift](https://www.swift.org/download/). -2. In the VS Code extension marketplace, search for `swift` and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). +2. Search for `swift` in the VS Code extension marketplace and install [Swift for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=sswg.swift-lang). -### 7.   JavaScript environment +### 7.   Javascript Environment 1. Download and install [Node.js](https://nodejs.org/en/). -2. (Optional) In the VS Code extension marketplace, search for `Prettier` and install the code formatting tool. +2. (Optional) Search for `Prettier` in the VS Code extension marketplace and install the code formatter. -### 8.   TypeScript environment +### 8.   Typescript Environment 1. Follow the same installation steps as the JavaScript environment. 2. Install [TypeScript Execute (tsx)](https://github.com/privatenumber/tsx?tab=readme-ov-file#global-installation). -3. In the VS Code extension marketplace, search for `typescript` and install [Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors). +3. Search for `typescript` in the VS Code extension marketplace and install [Pretty TypeScript Errors](https://marketplace.visualstudio.com/items?itemName=yoavbls.pretty-ts-errors). -### 9.   Dart environment +### 9.   Dart Environment 1. Download and install [Dart](https://dart.dev/get-dart). -2. In the VS Code extension marketplace, search for `dart` and install [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code). +2. Search for `dart` in the VS Code extension marketplace and install [Dart](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code). -### 10.   Rust environment +### 10.   Rust Environment 1. Download and install [Rust](https://www.rust-lang.org/tools/install). -2. In the VS Code extension marketplace, search for `rust` and install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). +2. Search for `rust` in the VS Code extension marketplace and install [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer). diff --git a/en/docs/chapter_appendix/terminology.md b/en/docs/chapter_appendix/terminology.md index e8e0957e0..085304a38 100644 --- a/en/docs/chapter_appendix/terminology.md +++ b/en/docs/chapter_appendix/terminology.md @@ -2,19 +2,19 @@ comments: true --- -# 16.3   Glossary +# 16.3   Terminology Table -Table 16-1 lists the important terms that appear in the book, and it is worth noting the following points. +The following table lists important terms that appear in this book. It is worth noting the following points: -- It is recommended to remember the English names of the terms to facilitate reading English literature. -- Some terms have different names in Simplified and Traditional Chinese. +- We recommend remembering the English names of terms to help with reading English literature. +- Some terms have different names in Simplified Chinese and Traditional Chinese.

Table 16-1   Important Terms in Data Structures and Algorithms

-| English | 简体中文 | 繁体中文 | -| ------------------------------ | -------------- | -------------- | +| English | Simplified Chinese | Traditional Chinese | +| ------------------------------ | ------------------ | ------------------- | | algorithm | 算法 | 演算法 | | data structure | 数据结构 | 資料結構 | | code | 代码 | 程式碼 | diff --git a/en/docs/chapter_array_and_linkedlist/array.md b/en/docs/chapter_array_and_linkedlist/array.md index 4494f3044..569536196 100755 --- a/en/docs/chapter_array_and_linkedlist/array.md +++ b/en/docs/chapter_array_and_linkedlist/array.md @@ -4,17 +4,17 @@ comments: true # 4.1   Array -An array is a linear data structure that operates as a lineup of similar items, stored together in a computer's memory in contiguous spaces. It's like a sequence that maintains organized storage. Each item in this lineup has its unique 'spot' known as an index. Please refer to Figure 4-1 to observe how arrays work and grasp these key terms. +An array is a linear data structure that stores elements of the same type in contiguous memory space. The position of an element in the array is called the element's index. Figure 4-1 illustrates the main concepts and storage method of arrays. ![Array definition and storage method](array.assets/array_definition.png){ class="animation-figure" }

Figure 4-1   Array definition and storage method

-## 4.1.1   Common operations on arrays +## 4.1.1   Common Array Operations -### 1.   Initializing arrays +### 1.   Initializing Arrays -Arrays can be initialized in two ways depending on the needs: either without initial values or with specified initial values. When initial values are not specified, most programming languages will set the array elements to $0$: +We can choose between two array initialization methods based on our needs: without initial values or with given initial values. When no initial values are specified, most programming languages will initialize array elements to $0$: === "Python" @@ -31,7 +31,7 @@ Arrays can be initialized in two ways depending on the needs: either without ini // Stored on stack int arr[5]; int nums[5] = { 1, 3, 2, 5, 4 }; - // Stored on heap (manual memory release needed) + // Stored on heap (requires manual memory release) int* arr1 = new int[5]; int* nums1 = new int[5] { 1, 3, 2, 5, 4 }; ``` @@ -57,9 +57,9 @@ Arrays can be initialized in two ways depending on the needs: either without ini ```go title="array.go" /* Initialize array */ var arr [5]int - // In Go, specifying the length ([5]int) denotes an array, while not specifying it ([]int) denotes a slice. - // Since Go's arrays are designed to have compile-time fixed length, only constants can be used to specify the length. - // For convenience in implementing the extend() method, the Slice will be considered as an Array here. + // In Go, specifying length ([5]int) creates an array; not specifying length ([]int) creates a slice + // Since Go's arrays are designed to have their length determined at compile time, only constants can be used to specify the length + // For convenience in implementing the extend() method, slices are treated as arrays below nums := []int{1, 3, 2, 5, 4} ``` @@ -101,10 +101,10 @@ Arrays can be initialized in two ways depending on the needs: either without ini /* Initialize array */ let arr: [i32; 5] = [0; 5]; // [0, 0, 0, 0, 0] let slice: &[i32] = &[0; 5]; - // In Rust, specifying the length ([i32; 5]) denotes an array, while not specifying it (&[i32]) denotes a slice. - // Since Rust's arrays are designed to have compile-time fixed length, only constants can be used to specify the length. - // Vectors are generally used as dynamic arrays in Rust. - // For convenience in implementing the extend() method, the vector will be considered as an array here. + // In Rust, specifying length ([i32; 5]) creates an array; not specifying length (&[i32]) creates a slice + // Since Rust's arrays are designed to have their length determined at compile time, only constants can be used to specify the length + // Vector is the type generally used as a dynamic array in Rust + // For convenience in implementing the extend() method, vectors are treated as arrays below let nums: Vec = vec![1, 3, 2, 5, 4]; ``` @@ -119,37 +119,44 @@ Arrays can be initialized in two ways depending on the needs: either without ini === "Kotlin" ```kotlin title="array.kt" - + /* Initialize array */ + var arr = IntArray(5) // { 0, 0, 0, 0, 0 } + var nums = intArrayOf(1, 3, 2, 5, 4) ``` -=== "Zig" +=== "Ruby" - ```zig title="array.zig" - // Initialize array - var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } - var nums = [_]i32{ 1, 3, 2, 5, 4 }; + ```ruby title="array.rb" + # Initialize array + arr = Array.new(5, 0) + nums = [1, 3, 2, 5, 4] ``` -### 2.   Accessing elements +??? pythontutor "Code Visualization" -Elements in an array are stored in contiguous memory spaces, making it simpler to compute each element's memory address. The formula shown in the Figure below aids in determining an element's memory address, utilizing the array's memory address (specifically, the first element's address) and the element's index. This computation streamlines direct access to the desired element. +
+
Full Screen >
+ +### 2.   Accessing Elements + +Array elements are stored in contiguous memory space, which means calculating the memory address of array elements is very easy. Given the array's memory address (the memory address of the first element) and an element's index, we can use the formula shown in Figure 4-2 to calculate the element's memory address and directly access that element. ![Memory address calculation for array elements](array.assets/array_memory_location_calculation.png){ class="animation-figure" }

Figure 4-2   Memory address calculation for array elements

-As observed in Figure 4-2, array indexing conventionally begins at $0$. While this might appear counterintuitive, considering counting usually starts at $1$, within the address calculation formula, **an index is essentially an offset from the memory address**. For the first element's address, this offset is $0$, validating its index as $0$. +Observing Figure 4-2, we find that the first element of an array has an index of $0$, which may seem counterintuitive since counting from $1$ would be more natural. However, from the perspective of the address calculation formula, **an index is essentially an offset from the memory address**. The address offset of the first element is $0$, so it is reasonable for its index to be $0$. -Accessing elements in an array is highly efficient, allowing us to randomly access any element in $O(1)$ time. +Accessing elements in an array is highly efficient; we can randomly access any element in the array in $O(1)$ time. === "Python" ```python title="array.py" def random_access(nums: list[int]) -> int: - """Random access to elements""" + """Random access to element""" # Randomly select a number from the interval [0, len(nums)-1] random_index = random.randint(0, len(nums) - 1) - # Retrieve and return a random element + # Retrieve and return the random element random_num = nums[random_index] return random_num ``` @@ -157,11 +164,11 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce === "C++" ```cpp title="array.cpp" - /* Random access to elements */ + /* Random access to element */ int randomAccess(int *nums, int size) { - // Randomly select a number in the range [0, size) + // Randomly select a number from interval [0, size) int randomIndex = rand() % size; - // Retrieve and return a random element + // Retrieve and return the random element int randomNum = nums[randomIndex]; return randomNum; } @@ -170,11 +177,11 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce === "Java" ```java title="array.java" - /* Random access to elements */ + /* Random access to element */ int randomAccess(int[] nums) { // Randomly select a number in the interval [0, nums.length) int randomIndex = ThreadLocalRandom.current().nextInt(0, nums.length); - // Retrieve and return a random element + // Retrieve and return the random element int randomNum = nums[randomIndex]; return randomNum; } @@ -183,101 +190,166 @@ Accessing elements in an array is highly efficient, allowing us to randomly acce === "C#" ```csharp title="array.cs" - [class]{array}-[func]{RandomAccess} + /* Random access to element */ + int RandomAccess(int[] nums) { + Random random = new(); + // Randomly select a number in interval [0, nums.Length) + int randomIndex = random.Next(nums.Length); + // Retrieve and return the random element + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{randomAccess} + /* Random access to element */ + func randomAccess(nums []int) (randomNum int) { + // Randomly select a number in the interval [0, nums.length) + randomIndex := rand.Intn(len(nums)) + // Retrieve and return the random element + randomNum = nums[randomIndex] + return + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{randomAccess} + /* Random access to element */ + func randomAccess(nums: [Int]) -> Int { + // Randomly select a number in interval [0, nums.count) + let randomIndex = nums.indices.randomElement()! + // Retrieve and return the random element + let randomNum = nums[randomIndex] + return randomNum + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{randomAccess} + /* Random access to element */ + function randomAccess(nums) { + // Randomly select a number in the interval [0, nums.length) + const random_index = Math.floor(Math.random() * nums.length); + // Retrieve and return the random element + const random_num = nums[random_index]; + return random_num; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{randomAccess} + /* Random access to element */ + function randomAccess(nums: number[]): number { + // Randomly select a number in the interval [0, nums.length) + const random_index = Math.floor(Math.random() * nums.length); + // Retrieve and return the random element + const random_num = nums[random_index]; + return random_num; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{randomAccess} + /* Random access to element */ + int randomAccess(List nums) { + // Randomly select a number in the interval [0, nums.length) + int randomIndex = Random().nextInt(nums.length); + // Retrieve and return the random element + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{random_access} + /* Random access to element */ + fn random_access(nums: &[i32]) -> i32 { + // Randomly select a number in interval [0, nums.len()) + let random_index = rand::thread_rng().gen_range(0..nums.len()); + // Retrieve and return the random element + let random_num = nums[random_index]; + random_num + } ``` === "C" ```c title="array.c" - [class]{}-[func]{randomAccess} + /* Random access to element */ + int randomAccess(int *nums, int size) { + // Randomly select a number from interval [0, size) + int randomIndex = rand() % size; + // Retrieve and return the random element + int randomNum = nums[randomIndex]; + return randomNum; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{randomAccess} + /* Random access to element */ + fun randomAccess(nums: IntArray): Int { + // Randomly select a number in interval [0, nums.size) + val randomIndex = ThreadLocalRandom.current().nextInt(0, nums.size) + // Retrieve and return the random element + val randomNum = nums[randomIndex] + return randomNum + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{random_access} + ### Random access element ### + def random_access(nums) + # Randomly select a number in the interval [0, nums.length) + random_index = Random.rand(0...nums.length) + + # Retrieve and return the random element + nums[random_index] + end ``` -=== "Zig" +### 3.   Inserting Elements - ```zig title="array.zig" - [class]{}-[func]{randomAccess} - ``` +Array elements are stored "tightly adjacent" in memory, with no space between them to store any additional data. As shown in Figure 4-3, if we want to insert an element in the middle of an array, we need to shift all elements after that position backward by one position, and then assign the value to that index. -### 3.   Inserting elements +![Example of inserting an element into an array](array.assets/array_insert_element.png){ class="animation-figure" } -Array elements are tightly packed in memory, with no space available to accommodate additional data between them. As illustrated in Figure 4-3, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element. +

Figure 4-3   Example of inserting an element into an array

-![Array element insertion example](array.assets/array_insert_element.png){ class="animation-figure" } - -

Figure 4-3   Array element insertion example

- -It's important to note that due to the fixed length of an array, inserting an element will unavoidably result in the loss of the last element in the array. Solutions to address this issue will be explored in the "List" chapter. +It is worth noting that since the length of an array is fixed, inserting an element will inevitably cause the element at the end of the array to be "lost". We will leave the solution to this problem for discussion in the "List" chapter. === "Python" ```python title="array.py" def insert(nums: list[int], num: int, index: int): - """Insert element num at `index`""" - # Move all elements after `index` one position backward + """Insert element num at index index in the array""" + # Move all elements at and after index index backward by one position for i in range(len(nums) - 1, index, -1): nums[i] = nums[i - 1] - # Assign num to the element at index + # Assign num to the element at index index nums[index] = num ``` === "C++" ```cpp title="array.cpp" - /* Insert element num at `index` */ + /* Insert element num at index index in the array */ void insert(int *nums, int size, int num, int index) { - // Move all elements after `index` one position backward + // Move all elements at and after index index backward by one position for (int i = size - 1; i > index; i--) { nums[i] = nums[i - 1]; } - // Assign num to the element at index + // Assign num to the element at index index nums[index] = num; } ``` @@ -285,13 +357,13 @@ It's important to note that due to the fixed length of an array, inserting an el === "Java" ```java title="array.java" - /* Insert element num at `index` */ + /* Insert element num at index index in the array */ void insert(int[] nums, int num, int index) { - // Move all elements after `index` one position backward + // Move all elements at and after index index backward by one position for (int i = nums.length - 1; i > index; i--) { nums[i] = nums[i - 1]; } - // Assign num to the element at index + // Assign num to the element at index index nums[index] = num; } ``` @@ -299,85 +371,160 @@ It's important to note that due to the fixed length of an array, inserting an el === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Insert} + /* Insert element num at index index in the array */ + void Insert(int[] nums, int num, int index) { + // Move all elements at and after index index backward by one position + for (int i = nums.Length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // Assign num to the element at index index + nums[index] = num; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + func insert(nums []int, num int, index int) { + // Move all elements at and after index index backward by one position + for i := len(nums) - 1; i > index; i-- { + nums[i] = nums[i-1] + } + // Assign num to the element at index index + nums[index] = num + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + func insert(nums: inout [Int], num: Int, index: Int) { + // Move all elements at and after index index backward by one position + for i in nums.indices.dropFirst(index).reversed() { + nums[i] = nums[i - 1] + } + // Assign num to the element at index index + nums[index] = num + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + function insert(nums, num, index) { + // Move all elements at and after index index backward by one position + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // Assign num to the element at index index + nums[index] = num; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + function insert(nums: number[], num: number, index: number): void { + // Move all elements at and after index index backward by one position + for (let i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // Assign num to the element at index index + nums[index] = num; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{insert} + /* Insert element _num at array index index */ + void insert(List nums, int _num, int index) { + // Move all elements at and after index index backward by one position + for (var i = nums.length - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // Assign _num to element at index + nums[index] = _num; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + fn insert(nums: &mut [i32], num: i32, index: usize) { + // Move all elements at and after index index backward by one position + for i in (index + 1..nums.len()).rev() { + nums[i] = nums[i - 1]; + } + // Assign num to the element at index index + nums[index] = num; + } ``` === "C" ```c title="array.c" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + void insert(int *nums, int size, int num, int index) { + // Move all elements at and after index index backward by one position + for (int i = size - 1; i > index; i--) { + nums[i] = nums[i - 1]; + } + // Assign num to the element at index index + nums[index] = num; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{insert} + /* Insert element num at index index in the array */ + fun insert(nums: IntArray, num: Int, index: Int) { + // Move all elements at and after index index backward by one position + for (i in nums.size - 1 downTo index + 1) { + nums[i] = nums[i - 1] + } + // Assign num to the element at index index + nums[index] = num + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{insert} + ### Insert element num at index in array ### + def insert(nums, num, index) + # Move all elements at and after index index backward by one position + for i in (nums.length - 1).downto(index + 1) + nums[i] = nums[i - 1] + end + + # Assign num to the element at index index + nums[index] = num + end ``` -=== "Zig" +### 4.   Removing Elements - ```zig title="array.zig" - [class]{}-[func]{insert} - ``` +Similarly, as shown in Figure 4-4, to delete the element at index $i$, we need to shift all elements after index $i$ forward by one position. -### 4.   Deleting elements +![Example of removing an element from an array](array.assets/array_remove_element.png){ class="animation-figure" } -Similarly, as depicted in Figure 4-4, to delete an element at index $i$, all elements following index $i$ must be moved forward by one position. +

Figure 4-4   Example of removing an element from an array

-![Array element deletion example](array.assets/array_remove_element.png){ class="animation-figure" } - -

Figure 4-4   Array element deletion example

- -Please note that after deletion, the former last element becomes "meaningless," hence requiring no specific modification. +Note that after the deletion is complete, the original last element becomes "meaningless", so we do not need to specifically modify it. === "Python" ```python title="array.py" def remove(nums: list[int], index: int): - """Remove the element at `index`""" - # Move all elements after `index` one position forward + """Remove the element at index index""" + # Move all elements after index index forward by one position for i in range(index, len(nums) - 1): nums[i] = nums[i + 1] ``` @@ -385,9 +532,9 @@ Please note that after deletion, the former last element becomes "meaningless," === "C++" ```cpp title="array.cpp" - /* Remove the element at `index` */ + /* Remove the element at index index */ void remove(int *nums, int size, int index) { - // Move all elements after `index` one position forward + // Move all elements after index index forward by one position for (int i = index; i < size - 1; i++) { nums[i] = nums[i + 1]; } @@ -397,9 +544,9 @@ Please note that after deletion, the former last element becomes "meaningless," === "Java" ```java title="array.java" - /* Remove the element at `index` */ + /* Remove the element at index index */ void remove(int[] nums, int index) { - // Move all elements after `index` one position forward + // Move all elements after index index forward by one position for (int i = index; i < nums.length - 1; i++) { nums[i] = nums[i + 1]; } @@ -409,78 +556,133 @@ Please note that after deletion, the former last element becomes "meaningless," === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Remove} + /* Remove the element at index index */ + void Remove(int[] nums, int index) { + // Move all elements after index index forward by one position + for (int i = index; i < nums.Length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{remove} + /* Remove the element at index index */ + func remove(nums []int, index int) { + // Move all elements after index index forward by one position + for i := index; i < len(nums)-1; i++ { + nums[i] = nums[i+1] + } + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{remove} + /* Remove the element at index index */ + func remove(nums: inout [Int], index: Int) { + // Move all elements after index index forward by one position + for i in nums.indices.dropFirst(index).dropLast() { + nums[i] = nums[i + 1] + } + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{remove} + /* Remove the element at index index */ + function remove(nums, index) { + // Move all elements after index index forward by one position + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{remove} + /* Remove the element at index index */ + function remove(nums: number[], index: number): void { + // Move all elements after index index forward by one position + for (let i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{remove} + /* Remove the element at index index */ + void remove(List nums, int index) { + // Move all elements after index index forward by one position + for (var i = index; i < nums.length - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{remove} + /* Remove the element at index index */ + fn remove(nums: &mut [i32], index: usize) { + // Move all elements after index index forward by one position + for i in index..nums.len() - 1 { + nums[i] = nums[i + 1]; + } + } ``` === "C" ```c title="array.c" - [class]{}-[func]{removeItem} + /* Remove the element at index index */ + // Note: stdio.h occupies the remove keyword + void removeItem(int *nums, int size, int index) { + // Move all elements after index index forward by one position + for (int i = index; i < size - 1; i++) { + nums[i] = nums[i + 1]; + } + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{remove} + /* Remove the element at index index */ + fun remove(nums: IntArray, index: Int) { + // Move all elements after index index forward by one position + for (i in index.. nums) { + int count = 0; + // Traverse array by index + for (var i = 0; i < nums.length; i++) { + count += nums[i]; + } + // Direct traversal of array elements + for (int _num in nums) { + count += _num; + } + // Traverse array using forEach method + nums.forEach((_num) { + count += _num; + }); + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{traverse} + /* Traverse array */ + fn traverse(nums: &[i32]) { + let mut _count = 0; + // Traverse array by index + for i in 0..nums.len() { + _count += nums[i]; + } + // Direct traversal of array elements + _count = 0; + for &num in nums { + _count += num; + } + } ``` === "C" ```c title="array.c" - [class]{}-[func]{traverse} + /* Traverse array */ + void traverse(int *nums, int size) { + int count = 0; + // Traverse array by index + for (int i = 0; i < size; i++) { + count += nums[i]; + } + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{traverse} + /* Traverse array */ + fun traverse(nums: IntArray) { + var count = 0 + // Traverse array by index + for (i in nums.indices) { + count += nums[i] + } + // Direct traversal of array elements + for (j in nums) { + count += j + } + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{traverse} + ### Traverse array ### + def traverse(nums) + count = 0 + + # Traverse array by index + for i in 0...nums.length + count += nums[i] + end + + # Direct traversal of array elements + for num in nums + count += num + end + end ``` -=== "Zig" +### 6.   Finding Elements - ```zig title="array.zig" - [class]{}-[func]{traverse} - ``` +Finding a specified element in an array requires traversing the array and checking whether the element value matches in each iteration; if it matches, output the corresponding index. -### 6.   Finding elements - -Locating a specific element within an array involves iterating through the array, checking each element to determine if it matches the desired value. - -Because arrays are linear data structures, this operation is commonly referred to as "linear search." +Since an array is a linear data structure, the above search operation is called a "linear search". === "Python" ```python title="array.py" def find(nums: list[int], target: int) -> int: - """Search for a specified element in the array""" + """Find the specified element in the array""" for i in range(len(nums)): if nums[i] == target: return i @@ -616,7 +936,7 @@ Because arrays are linear data structures, this operation is commonly referred t === "C++" ```cpp title="array.cpp" - /* Search for a specified element in the array */ + /* Find the specified element in the array */ int find(int *nums, int size, int target) { for (int i = 0; i < size; i++) { if (nums[i] == target) @@ -629,7 +949,7 @@ Because arrays are linear data structures, this operation is commonly referred t === "Java" ```java title="array.java" - /* Search for a specified element in the array */ + /* Find the specified element in the array */ int find(int[] nums, int target) { for (int i = 0; i < nums.length; i++) { if (nums[i] == target) @@ -642,86 +962,154 @@ Because arrays are linear data structures, this operation is commonly referred t === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + 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" - [class]{}-[func]{find} + /* Find the specified element in the array */ + int find(int *nums, int size, int target) { + for (int i = 0; i < size; i++) { + if (nums[i] == target) + return i; + } + return -1; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{find} + /* Find the specified element in the array */ + fun find(nums: IntArray, target: Int): Int { + for (i in nums.indices) { + if (nums[i] == target) + return i + } + return -1 + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{find} + ### Find specified element in array ### + def find(nums, target) + for i in 0...nums.length + return i if nums[i] == target + end + + -1 + end ``` -=== "Zig" +### 7.   Expanding Arrays - ```zig title="array.zig" - [class]{}-[func]{find} - ``` +In complex system environments, programs cannot guarantee that the memory space after an array is available, making it unsafe to expand the array's capacity. Therefore, in most programming languages, **the length of an array is immutable**. -### 7.   Expanding arrays - -In complex system environments, ensuring the availability of memory space after an array for safe capacity extension becomes challenging. Consequently, in most programming languages, **the length of an array is immutable**. - -To expand an array, it's necessary to create a larger array and then copy the elements from the original array. This operation has a time complexity of $O(n)$ and can be time-consuming for large arrays. The code are as follows: +If we want to expand an array, we need to create a new, larger array and then copy the original array elements to the new array one by one. This is an $O(n)$ operation, which is very time-consuming when the array is large. The code is shown below: === "Python" ```python title="array.py" def extend(nums: list[int], enlarge: int) -> list[int]: """Extend array length""" - # Initialize an extended length array + # Initialize an array with extended length res = [0] * (len(nums) + enlarge) # Copy all elements from the original array to the new array for i in range(len(nums)): res[i] = nums[i] - # Return the new array after expansion + # Return the extended new array return res ``` @@ -730,7 +1118,7 @@ To expand an array, it's necessary to create a larger array and then copy the e ```cpp title="array.cpp" /* Extend array length */ int *extend(int *nums, int size, int enlarge) { - // Initialize an extended length array + // Initialize an array with extended length int *res = new int[size + enlarge]; // Copy all elements from the original array to the new array for (int i = 0; i < size; i++) { @@ -738,7 +1126,7 @@ To expand an array, it's necessary to create a larger array and then copy the e } // Free memory delete[] nums; - // Return the new array after expansion + // Return the extended new array return res; } ``` @@ -748,13 +1136,13 @@ To expand an array, it's necessary to create a larger array and then copy the e ```java title="array.java" /* Extend array length */ int[] extend(int[] nums, int enlarge) { - // Initialize an extended length array + // Initialize an array with extended length int[] res = new int[nums.length + enlarge]; // Copy all elements from the original array to the new array for (int i = 0; i < nums.length; i++) { res[i] = nums[i]; } - // Return the new array after expansion + // Return the extended new array return res; } ``` @@ -762,89 +1150,194 @@ To expand an array, it's necessary to create a larger array and then copy the e === "C#" ```csharp title="array.cs" - [class]{array}-[func]{Extend} + /* Extend array length */ + int[] Extend(int[] nums, int enlarge) { + // Initialize an array with extended length + int[] res = new int[nums.Length + enlarge]; + // Copy all elements from the original array to the new array + for (int i = 0; i < nums.Length; i++) { + res[i] = nums[i]; + } + // Return the extended new array + return res; + } ``` === "Go" ```go title="array.go" - [class]{}-[func]{extend} + /* Extend array length */ + func extend(nums []int, enlarge int) []int { + // Initialize an array with extended length + res := make([]int, len(nums)+enlarge) + // Copy all elements from the original array to the new array + for i, num := range nums { + res[i] = num + } + // Return the extended new array + return res + } ``` === "Swift" ```swift title="array.swift" - [class]{}-[func]{extend} + /* Extend array length */ + func extend(nums: [Int], enlarge: Int) -> [Int] { + // Initialize an array with extended length + var res = Array(repeating: 0, count: nums.count + enlarge) + // Copy all elements from the original array to the new array + for i in nums.indices { + res[i] = nums[i] + } + // Return the extended new array + return res + } ``` === "JS" ```javascript title="array.js" - [class]{}-[func]{extend} + /* Extend array length */ + // Note: JavaScript's Array is dynamic array, can be directly expanded + // For learning purposes, this function treats Array as fixed-length array + function extend(nums, enlarge) { + // Initialize an array with extended length + const res = new Array(nums.length + enlarge).fill(0); + // Copy all elements from the original array to the new array + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // Return the extended new array + return res; + } ``` === "TS" ```typescript title="array.ts" - [class]{}-[func]{extend} + /* Extend array length */ + // Note: TypeScript's Array is dynamic array, can be directly expanded + // For learning purposes, this function treats Array as fixed-length array + function extend(nums: number[], enlarge: number): number[] { + // Initialize an array with extended length + const res = new Array(nums.length + enlarge).fill(0); + // Copy all elements from the original array to the new array + for (let i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // Return the extended new array + return res; + } ``` === "Dart" ```dart title="array.dart" - [class]{}-[func]{extend} + /* Extend array length */ + List extend(List nums, int enlarge) { + // Initialize an array with extended length + List res = List.filled(nums.length + enlarge, 0); + // Copy all elements from the original array to the new array + for (var i = 0; i < nums.length; i++) { + res[i] = nums[i]; + } + // Return the extended new array + return res; + } ``` === "Rust" ```rust title="array.rs" - [class]{}-[func]{extend} + /* Extend array length */ + fn extend(nums: &[i32], enlarge: usize) -> Vec { + // Initialize an array with extended length + let mut res: Vec = vec![0; nums.len() + enlarge]; + // Copy all elements from original array to new + res[0..nums.len()].copy_from_slice(nums); + + // Return the extended new array + res + } ``` === "C" ```c title="array.c" - [class]{}-[func]{extend} + /* Extend array length */ + int *extend(int *nums, int size, int enlarge) { + // Initialize an array with extended length + int *res = (int *)malloc(sizeof(int) * (size + enlarge)); + // Copy all elements from the original array to the new array + for (int i = 0; i < size; i++) { + res[i] = nums[i]; + } + // Initialize expanded space + for (int i = size; i < size + enlarge; i++) { + res[i] = 0; + } + // Return the extended new array + return res; + } ``` === "Kotlin" ```kotlin title="array.kt" - [class]{}-[func]{extend} + /* Extend array length */ + fun extend(nums: IntArray, enlarge: Int): IntArray { + // Initialize an array with extended length + val res = IntArray(nums.size + enlarge) + // Copy all elements from the original array to the new array + for (i in nums.indices) { + res[i] = nums[i] + } + // Return the extended new array + return res + } ``` === "Ruby" ```ruby title="array.rb" - [class]{}-[func]{extend} + ### Extend array length ### + # Note: Ruby's Array is dynamic array, can be directly expanded + # For learning purposes, this function treats Array as fixed-length array + def extend(nums, enlarge) + # Initialize an array with extended length + res = Array.new(nums.length + enlarge, 0) + + # Copy all elements from the original array to the new array + for i in 0...nums.length + res[i] = nums[i] + end + + # Return the extended new array + res + end ``` -=== "Zig" +## 4.1.2   Advantages and Limitations of Arrays - ```zig title="array.zig" - [class]{}-[func]{extend} - ``` +Arrays are stored in contiguous memory space with elements of the same type. This approach contains rich prior information that the system can use to optimize the efficiency of data structure operations. -## 4.1.2   Advantages and limitations of arrays +- **High space efficiency**: Arrays allocate contiguous memory blocks for data without additional structural overhead. +- **Support for random access**: Arrays allow accessing any element in $O(1)$ time. +- **Cache locality**: When accessing array elements, the computer not only loads the element but also caches the surrounding data, thereby leveraging the cache to improve the execution speed of subsequent operations. -Arrays are stored in contiguous memory spaces and consist of elements of the same type. This approach provides substantial prior information that systems can leverage to optimize the efficiency of data structure operations. +Contiguous space storage is a double-edged sword with the following limitations: -- **High space efficiency**: Arrays allocate a contiguous block of memory for data, eliminating the need for additional structural overhead. -- **Support for random access**: Arrays allow $O(1)$ time access to any element. -- **Cache locality**: When accessing array elements, the computer not only loads them but also caches the surrounding data, utilizing high-speed cache to enchance subsequent operation speeds. +- **Low insertion and deletion efficiency**: When an array has many elements, insertion and deletion operations require shifting a large number of elements. +- **Immutable length**: After an array is initialized, its length is fixed. Expanding the array requires copying all data to a new array, which is very costly. +- **Space waste**: If the allocated size of an array exceeds what is actually needed, the extra space is wasted. -However, continuous space storage is a double-edged sword, with the following limitations: +## 4.1.3   Typical Applications of Arrays -- **Low efficiency in insertion and deletion**: As arrays accumulate many elements, inserting or deleting elements requires shifting a large number of elements. -- **Fixed length**: The length of an array is fixed after initialization. Expanding an array requires copying all data to a new array, incurring significant costs. -- **Space wastage**: If the allocated array size exceeds the what is necessary, the extra space is wasted. +Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various complex data structures. -## 4.1.3   Typical applications of arrays - -Arrays are fundamental and widely used data structures. They find frequent application in various algorithms and serve in the implementation of complex data structures. - -- **Random access**: Arrays are ideal for storing data when random sampling is required. By generating a random sequence based on indices, we can achieve random sampling efficiently. -- **Sorting and searching**: Arrays are the most commonly used data structure for sorting and searching algorithms. Techniques like quick sort, merge sort, binary search, etc., are primarily operate on arrays. -- **Lookup tables**: Arrays serve as efficient lookup tables for quick element or relationship retrieval. For instance, mapping characters to ASCII codes becomes seamless by using the ASCII code values as indices and storing corresponding elements in the array. -- **Machine learning**: Within the domain of neural networks, arrays play a pivotal role in executing crucial linear algebra operations involving vectors, matrices, and tensors. Arrays serve as the primary and most extensively used data structure in neural network programming. -- **Data structure implementation**: Arrays serve as the building blocks for implementing various data structures like stacks, queues, hash tables, heaps, graphs, etc. For instance, the adjacency matrix representation of a graph is essentially a two-dimensional array. +- **Random access**: If we want to randomly sample some items, we can use an array to store them and generate a random sequence to implement random sampling based on indices. +- **Sorting and searching**: Arrays are the most commonly used data structure for sorting and searching algorithms. Quick sort, merge sort, binary search, and others are primarily performed on arrays. +- **Lookup tables**: When we need to quickly find an element or its corresponding relationship, we can use an array as a lookup table. For example, if we want to implement a mapping from characters to ASCII codes, we can use the ASCII code value of a character as an index, with the corresponding element stored at that position in the array. +- **Machine learning**: Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, all of which are constructed in the form of arrays. Arrays are the most commonly used data structure in neural network programming. +- **Data structure implementation**: Arrays can be used to implement stacks, queues, hash tables, heaps, graphs, and other data structures. For example, the adjacency matrix representation of a graph is essentially a two-dimensional array. diff --git a/en/docs/chapter_array_and_linkedlist/index.md b/en/docs/chapter_array_and_linkedlist/index.md index fd2e8796b..906b5521e 100644 --- a/en/docs/chapter_array_and_linkedlist/index.md +++ b/en/docs/chapter_array_and_linkedlist/index.md @@ -3,20 +3,20 @@ comments: true icon: material/view-list-outline --- -# Chapter 4.   Arrays and linked lists +# Chapter 4.   Array and Linked List -![Arrays and linked lists](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" } +![Array and Linked List](../assets/covers/chapter_array_and_linkedlist.jpg){ class="cover-image" } !!! abstract - The world of data structures resembles a sturdy brick wall. + The world of data structures is like a solid brick wall. - In arrays, envision bricks snugly aligned, each resting seamlessly beside the next, creating a unified formation. Meanwhile, in linked lists, these bricks disperse freely, embraced by vines gracefully knitting connections between them. + Array bricks are neatly arranged, tightly packed one by one. Linked list bricks are scattered everywhere, with connecting vines freely weaving through the gaps between bricks. ## Chapter contents - [4.1   Array](array.md) -- [4.2   Linked list](linked_list.md) +- [4.2   Linked List](linked_list.md) - [4.3   List](list.md) -- [4.4   Memory and cache *](ram_and_cache.md) +- [4.4   Memory and Cache *](ram_and_cache.md) - [4.5   Summary](summary.md) diff --git a/en/docs/chapter_array_and_linkedlist/linked_list.md b/en/docs/chapter_array_and_linkedlist/linked_list.md index 22e0ac6ec..91283010e 100755 --- a/en/docs/chapter_array_and_linkedlist/linked_list.md +++ b/en/docs/chapter_array_and_linkedlist/linked_list.md @@ -2,25 +2,25 @@ comments: true --- -# 4.2   Linked list +# 4.2   Linked List -Memory space is a shared resource among all programs. In a complex system environment, available memory can be dispersed throughout the memory space. We understand that the memory allocated for an array must be continuous. However, for very large arrays, finding a sufficiently large contiguous memory space might be challenging. This is where the flexible advantage of linked lists becomes evident. +Memory space is a shared resource for all programs. In a complex system runtime environment, available memory space may be scattered throughout the memory. We know that the memory space for storing an array must be contiguous, and when the array is very large, the memory may not be able to provide such a large contiguous space. This is where the flexibility advantage of linked lists becomes apparent. -A linked list is a linear data structure in which each element is a node object, and the nodes are interconnected through "references". These references hold the memory addresses of subsequent nodes, enabling navigation from one node to the next. +A linked list is a linear data structure in which each element is a node object, and the nodes are connected through "references". A reference records the memory address of the next node, through which the next node can be accessed from the current node. -The design of linked lists allows for their nodes to be distributed across memory locations without requiring contiguous memory addresses. +The design of linked lists allows nodes to be stored scattered throughout the memory, and their memory addresses do not need to be contiguous. ![Linked list definition and storage method](linked_list.assets/linkedlist_definition.png){ class="animation-figure" }

Figure 4-5   Linked list definition and storage method

-As shown in Figure 4-5, we see that the basic building block of a linked list is the node object. Each node comprises two key components: the node's "value" and a "reference" to the next node. +Observing Figure 4-5, the basic unit of a linked list is a node object. Each node contains two pieces of data: the node's "value" and a "reference" to the next node. -- The first node in a linked list is the "head node", and the final one is the "tail node". -- The tail node points to "null", designated as `null` in Java, `nullptr` in C++, and `None` in Python. -- In languages that support pointers, like C, C++, Go, and Rust, this "reference" is typically implemented as a "pointer". +- The first node of a linked list is called the "head node", and the last node is called the "tail node". +- The tail node points to "null", which is denoted as `null`, `nullptr`, and `None` in Java, C++, and Python, respectively. +- In languages that support pointers, such as C, C++, Go, and Rust, the aforementioned "reference" should be replaced with "pointer". -As the code below illustrates, a `ListNode` in a linked list, besides holding a value, must also maintain an additional reference (or pointer). Therefore, **a linked list occupies more memory space than an array when storing the same quantity of data.**. +As shown in the following code, a linked list node `ListNode` contains not only a value but also an additional reference (pointer). Therefore, **linked lists occupy more memory space than arrays when storing the same amount of data**. === "Python" @@ -168,39 +168,39 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a === "Kotlin" ```kotlin title="" - - ``` - -=== "Zig" - - ```zig title="" - // Linked list node class - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // Node value - next: ?*Self = null, // Pointer to the next node - - // Constructor - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - } - }; + /* Linked list node class */ + // Constructor + class ListNode(x: Int) { + val _val: Int = x // Node value + val next: ListNode? = null // Reference to the next node } ``` -## 4.2.1   Common operations on linked lists +=== "Ruby" -### 1.   Initializing a linked list + ```ruby title="" + # Linked list node class + class ListNode + attr_accessor :val # Node value + attr_accessor :next # Reference to the next node -Constructing a linked list is a two-step process: first, initializing each node object, and second, forming the reference links between the nodes. After initialization, we can traverse all nodes sequentially from the head node by following the `next` reference. + def initialize(val=0, next_node=nil) + @val = val + @next = next_node + end + end + ``` + +## 4.2.1   Common Linked List Operations + +### 1.   Initializing a Linked List + +Building a linked list involves two steps: first, initializing each node object; second, constructing the reference relationships between nodes. Once initialization is complete, we can traverse all nodes starting from the head node of the linked list through the reference `next`. === "Python" ```python title="linked_list.py" - # Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 + # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 # Initialize each node n0 = ListNode(1) n1 = ListNode(3) @@ -217,7 +217,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "C++" ```cpp title="linked_list.cpp" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node ListNode* n0 = new ListNode(1); ListNode* n1 = new ListNode(3); @@ -234,7 +234,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "Java" ```java title="linked_list.java" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node ListNode n0 = new ListNode(1); ListNode n1 = new ListNode(3); @@ -251,7 +251,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "C#" ```csharp title="linked_list.cs" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node ListNode n0 = new(1); ListNode n1 = new(3); @@ -268,7 +268,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "Go" ```go title="linked_list.go" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node n0 := NewListNode(1) n1 := NewListNode(3) @@ -285,7 +285,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "Swift" ```swift title="linked_list.swift" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node let n0 = ListNode(x: 1) let n1 = ListNode(x: 3) @@ -302,7 +302,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "JS" ```javascript title="linked_list.js" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node const n0 = new ListNode(1); const n1 = new ListNode(3); @@ -319,7 +319,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "TS" ```typescript title="linked_list.ts" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node const n0 = new ListNode(1); const n1 = new ListNode(3); @@ -336,7 +336,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "Dart" ```dart title="linked_list.dart" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */\ // Initialize each node ListNode n0 = ListNode(1); ListNode n1 = ListNode(3); @@ -353,7 +353,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "Rust" ```rust title="linked_list.rs" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node let n0 = Rc::new(RefCell::new(ListNode { val: 1, next: None })); let n1 = Rc::new(RefCell::new(ListNode { val: 3, next: None })); @@ -371,7 +371,7 @@ Constructing a linked list is a two-step process: first, initializing each node === "C" ```c title="linked_list.c" - /* Initialize linked list: 1 -> 3 -> 2 -> 5 -> 4 */ + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node ListNode* n0 = newListNode(1); ListNode* n1 = newListNode(3); @@ -388,37 +388,53 @@ Constructing a linked list is a two-step process: first, initializing each node === "Kotlin" ```kotlin title="linked_list.kt" - - ``` - -=== "Zig" - - ```zig title="linked_list.zig" - // Initialize linked list + /* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */ // Initialize each node - var n0 = inc.ListNode(i32){.val = 1}; - var n1 = inc.ListNode(i32){.val = 3}; - var n2 = inc.ListNode(i32){.val = 2}; - var n3 = inc.ListNode(i32){.val = 5}; - var n4 = inc.ListNode(i32){.val = 4}; + val n0 = ListNode(1) + val n1 = ListNode(3) + val n2 = ListNode(2) + val n3 = ListNode(5) + val n4 = ListNode(4) // Build references between nodes - n0.next = &n1; - n1.next = &n2; - n2.next = &n3; - n3.next = &n4; + n0.next = n1; + n1.next = n2; + n2.next = n3; + n3.next = n4; ``` -The array as a whole is a variable, for instance, the array `nums` includes elements like `nums[0]`, `nums[1]`, and so on, whereas a linked list is made up of several distinct node objects. **We typically refer to a linked list by its head node**, for example, the linked list in the previous code snippet is referred to as `n0`. +=== "Ruby" -### 2.   Inserting nodes + ```ruby title="linked_list.rb" + # Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 + # Initialize each node + n0 = ListNode.new(1) + n1 = ListNode.new(3) + n2 = ListNode.new(2) + n3 = ListNode.new(5) + n4 = ListNode.new(4) + # Build references between nodes + n0.next = n1 + n1.next = n2 + n2.next = n3 + n3.next = n4 + ``` -Inserting a node into a linked list is very easy. As shown in Figure 4-6, let's assume we aim to insert a new node `P` between two adjacent nodes `n0` and `n1`. **This can be achieved by simply modifying two node references (pointers)**, with a time complexity of $O(1)$. +??? pythontutor "Code Visualization" -By comparison, inserting an element into an array has a time complexity of $O(n)$, which becomes less efficient when dealing with large data volumes. +
+ -![Linked list node insertion example](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } +An array is a single variable; for example, an array `nums` contains elements `nums[0]`, `nums[1]`, etc. A linked list, however, is composed of multiple independent node objects. **We typically use the head node as the reference to the linked list**; for example, the linked list in the above code can be referred to as linked list `n0`. -

Figure 4-6   Linked list node insertion example

+### 2.   Inserting a Node + +Inserting a node in a linked list is very easy. As shown in Figure 4-6, suppose we want to insert a new node `P` between two adjacent nodes `n0` and `n1`. **We only need to change two node references (pointers)**, with a time complexity of $O(1)$. + +In contrast, the time complexity of inserting an element in an array is $O(n)$, which is inefficient when dealing with large amounts of data. + +![Example of inserting a node into a linked list](linked_list.assets/linkedlist_insert_node.png){ class="animation-figure" } + +

Figure 4-6   Example of inserting a node into a linked list

=== "Python" @@ -455,78 +471,124 @@ By comparison, inserting an element into an array has a time complexity of $O(n) === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Insert} + /* Insert node P after node n0 in the linked list */ + void Insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{insertNode} + /* Insert node P after node n0 in the linked list */ + func insertNode(n0 *ListNode, P *ListNode) { + n1 := n0.Next + P.Next = n1 + n0.Next = P + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + func insert(n0: ListNode, P: ListNode) { + let n1 = n0.next + P.next = n1 + n0.next = P + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + function insert(n0, P) { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + function insert(n0: ListNode, P: ListNode): void { + const n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + void insert(ListNode n0, ListNode P) { + ListNode? n1 = n0.next; + P.next = n1; + n0.next = P; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + #[allow(non_snake_case)] + pub fn insert(n0: &Rc>>, P: Rc>>) { + let n1 = n0.borrow_mut().next.take(); + P.borrow_mut().next = n1; + n0.borrow_mut().next = Some(P); + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + void insert(ListNode *n0, ListNode *P) { + ListNode *n1 = n0->next; + P->next = n1; + n0->next = P; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{insert} + /* Insert node P after node n0 in the linked list */ + fun insert(n0: ListNode?, p: ListNode?) { + val n1 = n0?.next + p?.next = n1 + n0?.next = p + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{insert} + ### Insert node _p after node n0 in linked list ### + # Ruby's `p` is a built-in function, `P` is a constant, so use `_p` instead + def insert(n0, _p) + n1 = n0.next + _p.next = n1 + n0.next = _p + end ``` -=== "Zig" +### 3.   Removing a Node - ```zig title="linked_list.zig" - [class]{}-[func]{insert} - ``` +As shown in Figure 4-7, removing a node in a linked list is also very convenient. **We only need to change one node's reference (pointer)**. -### 3.   Deleting nodes +Note that although node `P` still points to `n1` after the deletion operation is complete, the linked list can no longer access `P` when traversing, which means `P` no longer belongs to this linked list. -As shown in Figure 4-7, deleting a node from a linked list is also very easy, **involving only the modification of a single node's reference (pointer)**. +![Removing a node from a linked list](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } -It's important to note that even though node `P` continues to point to `n1` after being deleted, it becomes inaccessible during linked list traversal. This effectively means that `P` is no longer a part of the linked list. - -![Linked list node deletion](linked_list.assets/linkedlist_remove_node.png){ class="animation-figure" } - -

Figure 4-7   Linked list node deletion

+

Figure 4-7   Removing a node from a linked list

=== "Python" @@ -574,78 +636,157 @@ It's important to note that even though node `P` continues to point to `n1` afte === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Remove} + /* Remove the first node after node n0 in the linked list */ + void Remove(ListNode n0) { + if (n0.next == null) + return; + // n0 -> P -> n1 + ListNode P = n0.next; + ListNode? n1 = P.next; + n0.next = n1; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{removeItem} + /* Remove the first node after node n0 in the linked list */ + func removeItem(n0 *ListNode) { + if n0.Next == nil { + return + } + // n0 -> P -> n1 + P := n0.Next + n1 := P.Next + n0.Next = n1 + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + func remove(n0: ListNode) { + if n0.next == nil { + return + } + // n0 -> P -> n1 + let P = n0.next + let n1 = P?.next + n0.next = n1 + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + function remove(n0) { + if (!n0.next) return; + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + function remove(n0: ListNode): void { + if (!n0.next) { + return; + } + // n0 -> P -> n1 + const P = n0.next; + const n1 = P.next; + n0.next = n1; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + void remove(ListNode n0) { + if (n0.next == null) return; + // n0 -> P -> n1 + ListNode P = n0.next!; + ListNode? n1 = P.next; + n0.next = n1; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + #[allow(non_snake_case)] + pub fn remove(n0: &Rc>>) { + // n0 -> P -> n1 + let P = n0.borrow_mut().next.take(); + if let Some(node) = P { + let n1 = node.borrow_mut().next.take(); + n0.borrow_mut().next = n1; + } + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{removeItem} + /* Remove the first node after node n0 in the linked list */ + // Note: stdio.h occupies the remove keyword + void removeItem(ListNode *n0) { + if (!n0->next) + return; + // n0 -> P -> n1 + ListNode *P = n0->next; + ListNode *n1 = P->next; + n0->next = n1; + // Free memory + free(P); + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{remove} + /* Remove the first node after node n0 in the linked list */ + fun remove(n0: ListNode?) { + if (n0?.next == null) + return + // n0 -> P -> n1 + val p = n0.next + val n1 = p?.next + n0.next = n1 + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{remove} + ### Delete first node after node n0 in linked list ### + def remove(n0) + return if n0.next.nil? + + # n0 -> remove_node -> n1 + remove_node = n0.next + n1 = remove_node.next + n0.next = n1 + end ``` -=== "Zig" +### 4.   Accessing a Node - ```zig title="linked_list.zig" - [class]{}-[func]{remove} - ``` - -### 4.   Accessing nodes - -**Accessing nodes in a linked list is less efficient**. As previously mentioned, any element in an array can be accessed in $O(1)$ time. In contrast, with a linked list, the program involves starting from the head node and sequentially traversing through the nodes until the desired node is found. In other words, to access the $i$-th node in a linked list, the program must iterate through $i - 1$ nodes, resulting in a time complexity of $O(n)$. +**Accessing nodes in a linked list is less efficient**. As mentioned in the previous section, we can access any element in an array in $O(1)$ time. This is not the case with linked lists. The program needs to start from the head node and traverse backward one by one until the target node is found. That is, accessing the $i$-th node in a linked list requires $i - 1$ iterations, with a time complexity of $O(n)$. === "Python" ```python title="linked_list.py" def access(head: ListNode, index: int) -> ListNode | None: - """Access the node at `index` in the linked list""" + """Access the node at index index in the linked list""" for _ in range(index): if not head: return None @@ -656,7 +797,7 @@ It's important to note that even though node `P` continues to point to `n1` afte === "C++" ```cpp title="linked_list.cpp" - /* Access the node at `index` in the linked list */ + /* Access the node at index index in the linked list */ ListNode *access(ListNode *head, int index) { for (int i = 0; i < index; i++) { if (head == nullptr) @@ -670,7 +811,7 @@ It's important to note that even though node `P` continues to point to `n1` afte === "Java" ```java title="linked_list.java" - /* Access the node at `index` in the linked list */ + /* Access the node at index index in the linked list */ ListNode access(ListNode head, int index) { for (int i = 0; i < index; i++) { if (head == null) @@ -684,78 +825,167 @@ It's important to note that even though node `P` continues to point to `n1` afte === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Access} + /* Access the node at index index in the linked list */ + ListNode? Access(ListNode? head, int index) { + for (int i = 0; i < index; i++) { + if (head == null) + return null; + head = head.next; + } + return head; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + func access(head *ListNode, index int) *ListNode { + for i := 0; i < index; i++ { + if head == nil { + return nil + } + head = head.Next + } + return head + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + func access(head: ListNode, index: Int) -> ListNode? { + var head: ListNode? = head + for _ in 0 ..< index { + if head == nil { + return nil + } + head = head?.next + } + return head + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + function access(head, index) { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + function access(head: ListNode | null, index: number): ListNode | null { + for (let i = 0; i < index; i++) { + if (!head) { + return null; + } + head = head.next; + } + return head; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + ListNode? access(ListNode? head, int index) { + for (var i = 0; i < index; i++) { + if (head == null) return null; + head = head.next; + } + return head; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + pub fn access(head: Rc>>, index: i32) -> Option>>> { + fn dfs( + head: Option<&Rc>>>, + index: i32, + ) -> Option>>> { + if index <= 0 { + return head.cloned(); + } + + if let Some(node) = head { + dfs(node.borrow().next.as_ref(), index - 1) + } else { + None + } + } + + dfs(Some(head).as_ref(), index) + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + ListNode *access(ListNode *head, int index) { + for (int i = 0; i < index; i++) { + if (head == NULL) + return NULL; + head = head->next; + } + return head; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{access} + /* Access the node at index index in the linked list */ + fun access(head: ListNode?, index: Int): ListNode? { + var h = head + for (i in 0.. int: - """Search for the first node with value target in the linked list""" + """Find the first node with value target in the linked list""" index = 0 while head: if head.val == target: @@ -768,7 +998,7 @@ Traverse the linked list to locate a node whose value matches `target`, and then === "C++" ```cpp title="linked_list.cpp" - /* Search for the first node with value target in the linked list */ + /* Find the first node with value target in the linked list */ int find(ListNode *head, int target) { int index = 0; while (head != nullptr) { @@ -784,7 +1014,7 @@ Traverse the linked list to locate a node whose value matches `target`, and then === "Java" ```java title="linked_list.java" - /* Search for the first node with value target in the linked list */ + /* Find the first node with value target in the linked list */ int find(ListNode head, int target) { int index = 0; while (head != null) { @@ -800,111 +1030,216 @@ Traverse the linked list to locate a node whose value matches `target`, and then === "C#" ```csharp title="linked_list.cs" - [class]{linked_list}-[func]{Find} + /* Find the first node with value target in the linked list */ + int Find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) + return index; + head = head.next; + index++; + } + return -1; + } ``` === "Go" ```go title="linked_list.go" - [class]{}-[func]{findNode} + /* Find the first node with value target in the linked list */ + func findNode(head *ListNode, target int) int { + index := 0 + for head != nil { + if head.Val == target { + return index + } + head = head.Next + index++ + } + return -1 + } ``` === "Swift" ```swift title="linked_list.swift" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + func find(head: ListNode, target: Int) -> Int { + var head: ListNode? = head + var index = 0 + while head != nil { + if head?.val == target { + return index + } + head = head?.next + index += 1 + } + return -1 + } ``` === "JS" ```javascript title="linked_list.js" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + function find(head, target) { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } ``` === "TS" ```typescript title="linked_list.ts" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + function find(head: ListNode | null, target: number): number { + let index = 0; + while (head !== null) { + if (head.val === target) { + return index; + } + head = head.next; + index += 1; + } + return -1; + } ``` === "Dart" ```dart title="linked_list.dart" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + int find(ListNode? head, int target) { + int index = 0; + while (head != null) { + if (head.val == target) { + return index; + } + head = head.next; + index++; + } + return -1; + } ``` === "Rust" ```rust title="linked_list.rs" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + pub fn find(head: Rc>>, target: T) -> i32 { + fn find(head: Option<&Rc>>>, target: T, idx: i32) -> i32 { + if let Some(node) = head { + if node.borrow().val == target { + return idx; + } + return find(node.borrow().next.as_ref(), target, idx + 1); + } else { + -1 + } + } + + find(Some(head).as_ref(), target, 0) + } ``` === "C" ```c title="linked_list.c" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + int find(ListNode *head, int target) { + int index = 0; + while (head) { + if (head->val == target) + return index; + head = head->next; + index++; + } + return -1; + } ``` === "Kotlin" ```kotlin title="linked_list.kt" - [class]{}-[func]{find} + /* Find the first node with value target in the linked list */ + fun find(head: ListNode?, target: Int): Int { + var index = 0 + var h = head + while (h != null) { + if (h._val == target) + return index + h = h.next + index++ + } + return -1 + } ``` === "Ruby" ```ruby title="linked_list.rb" - [class]{}-[func]{find} + ### Find first node with value target in linked list ### + def find(head, target) + index = 0 + while head + return index if head.val == target + head = head.next + index += 1 + end + + -1 + end ``` -=== "Zig" +## 4.2.2   Arrays vs. Linked Lists - ```zig title="linked_list.zig" - [class]{}-[func]{find} - ``` +Table 4-1 summarizes the characteristics of arrays and linked lists and compares their operational efficiencies. Since they employ two opposite storage strategies, their various properties and operational efficiencies also exhibit contrasting characteristics. -## 4.2.2   Arrays vs. linked lists - -Table 4-1 summarizes the characteristics of arrays and linked lists, and it also compares their efficiencies in various operations. Because they utilize opposing storage strategies, their respective properties and operational efficiencies exhibit distinct contrasts. - -

Table 4-1   Efficiency comparison of arrays and linked lists

+

Table 4-1   Comparison of array and linked list efficiencies

-| | Arrays | Linked Lists | -| ------------------ | ------------------------------------------------ | ----------------------- | -| Storage | Contiguous Memory Space | Dispersed Memory Space | -| Capacity Expansion | Fixed Length | Flexible Expansion | -| Memory Efficiency | Less Memory per Element, Potential Space Wastage | More Memory per Element | -| Accessing Elements | $O(1)$ | $O(n)$ | -| Adding Elements | $O(n)$ | $O(1)$ | -| Deleting Elements | $O(n)$ | $O(1)$ | +| | Array | Linked List | +| ---------------------- | --------------------------------------------- | -------------------------- | +| Storage method | Contiguous memory space | Scattered memory space | +| Capacity expansion | Immutable length | Flexible expansion | +| Memory efficiency | Elements occupy less memory, but space may be wasted | Elements occupy more memory | +| Accessing an element | $O(1)$ | $O(n)$ | +| Adding an element | $O(n)$ | $O(1)$ | +| Removing an element | $O(n)$ | $O(1)$ |
-## 4.2.3   Common types of linked lists +## 4.2.3   Common Types of Linked Lists -As shown in Figure 4-8, there are three common types of linked lists. +As shown in Figure 4-8, there are three common types of linked lists: -- **Singly linked list**: This is the standard linked list described earlier. Nodes in a singly linked list include a value and a reference to the next node. The first node is known as the head node, and the last node, which points to null (`None`), is the tail node. -- **Circular linked list**: This is formed when the tail node of a singly linked list points back to the head node, creating a loop. In a circular linked list, any node can function as the head node. -- **Doubly linked list**: In contrast to a singly linked list, a doubly linked list maintains references in two directions. Each node contains references (pointer) to both its successor (the next node) and predecessor (the previous node). Although doubly linked lists offer more flexibility for traversing in either direction, they also consume more memory space. +- **Singly linked list**: This is the ordinary linked list introduced earlier. The nodes of a singly linked list contain a value and a reference to the next node. We call the first node the head node and the last node the tail node, which points to null `None`. +- **Circular linked list**: If we make the tail node of a singly linked list point to the head node (connecting the tail to the head), we get a circular linked list. In a circular linked list, any node can be viewed as the head node. +- **Doubly linked list**: Compared to a singly linked list, a doubly linked list records references in both directions. The node definition of a doubly linked list includes references to both the successor node (next node) and the predecessor node (previous node). Compared to a singly linked list, a doubly linked list is more flexible and can traverse the linked list in both directions, but it also requires more memory space. === "Python" ```python title="" class ListNode: - """Bidirectional linked list node class""" + """Doubly linked list node class""" def __init__(self, val: int): self.val: int = val # Node value self.next: ListNode | None = None # Reference to the successor node - self.prev: ListNode | None = None # Reference to a predecessor node + self.prev: ListNode | None = None # Reference to the predecessor node ``` === "C++" ```cpp title="" - /* Bidirectional linked list node structure */ + /* Doubly linked list node structure */ struct ListNode { int val; // Node value ListNode *next; // Pointer to the successor node @@ -916,10 +1251,10 @@ As shown in Figure 4-8, there are three common types of linked lists. === "Java" ```java title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode { int val; // Node value - ListNode next; // Reference to the next node + ListNode next; // Reference to the successor node ListNode prev; // Reference to the predecessor node ListNode(int x) { val = x; } // Constructor } @@ -928,10 +1263,10 @@ As shown in Figure 4-8, there are three common types of linked lists. === "C#" ```csharp title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode(int x) { // Constructor int val = x; // Node value - ListNode next; // Reference to the next node + ListNode next; // Reference to the successor node ListNode prev; // Reference to the predecessor node } ``` @@ -939,14 +1274,14 @@ As shown in Figure 4-8, there are three common types of linked lists. === "Go" ```go title="" - /* Bidirectional linked list node structure */ + /* Doubly linked list node structure */ type DoublyListNode struct { Val int // Node value Next *DoublyListNode // Pointer to the successor node Prev *DoublyListNode // Pointer to the predecessor node } - // NewDoublyListNode initialization + // NewDoublyListNode Initialization func NewDoublyListNode(val int) *DoublyListNode { return &DoublyListNode{ Val: val, @@ -959,10 +1294,10 @@ As shown in Figure 4-8, there are three common types of linked lists. === "Swift" ```swift title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode { var val: Int // Node value - var next: ListNode? // Reference to the next node + var next: ListNode? // Reference to the successor node var prev: ListNode? // Reference to the predecessor node init(x: Int) { // Constructor @@ -974,7 +1309,7 @@ As shown in Figure 4-8, there are three common types of linked lists. === "JS" ```javascript title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode { constructor(val, next, prev) { this.val = val === undefined ? 0 : val; // Node value @@ -987,7 +1322,7 @@ As shown in Figure 4-8, there are three common types of linked lists. === "TS" ```typescript title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode { val: number; next: ListNode | null; @@ -1003,11 +1338,11 @@ As shown in Figure 4-8, there are three common types of linked lists. === "Dart" ```dart title="" - /* Bidirectional linked list node class */ + /* Doubly linked list node class */ class ListNode { int val; // Node value - ListNode next; // Reference to the next node - ListNode prev; // Reference to the predecessor node + ListNode? next; // Reference to the successor node + ListNode? prev; // Reference to the predecessor node ListNode(this.val, [this.next, this.prev]); // Constructor } ``` @@ -1018,15 +1353,15 @@ As shown in Figure 4-8, there are three common types of linked lists. use std::rc::Rc; use std::cell::RefCell; - /* Bidirectional linked list node type */ + /* Doubly linked list node type */ #[derive(Debug)] struct ListNode { val: i32, // Node value - next: Option>>, // Pointer to successor node - prev: Option>>, // Pointer to predecessor node + next: Option>>, // Pointer to the successor node + prev: Option>>, // Pointer to the predecessor node } - /* Constructors */ + /* Constructor */ impl ListNode { fn new(val: i32) -> Self { ListNode { @@ -1041,16 +1376,16 @@ As shown in Figure 4-8, there are three common types of linked lists. === "C" ```c title="" - /* Bidirectional linked list node structure */ + /* Doubly linked list node structure */ typedef struct ListNode { int val; // Node value struct ListNode *next; // Pointer to the successor node struct ListNode *prev; // Pointer to the predecessor node } ListNode; - /* Constructors */ + /* Constructor */ ListNode *newListNode(int val) { - ListNode *node, *next; + ListNode *node; node = (ListNode *) malloc(sizeof(ListNode)); node->val = val; node->next = NULL; @@ -1062,50 +1397,51 @@ As shown in Figure 4-8, there are three common types of linked lists. === "Kotlin" ```kotlin title="" - + /* Doubly linked list node class */ + // Constructor + class ListNode(x: Int) { + val _val: Int = x // Node value + val next: ListNode? = null // Reference to the successor node + val prev: ListNode? = null // Reference to the predecessor node + } ``` -=== "Zig" +=== "Ruby" - ```zig title="" - // Bidirectional linked list node class - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); + ```ruby title="" + # Doubly linked list node class + class ListNode + attr_accessor :val # Node value + attr_accessor :next # Reference to the successor node + attr_accessor :prev # Reference to the predecessor node - val: T = 0, // Node value - next: ?*Self = null, // Pointer to the successor node - prev: ?*Self = null, // Pointer to the predecessor node - - // Constructor - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - self.prev = null; - } - }; - } + def initialize(val=0, next_node=nil, prev_node=nil) + @val = val + @next = next_node + @prev = prev_node + end + end ``` ![Common types of linked lists](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" }

Figure 4-8   Common types of linked lists

-## 4.2.4   Typical applications of linked lists +## 4.2.4   Typical Applications of Linked Lists -Singly linked lists are frequently utilized in implementing stacks, queues, hash tables, and graphs. +Singly linked lists are commonly used to implement stacks, queues, hash tables, and graphs. -- **Stacks and queues**: In singly linked lists, if insertions and deletions occur at the same end, it behaves like a stack (last-in-first-out). Conversely, if insertions are at one end and deletions at the other, it functions like a queue (first-in-first-out). -- **Hash tables**: Linked lists are used in chaining, a popular method for resolving hash collisions. Here, all collided elements are grouped into a linked list. -- **Graphs**: Adjacency lists, a standard method for graph representation, associate each graph vertex with a linked list. This list contains elements that represent vertices connected to the corresponding vertex. +- **Stacks and queues**: When insertion and deletion operations both occur at one end of the linked list, it exhibits last-in-first-out characteristics, corresponding to a stack. When insertion operations occur at one end of the linked list and deletion operations occur at the other end, it exhibits first-in-first-out characteristics, corresponding to a queue. +- **Hash tables**: Separate chaining is one of the mainstream solutions for resolving hash collisions. In this approach, all colliding elements are placed in a linked list. +- **Graphs**: An adjacency list is a common way to represent a graph, where each vertex in the graph is associated with a linked list, and each element in the linked list represents another vertex connected to that vertex. -Doubly linked lists are ideal for scenarios requiring rapid access to preceding and succeeding elements. +Doubly linked lists are commonly used in scenarios where quick access to the previous and next elements is needed. -- **Advanced data structures**: In structures like red-black trees and B-trees, accessing a node's parent is essential. This is achieved by incorporating a reference to the parent node in each node, akin to a doubly linked list. -- **Browser history**: In web browsers, doubly linked lists facilitate navigating the history of visited pages when users click forward or back. -- **LRU algorithm**: Doubly linked lists are apt for Least Recently Used (LRU) cache eviction algorithms, enabling swift identification of the least recently used data and facilitating fast node addition and removal. +- **Advanced data structures**: For example, in red-black trees and B-trees, we need to access the parent node of a node, which can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list. +- **Browser history**: In web browsers, when a user clicks the forward or backward button, the browser needs to know the previous and next web pages the user visited. The characteristics of doubly linked lists make this operation simple. +- **LRU algorithm**: In cache eviction (LRU) algorithms, we need to quickly find the least recently used data and support quick addition and deletion of nodes. Using a doubly linked list is very suitable for this. -Circular linked lists are ideal for applications that require periodic operations, such as resource scheduling in operating systems. +Circular linked lists are commonly used in scenarios that require periodic operations, such as operating system resource scheduling. -- **Round-robin scheduling algorithm**: In operating systems, the round-robin scheduling algorithm is a common CPU scheduling method, requiring cycling through a group of processes. Each process is assigned a time slice, and upon expiration, the CPU rotates to the next process. This cyclical operation can be efficiently realized using a circular linked list, allowing for a fair and time-shared system among all processes. -- **Data buffers**: Circular linked lists are also used in data buffers, like in audio and video players, where the data stream is divided into multiple buffer blocks arranged in a circular fashion for seamless playback. +- **Round-robin scheduling algorithm**: In operating systems, round-robin scheduling is a common CPU scheduling algorithm that needs to cycle through a set of processes. Each process is assigned a time slice, and when the time slice expires, the CPU switches to the next process. This cyclic operation can be implemented using a circular linked list. +- **Data buffers**: In some data buffer implementations, circular linked lists may also be used. For example, in audio and video players, the data stream may be divided into multiple buffer blocks and placed in a circular linked list to achieve seamless playback. diff --git a/en/docs/chapter_array_and_linkedlist/list.md b/en/docs/chapter_array_and_linkedlist/list.md index 7dbc02836..ac64cbfef 100755 --- a/en/docs/chapter_array_and_linkedlist/list.md +++ b/en/docs/chapter_array_and_linkedlist/list.md @@ -4,27 +4,27 @@ comments: true # 4.3   List -A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, addition, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays. +A list is an abstract data structure concept that represents an ordered collection of elements, supporting operations such as element access, modification, insertion, deletion, and traversal, without requiring users to consider capacity limitations. Lists can be implemented based on linked lists or arrays. -- A linked list inherently serves as a list, supporting operations for adding, deleting, searching, and modifying elements, with the flexibility to dynamically adjust its size. -- Arrays also support these operations, but due to their immutable length, they can be considered as a list with a length limit. +- A linked list can naturally be viewed as a list, supporting element insertion, deletion, search, and modification operations, and can flexibly expand dynamically. +- An array also supports element insertion, deletion, search, and modification, but since its length is immutable, it can only be viewed as a list with length limitations. -When implementing lists using arrays, **the immutability of length reduces the practicality of the list**. This is because predicting the amount of data to be stored in advance is often challenging, making it difficult to choose an appropriate list length. If the length is too small, it may not meet the requirements; if too large, it may waste memory space. +When implementing lists using arrays, **the immutable length property reduces the practicality of the list**. This is because we usually cannot determine in advance how much data we need to store, making it difficult to choose an appropriate list length. If the length is too small, it may fail to meet usage requirements; if the length is too large, it will waste memory space. -To solve this problem, we can implement lists using a dynamic array. It inherits the advantages of arrays and can dynamically expand during program execution. +To solve this problem, we can use a dynamic array to implement a list. It inherits all the advantages of arrays and can dynamically expand during program execution. -In fact, **many programming languages' standard libraries implement lists using dynamic arrays**, such as Python's `list`, Java's `ArrayList`, C++'s `vector`, and C#'s `List`. In the following discussion, we will consider "list" and "dynamic array" as synonymous concepts. +In fact, **the lists provided in the standard libraries of many programming languages are implemented based on dynamic arrays**, such as `list` in Python, `ArrayList` in Java, `vector` in C++, and `List` in C#. In the following discussion, we will treat "list" and "dynamic array" as equivalent concepts. -## 4.3.1   Common list operations +## 4.3.1   Common List Operations -### 1.   Initializing a list +### 1.   Initialize a List -We typically use two initialization methods: "without initial values" and "with initial values". +We typically use two initialization methods: "without initial values" and "with initial values": === "Python" ```python title="list.py" - # Initialize list + # Initialize a list # Without initial values nums1: list[int] = [] # With initial values @@ -34,8 +34,8 @@ We typically use two initialization methods: "without initial values" and "with === "C++" ```cpp title="list.cpp" - /* Initialize list */ - // Note, in C++ the vector is the equivalent of nums described here + /* Initialize a list */ + // Note that vector in C++ is equivalent to nums as described in this article // Without initial values vector nums1; // With initial values @@ -45,10 +45,10 @@ We typically use two initialization methods: "without initial values" and "with === "Java" ```java title="list.java" - /* Initialize list */ + /* Initialize a list */ // Without initial values List nums1 = new ArrayList<>(); - // With initial values (note the element type should be the wrapper class Integer[] for int[]) + // With initial values (note that array elements should use the wrapper class Integer[] instead of int[]) Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 }; List nums = new ArrayList<>(Arrays.asList(numbers)); ``` @@ -56,7 +56,7 @@ We typically use two initialization methods: "without initial values" and "with === "C#" ```csharp title="list.cs" - /* Initialize list */ + /* Initialize a list */ // Without initial values List nums1 = []; // With initial values @@ -67,7 +67,7 @@ We typically use two initialization methods: "without initial values" and "with === "Go" ```go title="list_test.go" - /* Initialize list */ + /* Initialize a list */ // Without initial values nums1 := []int{} // With initial values @@ -77,7 +77,7 @@ We typically use two initialization methods: "without initial values" and "with === "Swift" ```swift title="list.swift" - /* Initialize list */ + /* Initialize a list */ // Without initial values let nums1: [Int] = [] // With initial values @@ -87,7 +87,7 @@ We typically use two initialization methods: "without initial values" and "with === "JS" ```javascript title="list.js" - /* Initialize list */ + /* Initialize a list */ // Without initial values const nums1 = []; // With initial values @@ -97,7 +97,7 @@ We typically use two initialization methods: "without initial values" and "with === "TS" ```typescript title="list.ts" - /* Initialize list */ + /* Initialize a list */ // Without initial values const nums1: number[] = []; // With initial values @@ -107,7 +107,7 @@ We typically use two initialization methods: "without initial values" and "with === "Dart" ```dart title="list.dart" - /* Initialize list */ + /* Initialize a list */ // Without initial values List nums1 = []; // With initial values @@ -117,7 +117,7 @@ We typically use two initialization methods: "without initial values" and "with === "Rust" ```rust title="list.rs" - /* Initialize list */ + /* Initialize a list */ // Without initial values let nums1: Vec = Vec::new(); // With initial values @@ -133,119 +133,130 @@ We typically use two initialization methods: "without initial values" and "with === "Kotlin" ```kotlin title="list.kt" - + /* Initialize a list */ + // Without initial values + var nums1 = listOf() + // With initial values + var numbers = arrayOf(1, 3, 2, 5, 4) + var nums = numbers.toMutableList() ``` -=== "Zig" +=== "Ruby" - ```zig title="list.zig" - // Initialize list - var nums = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums.deinit(); - try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); + ```ruby title="list.rb" + # Initialize a list + # Without initial values + nums1 = [] + # With initial values + nums = [1, 3, 2, 5, 4] ``` -### 2.   Accessing elements +??? pythontutor "Code Visualization" -Lists are essentially arrays, thus they can access and update elements in $O(1)$ time, which is very efficient. +
+ + +### 2.   Access Elements + +Since a list is essentially an array, we can access and update elements in $O(1)$ time complexity, which is very efficient. === "Python" ```python title="list.py" - # Access elements - num: int = nums[1] # Access the element at index 1 + # Access an element + num: int = nums[1] # Access element at index 1 - # Update elements - nums[1] = 0 # Update the element at index 1 to 0 + # Update an element + nums[1] = 0 # Update element at index 1 to 0 ``` === "C++" ```cpp title="list.cpp" - /* Access elements */ - int num = nums[1]; // Access the element at index 1 + /* Access an element */ + int num = nums[1]; // Access element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "Java" ```java title="list.java" - /* Access elements */ - int num = nums.get(1); // Access the element at index 1 + /* Access an element */ + int num = nums.get(1); // Access element at index 1 - /* Update elements */ - nums.set(1, 0); // Update the element at index 1 to 0 + /* Update an element */ + nums.set(1, 0); // Update element at index 1 to 0 ``` === "C#" ```csharp title="list.cs" - /* Access elements */ - int num = nums[1]; // Access the element at index 1 + /* Access an element */ + int num = nums[1]; // Access element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "Go" ```go title="list_test.go" - /* Access elements */ - num := nums[1] // Access the element at index 1 + /* Access an element */ + num := nums[1] // Access element at index 1 - /* Update elements */ - nums[1] = 0 // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0 // Update element at index 1 to 0 ``` === "Swift" ```swift title="list.swift" - /* Access elements */ - let num = nums[1] // Access the element at index 1 + /* Access an element */ + let num = nums[1] // Access element at index 1 - /* Update elements */ - nums[1] = 0 // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0 // Update element at index 1 to 0 ``` === "JS" ```javascript title="list.js" - /* Access elements */ - const num = nums[1]; // Access the element at index 1 + /* Access an element */ + const num = nums[1]; // Access element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "TS" ```typescript title="list.ts" - /* Access elements */ - const num: number = nums[1]; // Access the element at index 1 + /* Access an element */ + const num: number = nums[1]; // Access element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "Dart" ```dart title="list.dart" - /* Access elements */ - int num = nums[1]; // Access the element at index 1 + /* Access an element */ + int num = nums[1]; // Access element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "Rust" ```rust title="list.rs" - /* Access elements */ - let num: i32 = nums[1]; // Access the element at index 1 - /* Update elements */ - nums[1] = 0; // Update the element at index 1 to 0 + /* Access an element */ + let num: i32 = nums[1]; // Access element at index 1 + /* Update an element */ + nums[1] = 0; // Update element at index 1 to 0 ``` === "C" @@ -257,221 +268,228 @@ Lists are essentially arrays, thus they can access and update elements in $O(1)$ === "Kotlin" ```kotlin title="list.kt" - + /* Access an element */ + val num = nums[1] // Access element at index 1 + /* Update an element */ + nums[1] = 0 // Update element at index 1 to 0 ``` -=== "Zig" +=== "Ruby" - ```zig title="list.zig" - // Access elements - var num = nums.items[1]; // Access the element at index 1 - - // Update elements - nums.items[1] = 0; // Update the element at index 1 to 0 + ```ruby title="list.rb" + # Access an element + num = nums[1] # Access element at index 1 + # Update an element + nums[1] = 0 # Update element at index 1 to 0 ``` -### 3.   Inserting and removing elements +??? pythontutor "Code Visualization" -Compared to arrays, lists offer more flexibility in adding and removing elements. While adding elements to the end of a list is an $O(1)$ operation, the efficiency of inserting and removing elements elsewhere in the list remains the same as in arrays, with a time complexity of $O(n)$. +
+ + +### 3.   Insert and Delete Elements + +Compared to arrays, lists can freely add and delete elements. Adding an element at the end of a list has a time complexity of $O(1)$, but inserting and deleting elements still have the same efficiency as arrays, with a time complexity of $O(n)$. === "Python" ```python title="list.py" - # Clear list + # Clear the list nums.clear() - # Append elements at the end + # Add elements at the end nums.append(1) nums.append(3) nums.append(2) nums.append(5) nums.append(4) - # Insert element in the middle + # Insert an element in the middle nums.insert(3, 6) # Insert number 6 at index 3 - # Remove elements - nums.pop(3) # Remove the element at index 3 + # Delete an element + nums.pop(3) # Delete element at index 3 ``` === "C++" ```cpp title="list.cpp" - /* Clear list */ + /* Clear the list */ nums.clear(); - /* Append elements at the end */ + /* Add elements at the end */ nums.push_back(1); nums.push_back(3); nums.push_back(2); nums.push_back(5); nums.push_back(4); - /* Insert element in the middle */ + /* Insert an element in the middle */ nums.insert(nums.begin() + 3, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.erase(nums.begin() + 3); // Remove the element at index 3 + /* Delete an element */ + nums.erase(nums.begin() + 3); // Delete element at index 3 ``` === "Java" ```java title="list.java" - /* Clear list */ + /* Clear the list */ nums.clear(); - /* Append elements at the end */ + /* Add elements at the end */ nums.add(1); nums.add(3); nums.add(2); nums.add(5); nums.add(4); - /* Insert element in the middle */ + /* Insert an element in the middle */ nums.add(3, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.remove(3); // Remove the element at index 3 + /* Delete an element */ + nums.remove(3); // Delete element at index 3 ``` === "C#" ```csharp title="list.cs" - /* Clear list */ + /* Clear the list */ nums.Clear(); - /* Append elements at the end */ + /* Add elements at the end */ nums.Add(1); nums.Add(3); nums.Add(2); nums.Add(5); nums.Add(4); - /* Insert element in the middle */ - nums.Insert(3, 6); + /* Insert an element in the middle */ + nums.Insert(3, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.RemoveAt(3); + /* Delete an element */ + nums.RemoveAt(3); // Delete element at index 3 ``` === "Go" ```go title="list_test.go" - /* Clear list */ + /* Clear the list */ nums = nil - /* Append elements at the end */ + /* Add elements at the end */ nums = append(nums, 1) nums = append(nums, 3) nums = append(nums, 2) nums = append(nums, 5) nums = append(nums, 4) - /* Insert element in the middle */ + /* Insert an element in the middle */ nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // Insert number 6 at index 3 - /* Remove elements */ - nums = append(nums[:3], nums[4:]...) // Remove the element at index 3 + /* Delete an element */ + nums = append(nums[:3], nums[4:]...) // Delete element at index 3 ``` === "Swift" ```swift title="list.swift" - /* Clear list */ + /* Clear the list */ nums.removeAll() - /* Append elements at the end */ + /* Add elements at the end */ nums.append(1) nums.append(3) nums.append(2) nums.append(5) nums.append(4) - /* Insert element in the middle */ + /* Insert an element in the middle */ nums.insert(6, at: 3) // Insert number 6 at index 3 - /* Remove elements */ - nums.remove(at: 3) // Remove the element at index 3 + /* Delete an element */ + nums.remove(at: 3) // Delete element at index 3 ``` === "JS" ```javascript title="list.js" - /* Clear list */ + /* Clear the list */ nums.length = 0; - /* Append elements at the end */ + /* Add elements at the end */ nums.push(1); nums.push(3); nums.push(2); nums.push(5); nums.push(4); - /* Insert element in the middle */ - nums.splice(3, 0, 6); + /* Insert an element in the middle */ + nums.splice(3, 0, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.splice(3, 1); + /* Delete an element */ + nums.splice(3, 1); // Delete element at index 3 ``` === "TS" ```typescript title="list.ts" - /* Clear list */ + /* Clear the list */ nums.length = 0; - /* Append elements at the end */ + /* Add elements at the end */ nums.push(1); nums.push(3); nums.push(2); nums.push(5); nums.push(4); - /* Insert element in the middle */ - nums.splice(3, 0, 6); + /* Insert an element in the middle */ + nums.splice(3, 0, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.splice(3, 1); + /* Delete an element */ + nums.splice(3, 1); // Delete element at index 3 ``` === "Dart" ```dart title="list.dart" - /* Clear list */ + /* Clear the list */ nums.clear(); - /* Append elements at the end */ + /* Add elements at the end */ nums.add(1); nums.add(3); nums.add(2); nums.add(5); nums.add(4); - /* Insert element in the middle */ + /* Insert an element in the middle */ nums.insert(3, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.removeAt(3); // Remove the element at index 3 + /* Delete an element */ + nums.removeAt(3); // Delete element at index 3 ``` === "Rust" ```rust title="list.rs" - /* Clear list */ + /* Clear the list */ nums.clear(); - /* Append elements at the end */ + /* Add elements at the end */ nums.push(1); nums.push(3); nums.push(2); nums.push(5); nums.push(4); - /* Insert element in the middle */ + /* Insert an element in the middle */ nums.insert(3, 6); // Insert number 6 at index 3 - /* Remove elements */ - nums.remove(3); // Remove the element at index 3 + /* Delete an element */ + nums.remove(3); // Delete element at index 3 ``` === "C" @@ -483,42 +501,61 @@ Compared to arrays, lists offer more flexibility in adding and removing elements === "Kotlin" ```kotlin title="list.kt" + /* Clear the list */ + nums.clear(); + /* Add elements at the end */ + nums.add(1); + nums.add(3); + nums.add(2); + nums.add(5); + nums.add(4); + + /* Insert an element in the middle */ + nums.add(3, 6); // Insert number 6 at index 3 + + /* Delete an element */ + nums.remove(3); // Delete element at index 3 ``` -=== "Zig" +=== "Ruby" - ```zig title="list.zig" - // Clear list - nums.clearRetainingCapacity(); + ```ruby title="list.rb" + # Clear the list + nums.clear - // Append elements at the end - try nums.append(1); - try nums.append(3); - try nums.append(2); - try nums.append(5); - try nums.append(4); + # Add elements at the end + nums << 1 + nums << 3 + nums << 2 + nums << 5 + nums << 4 - // Insert element in the middle - try nums.insert(3, 6); // Insert number 6 at index 3 + # Insert an element in the middle + nums.insert(3, 6) # Insert number 6 at index 3 - // Remove elements - _ = nums.orderedRemove(3); // Remove the element at index 3 + # Delete an element + nums.delete_at(3) # Delete element at index 3 ``` -### 4.   Iterating the list +??? pythontutor "Code Visualization" -Similar to arrays, lists can be iterated either by using indices or by directly iterating through each element. +
+ + +### 4.   Traverse a List + +Like arrays, lists can be traversed by index or by directly iterating through elements. === "Python" ```python title="list.py" - # Iterate through the list by index + # Traverse the list by index count = 0 for i in range(len(nums)): count += nums[i] - # Iterate directly through list elements + # Traverse list elements directly for num in nums: count += num ``` @@ -526,13 +563,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "C++" ```cpp title="list.cpp" - /* Iterate through the list by index */ + /* Traverse the list by index */ int count = 0; for (int i = 0; i < nums.size(); i++) { count += nums[i]; } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0; for (int num : nums) { count += num; @@ -542,13 +579,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Java" ```java title="list.java" - /* Iterate through the list by index */ + /* Traverse the list by index */ int count = 0; for (int i = 0; i < nums.size(); i++) { count += nums.get(i); } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ for (int num : nums) { count += num; } @@ -557,13 +594,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "C#" ```csharp title="list.cs" - /* Iterate through the list by index */ + /* Traverse the list by index */ int count = 0; for (int i = 0; i < nums.Count; i++) { count += nums[i]; } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0; foreach (int num in nums) { count += num; @@ -573,13 +610,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Go" ```go title="list_test.go" - /* Iterate through the list by index */ + /* Traverse the list by index */ count := 0 for i := 0; i < len(nums); i++ { count += nums[i] } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0 for _, num := range nums { count += num @@ -589,13 +626,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Swift" ```swift title="list.swift" - /* Iterate through the list by index */ + /* Traverse the list by index */ var count = 0 for i in nums.indices { count += nums[i] } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0 for num in nums { count += num @@ -605,13 +642,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "JS" ```javascript title="list.js" - /* Iterate through the list by index */ + /* Traverse the list by index */ let count = 0; for (let i = 0; i < nums.length; i++) { count += nums[i]; } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0; for (const num of nums) { count += num; @@ -621,13 +658,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "TS" ```typescript title="list.ts" - /* Iterate through the list by index */ + /* Traverse the list by index */ let count = 0; for (let i = 0; i < nums.length; i++) { count += nums[i]; } - /* Iterate directly through list elements */ + /* Traverse list elements directly */ count = 0; for (const num of nums) { count += num; @@ -637,13 +674,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Dart" ```dart title="list.dart" - /* Iterate through the list by index */ + /* Traverse the list by index */ int count = 0; for (var i = 0; i < nums.length; i++) { count += nums[i]; } - - /* Iterate directly through list elements */ + + /* Traverse list elements directly */ count = 0; for (var num in nums) { count += num; @@ -653,13 +690,13 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Rust" ```rust title="list.rs" - // Iterate through the list by index + // Traverse the list by index let mut _count = 0; for i in 0..nums.len() { _count += nums[i]; } - // Iterate directly through list elements + // Traverse list elements directly _count = 0; for num in &nums { _count += num; @@ -675,36 +712,49 @@ Similar to arrays, lists can be iterated either by using indices or by directly === "Kotlin" ```kotlin title="list.kt" - - ``` - -=== "Zig" - - ```zig title="list.zig" - // Iterate through the list by index - var count: i32 = 0; - var i: i32 = 0; - while (i < nums.items.len) : (i += 1) { - count += nums[i]; + /* Traverse the list by index */ + var count = 0 + for (i in nums.indices) { + count += nums[i] } - // Iterate directly through list elements - count = 0; - for (nums.items) |num| { - count += num; + /* Traverse list elements directly */ + for (num in nums) { + count += num } ``` -### 5.   Concatenating lists +=== "Ruby" -Given a new list `nums1`, we can append it to the end of the original list. + ```ruby title="list.rb" + # Traverse the list by index + count = 0 + for i in 0...nums.length + count += nums[i] + end + + # Traverse list elements directly + count = 0 + for num in nums + count += num + end + ``` + +??? pythontutor "Code Visualization" + +
+ + +### 5.   Concatenate Lists + +Given a new list `nums1`, we can concatenate it to the end of the original list. === "Python" ```python title="list.py" # Concatenate two lists nums1: list[int] = [6, 8, 7, 10, 9] - nums += nums1 # Concatenate nums1 to the end of nums + nums += nums1 # Concatenate list nums1 to the end of nums ``` === "C++" @@ -712,7 +762,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```cpp title="list.cpp" /* Concatenate two lists */ vector nums1 = { 6, 8, 7, 10, 9 }; - // Concatenate nums1 to the end of nums + // Concatenate list nums1 to the end of nums nums.insert(nums.end(), nums1.begin(), nums1.end()); ``` @@ -721,7 +771,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```java title="list.java" /* Concatenate two lists */ List nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 })); - nums.addAll(nums1); // Concatenate nums1 to the end of nums + nums.addAll(nums1); // Concatenate list nums1 to the end of nums ``` === "C#" @@ -729,7 +779,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```csharp title="list.cs" /* Concatenate two lists */ List nums1 = [6, 8, 7, 10, 9]; - nums.AddRange(nums1); // Concatenate nums1 to the end of nums + nums.AddRange(nums1); // Concatenate list nums1 to the end of nums ``` === "Go" @@ -737,7 +787,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```go title="list_test.go" /* Concatenate two lists */ nums1 := []int{6, 8, 7, 10, 9} - nums = append(nums, nums1...) // Concatenate nums1 to the end of nums + nums = append(nums, nums1...) // Concatenate list nums1 to the end of nums ``` === "Swift" @@ -745,7 +795,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```swift title="list.swift" /* Concatenate two lists */ let nums1 = [6, 8, 7, 10, 9] - nums.append(contentsOf: nums1) // Concatenate nums1 to the end of nums + nums.append(contentsOf: nums1) // Concatenate list nums1 to the end of nums ``` === "JS" @@ -753,7 +803,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```javascript title="list.js" /* Concatenate two lists */ const nums1 = [6, 8, 7, 10, 9]; - nums.push(...nums1); // Concatenate nums1 to the end of nums + nums.push(...nums1); // Concatenate list nums1 to the end of nums ``` === "TS" @@ -761,7 +811,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```typescript title="list.ts" /* Concatenate two lists */ const nums1: number[] = [6, 8, 7, 10, 9]; - nums.push(...nums1); // Concatenate nums1 to the end of nums + nums.push(...nums1); // Concatenate list nums1 to the end of nums ``` === "Dart" @@ -769,7 +819,7 @@ Given a new list `nums1`, we can append it to the end of the original list. ```dart title="list.dart" /* Concatenate two lists */ List nums1 = [6, 8, 7, 10, 9]; - nums.addAll(nums1); // Concatenate nums1 to the end of nums + nums.addAll(nums1); // Concatenate list nums1 to the end of nums ``` === "Rust" @@ -789,91 +839,96 @@ Given a new list `nums1`, we can append it to the end of the original list. === "Kotlin" ```kotlin title="list.kt" - + /* Concatenate two lists */ + val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList() + nums.addAll(nums1) // Concatenate list nums1 to the end of nums ``` -=== "Zig" +=== "Ruby" - ```zig title="list.zig" - // Concatenate two lists - var nums1 = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums1.deinit(); - try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); - try nums.insertSlice(nums.items.len, nums1.items); // Concatenate nums1 to the end of nums + ```ruby title="list.rb" + # Concatenate two lists + nums1 = [6, 8, 7, 10, 9] + nums += nums1 ``` -### 6.   Sorting the list +??? pythontutor "Code Visualization" -Once the list is sorted, we can employ algorithms commonly used in array-related algorithm problems, such as "binary search" and "two-pointer" algorithms. +
+ + +### 6.   Sort a List + +After sorting a list, we can use "binary search" and "two-pointer" algorithms, which are frequently tested in array algorithm problems. === "Python" ```python title="list.py" - # Sort the list - nums.sort() # After sorting, the list elements are in ascending order + # Sort a list + nums.sort() # After sorting, list elements are arranged from smallest to largest ``` === "C++" ```cpp title="list.cpp" - /* Sort the list */ - sort(nums.begin(), nums.end()); // After sorting, the list elements are in ascending order + /* Sort a list */ + sort(nums.begin(), nums.end()); // After sorting, list elements are arranged from smallest to largest ``` === "Java" ```java title="list.java" - /* Sort the list */ - Collections.sort(nums); // After sorting, the list elements are in ascending order + /* Sort a list */ + Collections.sort(nums); // After sorting, list elements are arranged from smallest to largest ``` === "C#" ```csharp title="list.cs" - /* Sort the list */ - nums.Sort(); // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.Sort(); // After sorting, list elements are arranged from smallest to largest ``` === "Go" ```go title="list_test.go" - /* Sort the list */ - sort.Ints(nums) // After sorting, the list elements are in ascending order + /* Sort a list */ + sort.Ints(nums) // After sorting, list elements are arranged from smallest to largest ``` === "Swift" ```swift title="list.swift" - /* Sort the list */ - nums.sort() // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.sort() // After sorting, list elements are arranged from smallest to largest ``` === "JS" ```javascript title="list.js" - /* Sort the list */ - nums.sort((a, b) => a - b); // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.sort((a, b) => a - b); // After sorting, list elements are arranged from smallest to largest ``` === "TS" ```typescript title="list.ts" - /* Sort the list */ - nums.sort((a, b) => a - b); // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.sort((a, b) => a - b); // After sorting, list elements are arranged from smallest to largest ``` === "Dart" ```dart title="list.dart" - /* Sort the list */ - nums.sort(); // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.sort(); // After sorting, list elements are arranged from smallest to largest ``` === "Rust" ```rust title="list.rs" - /* Sort the list */ - nums.sort(); // After sorting, the list elements are in ascending order + /* Sort a list */ + nums.sort(); // After sorting, list elements are arranged from smallest to largest ``` === "C" @@ -885,25 +940,31 @@ Once the list is sorted, we can employ algorithms commonly used in array-related === "Kotlin" ```kotlin title="list.kt" - + /* Sort a list */ + nums.sort() // After sorting, list elements are arranged from smallest to largest ``` -=== "Zig" +=== "Ruby" - ```zig title="list.zig" - // Sort the list - std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32)); + ```ruby title="list.rb" + # Sort a list + nums = nums.sort { |a, b| a <=> b } # After sorting, list elements are arranged from smallest to largest ``` -## 4.3.2   List implementation +??? pythontutor "Code Visualization" -Many programming languages come with built-in lists, including Java, C++, Python, etc. Their implementations tend to be intricate, featuring carefully considered settings for various parameters, like initial capacity and expansion factors. Readers who are curious can delve into the source code for further learning. +
+ -To enhance our understanding of how lists work, we will attempt to implement a simplified version of a list, focusing on three crucial design aspects: +## 4.3.2   List Implementation -- **Initial capacity**: Choose a reasonable initial capacity for the array. In this example, we choose 10 as the initial capacity. -- **Size recording**: Declare a variable `size` to record the current number of elements in the list, updating in real-time with element insertion and deletion. With this variable, we can locate the end of the list and determine whether expansion is needed. -- **Expansion mechanism**: If the list reaches full capacity upon an element insertion, an expansion process is required. This involves creating a larger array based on the expansion factor, and then transferring all elements from the current array to the new one. In this example, we stipulate that the array size should double with each expansion. +Many programming languages have built-in lists, such as Java, C++, and Python. Their implementations are quite complex, and the parameters are carefully considered, such as initial capacity, expansion multiples, and so on. Interested readers can consult the source code to learn more. + +To deepen our understanding of how lists work, we attempt to implement a simple list with three key design considerations: + +- **Initial capacity**: Select a reasonable initial capacity for the underlying array. In this example, we choose 10 as the initial capacity. +- **Size tracking**: Declare a variable `size` to record the current number of elements in the list and update it in real-time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed. +- **Expansion mechanism**: When the list capacity is full upon inserting an element, we need to expand. We create a larger array based on the expansion multiple and then move all elements from the current array to the new array in order. In this example, we specify that the array should be expanded to 2 times its previous size each time. === "Python" @@ -916,7 +977,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s self._capacity: int = 10 # List capacity self._arr: list[int] = [0] * self._capacity # Array (stores list elements) self._size: int = 0 # List length (current number of elements) - self._extend_ratio: int = 2 # Multiple for each list expansion + self._extend_ratio: int = 2 # Multiple by which the list capacity is extended each time def size(self) -> int: """Get list length (current number of elements)""" @@ -941,7 +1002,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s def add(self, num: int): """Add element at the end""" - # When the number of elements exceeds capacity, trigger the expansion mechanism + # When the number of elements exceeds capacity, trigger the extension mechanism if self.size() == self.capacity(): self.extend_capacity() self._arr[self._size] = num @@ -951,10 +1012,10 @@ To enhance our understanding of how lists work, we will attempt to implement a s """Insert element in the middle""" if index < 0 or index >= self._size: raise IndexError("Index out of bounds") - # When the number of elements exceeds capacity, trigger the expansion mechanism + # When the number of elements exceeds capacity, trigger the extension mechanism if self._size == self.capacity(): self.extend_capacity() - # Move all elements after `index` one position backward + # Move all elements at and after index index backward by one position for j in range(self._size - 1, index - 1, -1): self._arr[j + 1] = self._arr[j] self._arr[index] = num @@ -966,7 +1027,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s if index < 0 or index >= self._size: raise IndexError("Index out of bounds") num = self._arr[index] - # Move all elements after `index` one position forward + # Move all elements after index index forward by one position for j in range(index, self._size - 1): self._arr[j] = self._arr[j + 1] # Update the number of elements @@ -975,14 +1036,14 @@ To enhance our understanding of how lists work, we will attempt to implement a s return num def extend_capacity(self): - """Extend list""" - # Create a new array of _extend_ratio times the length of the original array and copy the original array to the new array + """Extend list capacity""" + # Create a new array with length _extend_ratio times the original array, and copy the original array to the new array self._arr = self._arr + [0] * self.capacity() * (self._extend_ratio - 1) # Update list capacity self._capacity = len(self._arr) def to_array(self) -> list[int]: - """Return a list of valid lengths""" + """Return list with valid length""" return self._arr[: self._size] ``` @@ -995,7 +1056,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s int *arr; // Array (stores list elements) int arrCapacity = 10; // List capacity int arrSize = 0; // List length (current number of elements) - int extendRatio = 2; // Multiple for each list expansion + int extendRatio = 2; // Multiple by which the list capacity is extended each time public: /* Constructor */ @@ -1018,7 +1079,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s return arrCapacity; } - /* Access element */ + /* Update element */ int get(int index) { // If the index is out of bounds, throw an exception, as below if (index < 0 || index >= size()) @@ -1026,16 +1087,16 @@ To enhance our understanding of how lists work, we will attempt to implement a s return arr[index]; } - /* Update element */ + /* Add elements at the end */ void set(int index, int num) { if (index < 0 || index >= size()) throw out_of_range("Index out of bounds"); arr[index] = num; } - /* Add element at the end */ + /* Direct traversal of list elements */ void add(int num) { - // When the number of elements exceeds capacity, trigger the expansion mechanism + // When the number of elements exceeds capacity, trigger the extension mechanism if (size() == capacity()) extendCapacity(); arr[size()] = num; @@ -1043,14 +1104,14 @@ To enhance our understanding of how lists work, we will attempt to implement a s arrSize++; } - /* Insert element in the middle */ + /* Sort list */ void insert(int index, int num) { if (index < 0 || index >= size()) throw out_of_range("Index out of bounds"); - // When the number of elements exceeds capacity, trigger the expansion mechanism + // When the number of elements exceeds capacity, trigger the extension mechanism if (size() == capacity()) extendCapacity(); - // Move all elements after `index` one position backward + // Move all elements after index index forward by one position for (int j = size() - 1; j >= index; j--) { arr[j + 1] = arr[j]; } @@ -1064,7 +1125,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s if (index < 0 || index >= size()) throw out_of_range("Index out of bounds"); int num = arr[index]; - // Move all elements after `index` one position forward + // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array for (int j = index; j < size() - 1; j++) { arr[j] = arr[j + 1]; } @@ -1074,9 +1135,9 @@ To enhance our understanding of how lists work, we will attempt to implement a s return num; } - /* Extend list */ + /* Driver Code */ void extendCapacity() { - // Create a new array with a length multiple of the original array by extendRatio + // Create a new array with length extendRatio times the original array int newCapacity = capacity() * extendRatio; int *tmp = arr; arr = new int[newCapacity]; @@ -1089,9 +1150,9 @@ To enhance our understanding of how lists work, we will attempt to implement a s arrCapacity = newCapacity; } - /* Convert the list to a Vector for printing */ + /* Convert list to Vector for printing */ vector toVector() { - // Only convert elements within valid length range + // Elements enqueue vector vec(size()); for (int i = 0; i < size(); i++) { vec[i] = arr[i]; @@ -1109,7 +1170,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s private int[] arr; // Array (stores list elements) private int capacity = 10; // List capacity private int size = 0; // List length (current number of elements) - private int extendRatio = 2; // Multiple for each list expansion + private int extendRatio = 2; // Multiple by which the list capacity is extended each time /* Constructor */ public MyList() { @@ -1126,7 +1187,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s return capacity; } - /* Access element */ + /* Update element */ public int get(int index) { // If the index is out of bounds, throw an exception, as below if (index < 0 || index >= size) @@ -1134,16 +1195,16 @@ To enhance our understanding of how lists work, we will attempt to implement a s return arr[index]; } - /* Update element */ + /* Add elements at the end */ public void set(int index, int num) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index out of bounds"); arr[index] = num; } - /* Add element at the end */ + /* Direct traversal of list elements */ public void add(int num) { - // When the number of elements exceeds capacity, trigger the expansion mechanism + // When the number of elements exceeds capacity, trigger the extension mechanism if (size == capacity()) extendCapacity(); arr[size] = num; @@ -1151,14 +1212,14 @@ To enhance our understanding of how lists work, we will attempt to implement a s size++; } - /* Insert element in the middle */ + /* Sort list */ public void insert(int index, int num) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index out of bounds"); - // When the number of elements exceeds capacity, trigger the expansion mechanism + // When the number of elements exceeds capacity, trigger the extension mechanism if (size == capacity()) extendCapacity(); - // Move all elements after `index` one position backward + // Move all elements after index index forward by one position for (int j = size - 1; j >= index; j--) { arr[j + 1] = arr[j]; } @@ -1172,7 +1233,7 @@ To enhance our understanding of how lists work, we will attempt to implement a s if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index out of bounds"); int num = arr[index]; - // Move all elements after `index` one position forward + // Move all elements after index forward by one position for (int j = index; j < size - 1; j++) { arr[j] = arr[j + 1]; } @@ -1182,18 +1243,18 @@ To enhance our understanding of how lists work, we will attempt to implement a s return num; } - /* Extend list */ + /* Driver Code */ public void extendCapacity() { - // Create a new array with a length multiple of the original array by extendRatio, and copy the original array to the new array + // Create a new array with length extendRatio times the original array and copy the original array to the new array arr = Arrays.copyOf(arr, capacity() * extendRatio); - // Update list capacity + // Add elements at the end capacity = arr.length; } - /* Convert the list to an array */ + /* Convert list to array */ public int[] toArray() { int size = size(); - // Only convert elements within valid length range + // Elements enqueue int[] arr = new int[size]; for (int i = 0; i < size; i++) { arr[i] = get(i); @@ -1206,65 +1267,1030 @@ To enhance our understanding of how lists work, we will attempt to implement a s === "C#" ```csharp title="my_list.cs" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + private int[] arr; // Array (stores list elements) + private int arrCapacity = 10; // List capacity + private int arrSize = 0; // List length (current number of elements) + private readonly int extendRatio = 2; // Multiple by which the list capacity is extended each time + + /* Constructor */ + public MyList() { + arr = new int[arrCapacity]; + } + + /* Get list length (current number of elements) */ + public int Size() { + return arrSize; + } + + /* Get list capacity */ + public int Capacity() { + return arrCapacity; + } + + /* Update element */ + public int Get(int index) { + // If the index is out of bounds, throw an exception, as below + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("Index out of bounds"); + return arr[index]; + } + + /* Add elements at the end */ + public void Set(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("Index out of bounds"); + arr[index] = num; + } + + /* Direct traversal of list elements */ + public void Add(int num) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if (arrSize == arrCapacity) + ExtendCapacity(); + arr[arrSize] = num; + // Update the number of elements + arrSize++; + } + + /* Sort list */ + public void Insert(int index, int num) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("Index out of bounds"); + // When the number of elements exceeds capacity, trigger the extension mechanism + if (arrSize == arrCapacity) + ExtendCapacity(); + // Move all elements after index index forward by one position + for (int j = arrSize - 1; j >= index; j--) { + arr[j + 1] = arr[j]; + } + arr[index] = num; + // Update the number of elements + arrSize++; + } + + /* Remove element */ + public int Remove(int index) { + if (index < 0 || index >= arrSize) + throw new IndexOutOfRangeException("Index out of bounds"); + int num = arr[index]; + // Move all elements after index forward by one position + for (int j = index; j < arrSize - 1; j++) { + arr[j] = arr[j + 1]; + } + // Update the number of elements + arrSize--; + // Return the removed element + return num; + } + + /* Driver Code */ + public void ExtendCapacity() { + // Create new array of length arrCapacity * extendRatio and copy original array to new array + Array.Resize(ref arr, arrCapacity * extendRatio); + // Add elements at the end + arrCapacity = arr.Length; + } + + /* Convert list to array */ + public int[] ToArray() { + // Elements enqueue + int[] arr = new int[arrSize]; + for (int i = 0; i < arrSize; i++) { + arr[i] = Get(i); + } + return arr; + } + } ``` === "Go" ```go title="my_list.go" - [class]{myList}-[func]{} + /* List class */ + type myList struct { + arrCapacity int + arr []int + arrSize int + extendRatio int + } + + /* Constructor */ + func newMyList() *myList { + return &myList{ + arrCapacity: 10, // List capacity + arr: make([]int, 10), // Array (stores list elements) + arrSize: 0, // List length (current number of elements) + extendRatio: 2, // Multiple by which the list capacity is extended each time + } + } + + /* Get list length (current number of elements) */ + func (l *myList) size() int { + return l.arrSize + } + + /* Get list capacity */ + func (l *myList) capacity() int { + return l.arrCapacity + } + + /* Update element */ + func (l *myList) get(index int) int { + // If the index is out of bounds, throw an exception, as below + if index < 0 || index >= l.arrSize { + panic("Index out of bounds") + } + return l.arr[index] + } + + /* Add elements at the end */ + func (l *myList) set(num, index int) { + if index < 0 || index >= l.arrSize { + panic("Index out of bounds") + } + l.arr[index] = num + } + + /* Direct traversal of list elements */ + func (l *myList) add(num int) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + l.arr[l.arrSize] = num + // Update the number of elements + l.arrSize++ + } + + /* Sort list */ + func (l *myList) insert(num, index int) { + if index < 0 || index >= l.arrSize { + panic("Index out of bounds") + } + // When the number of elements exceeds capacity, trigger the extension mechanism + if l.arrSize == l.arrCapacity { + l.extendCapacity() + } + // Move all elements after index index forward by one position + for j := l.arrSize - 1; j >= index; j-- { + l.arr[j+1] = l.arr[j] + } + l.arr[index] = num + // Update the number of elements + l.arrSize++ + } + + /* Remove element */ + func (l *myList) remove(index int) int { + if index < 0 || index >= l.arrSize { + panic("Index out of bounds") + } + num := l.arr[index] + // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array + for j := index; j < l.arrSize-1; j++ { + l.arr[j] = l.arr[j+1] + } + // Update the number of elements + l.arrSize-- + // Return the removed element + return num + } + + /* Driver Code */ + func (l *myList) extendCapacity() { + // Create a new array with length extendRatio times the original array and copy the original array to the new array + l.arr = append(l.arr, make([]int, l.arrCapacity*(l.extendRatio-1))...) + // Add elements at the end + l.arrCapacity = len(l.arr) + } + + /* Return list with valid length */ + func (l *myList) toArray() []int { + // Elements enqueue + return l.arr[:l.arrSize] + } ``` === "Swift" ```swift title="my_list.swift" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + private var arr: [Int] // Array (stores list elements) + private var _capacity: Int // List capacity + private var _size: Int // List length (current number of elements) + private let extendRatio: Int // Multiple by which the list capacity is extended each time + + /* Constructor */ + init() { + _capacity = 10 + _size = 0 + extendRatio = 2 + arr = Array(repeating: 0, count: _capacity) + } + + /* Get list length (current number of elements) */ + func size() -> Int { + _size + } + + /* Get list capacity */ + func capacity() -> Int { + _capacity + } + + /* Update element */ + func get(index: Int) -> Int { + // Throw error if index out of bounds, same below + if index < 0 || index >= size() { + fatalError("Index out of bounds") + } + return arr[index] + } + + /* Add elements at the end */ + func set(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("Index out of bounds") + } + arr[index] = num + } + + /* Direct traversal of list elements */ + func add(num: Int) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if size() == capacity() { + extendCapacity() + } + arr[size()] = num + // Update the number of elements + _size += 1 + } + + /* Sort list */ + func insert(index: Int, num: Int) { + if index < 0 || index >= size() { + fatalError("Index out of bounds") + } + // When the number of elements exceeds capacity, trigger the extension mechanism + if size() == capacity() { + extendCapacity() + } + // Move all elements after index index forward by one position + for j in (index ..< size()).reversed() { + arr[j + 1] = arr[j] + } + arr[index] = num + // Update the number of elements + _size += 1 + } + + /* Remove element */ + @discardableResult + func remove(index: Int) -> Int { + if index < 0 || index >= size() { + fatalError("Index out of bounds") + } + let num = arr[index] + // Move all elements after index forward by one position + for j in index ..< (size() - 1) { + arr[j] = arr[j + 1] + } + // Update the number of elements + _size -= 1 + // Return the removed element + return num + } + + /* Driver Code */ + func extendCapacity() { + // Create a new array with length extendRatio times the original array and copy the original array to the new array + arr = arr + Array(repeating: 0, count: capacity() * (extendRatio - 1)) + // Add elements at the end + _capacity = arr.count + } + + /* Convert list to array */ + func toArray() -> [Int] { + Array(arr.prefix(size())) + } + } ``` === "JS" ```javascript title="my_list.js" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + #arr = new Array(); // Array (stores list elements) + #capacity = 10; // List capacity + #size = 0; // List length (current number of elements) + #extendRatio = 2; // Multiple by which the list capacity is extended each time + + /* Constructor */ + constructor() { + this.#arr = new Array(this.#capacity); + } + + /* Get list length (current number of elements) */ + size() { + return this.#size; + } + + /* Get list capacity */ + capacity() { + return this.#capacity; + } + + /* Update element */ + get(index) { + // If the index is out of bounds, throw an exception, as below + if (index < 0 || index >= this.#size) throw new Error('Index out of bounds'); + return this.#arr[index]; + } + + /* Add elements at the end */ + set(index, num) { + if (index < 0 || index >= this.#size) throw new Error('Index out of bounds'); + this.#arr[index] = num; + } + + /* Direct traversal of list elements */ + add(num) { + // If length equals capacity, need to expand + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // Add new element to end of list + this.#arr[this.#size] = num; + this.#size++; + } + + /* Sort list */ + insert(index, num) { + if (index < 0 || index >= this.#size) throw new Error('Index out of bounds'); + // When the number of elements exceeds capacity, trigger the extension mechanism + if (this.#size === this.#capacity) { + this.extendCapacity(); + } + // Move all elements after index index forward by one position + for (let j = this.#size - 1; j >= index; j--) { + this.#arr[j + 1] = this.#arr[j]; + } + // Update the number of elements + this.#arr[index] = num; + this.#size++; + } + + /* Remove element */ + remove(index) { + if (index < 0 || index >= this.#size) throw new Error('Index out of bounds'); + let num = this.#arr[index]; + // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array + for (let j = index; j < this.#size - 1; j++) { + this.#arr[j] = this.#arr[j + 1]; + } + // Update the number of elements + this.#size--; + // Return the removed element + return num; + } + + /* Driver Code */ + extendCapacity() { + // Create a new array with length extendRatio times the original array and copy the original array to the new array + this.#arr = this.#arr.concat( + new Array(this.capacity() * (this.#extendRatio - 1)) + ); + // Add elements at the end + this.#capacity = this.#arr.length; + } + + /* Convert list to array */ + toArray() { + let size = this.size(); + // Elements enqueue + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } ``` === "TS" ```typescript title="my_list.ts" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + private arr: Array; // Array (stores list elements) + private _capacity: number = 10; // List capacity + private _size: number = 0; // List length (current number of elements) + private extendRatio: number = 2; // Multiple by which the list capacity is extended each time + + /* Constructor */ + constructor() { + this.arr = new Array(this._capacity); + } + + /* Get list length (current number of elements) */ + public size(): number { + return this._size; + } + + /* Get list capacity */ + public capacity(): number { + return this._capacity; + } + + /* Update element */ + public get(index: number): number { + // If the index is out of bounds, throw an exception, as below + if (index < 0 || index >= this._size) throw new Error('Index out of bounds'); + return this.arr[index]; + } + + /* Add elements at the end */ + public set(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('Index out of bounds'); + this.arr[index] = num; + } + + /* Direct traversal of list elements */ + public add(num: number): void { + // If length equals capacity, need to expand + if (this._size === this._capacity) this.extendCapacity(); + // Add new element to end of list + this.arr[this._size] = num; + this._size++; + } + + /* Sort list */ + public insert(index: number, num: number): void { + if (index < 0 || index >= this._size) throw new Error('Index out of bounds'); + // When the number of elements exceeds capacity, trigger the extension mechanism + if (this._size === this._capacity) { + this.extendCapacity(); + } + // Move all elements after index index forward by one position + for (let j = this._size - 1; j >= index; j--) { + this.arr[j + 1] = this.arr[j]; + } + // Update the number of elements + this.arr[index] = num; + this._size++; + } + + /* Remove element */ + public remove(index: number): number { + if (index < 0 || index >= this._size) throw new Error('Index out of bounds'); + let num = this.arr[index]; + // Move all elements after index forward by one position + for (let j = index; j < this._size - 1; j++) { + this.arr[j] = this.arr[j + 1]; + } + // Update the number of elements + this._size--; + // Return the removed element + return num; + } + + /* Driver Code */ + public extendCapacity(): void { + // Create new array of length size and copy original array to new array + this.arr = this.arr.concat( + new Array(this.capacity() * (this.extendRatio - 1)) + ); + // Add elements at the end + this._capacity = this.arr.length; + } + + /* Convert list to array */ + public toArray(): number[] { + let size = this.size(); + // Elements enqueue + const arr = new Array(size); + for (let i = 0; i < size; i++) { + arr[i] = this.get(i); + } + return arr; + } + } ``` === "Dart" ```dart title="my_list.dart" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + late List _arr; // Array (stores list elements) + int _capacity = 10; // List capacity + int _size = 0; // List length (current number of elements) + int _extendRatio = 2; // Multiple by which the list capacity is extended each time + + /* Constructor */ + MyList() { + _arr = List.filled(_capacity, 0); + } + + /* Get list length (current number of elements) */ + int size() => _size; + + /* Get list capacity */ + int capacity() => _capacity; + + /* Update element */ + int get(int index) { + if (index >= _size) throw RangeError('Index out of bounds'); + return _arr[index]; + } + + /* Add elements at the end */ + void set(int index, int _num) { + if (index >= _size) throw RangeError('Index out of bounds'); + _arr[index] = _num; + } + + /* Direct traversal of list elements */ + void add(int _num) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if (_size == _capacity) extendCapacity(); + _arr[_size] = _num; + // Update the number of elements + _size++; + } + + /* Sort list */ + void insert(int index, int _num) { + if (index >= _size) throw RangeError('Index out of bounds'); + // When the number of elements exceeds capacity, trigger the extension mechanism + if (_size == _capacity) extendCapacity(); + // Move all elements after index index forward by one position + for (var j = _size - 1; j >= index; j--) { + _arr[j + 1] = _arr[j]; + } + _arr[index] = _num; + // Update the number of elements + _size++; + } + + /* Remove element */ + int remove(int index) { + if (index >= _size) throw RangeError('Index out of bounds'); + int _num = _arr[index]; + // Move all elements after index forward by one position + for (var j = index; j < _size - 1; j++) { + _arr[j] = _arr[j + 1]; + } + // Update the number of elements + _size--; + // Return the removed element + return _num; + } + + /* Driver Code */ + void extendCapacity() { + // Create new array with length _extendRatio times original array + final _newNums = List.filled(_capacity * _extendRatio, 0); + // Copy original array to new array + List.copyRange(_newNums, 0, _arr); + // Update _arr reference + _arr = _newNums; + // Add elements at the end + _capacity = _arr.length; + } + + /* Convert list to array */ + List toArray() { + List arr = []; + for (var i = 0; i < _size; i++) { + arr.add(get(i)); + } + return arr; + } + } ``` === "Rust" ```rust title="my_list.rs" - [class]{MyList}-[func]{} + /* List class */ + #[allow(dead_code)] + struct MyList { + arr: Vec, // Array (stores list elements) + capacity: usize, // List capacity + size: usize, // List length (current number of elements) + extend_ratio: usize, // Multiple by which the list capacity is extended each time + } + + #[allow(unused, unused_comparisons)] + impl MyList { + /* Constructor */ + pub fn new(capacity: usize) -> Self { + let mut vec = vec![0; capacity]; + Self { + arr: vec, + capacity, + size: 0, + extend_ratio: 2, + } + } + + /* Get list length (current number of elements) */ + pub fn size(&self) -> usize { + return self.size; + } + + /* Get list capacity */ + pub fn capacity(&self) -> usize { + return self.capacity; + } + + /* Update element */ + pub fn get(&self, index: usize) -> i32 { + // If the index is out of bounds, throw an exception, as below + if index >= self.size { + panic!("Index out of bounds") + }; + return self.arr[index]; + } + + /* Add elements at the end */ + pub fn set(&mut self, index: usize, num: i32) { + if index >= self.size { + panic!("Index out of bounds") + }; + self.arr[index] = num; + } + + /* Direct traversal of list elements */ + pub fn add(&mut self, num: i32) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if self.size == self.capacity() { + self.extend_capacity(); + } + self.arr[self.size] = num; + // Update the number of elements + self.size += 1; + } + + /* Sort list */ + pub fn insert(&mut self, index: usize, num: i32) { + if index >= self.size() { + panic!("Index out of bounds") + }; + // When the number of elements exceeds capacity, trigger the extension mechanism + if self.size == self.capacity() { + self.extend_capacity(); + } + // Move all elements after index index forward by one position + for j in (index..self.size).rev() { + self.arr[j + 1] = self.arr[j]; + } + self.arr[index] = num; + // Update the number of elements + self.size += 1; + } + + /* Remove element */ + pub fn remove(&mut self, index: usize) -> i32 { + if index >= self.size() { + panic!("Index out of bounds") + }; + let num = self.arr[index]; + // Create a new array with length _extend_ratio times the original array, and copy the original array to the new array + for j in index..self.size - 1 { + self.arr[j] = self.arr[j + 1]; + } + // Update the number of elements + self.size -= 1; + // Return the removed element + return num; + } + + /* Driver Code */ + pub fn extend_capacity(&mut self) { + // Create new array with length extend_ratio times original, copy original array to new array + let new_capacity = self.capacity * self.extend_ratio; + self.arr.resize(new_capacity, 0); + // Add elements at the end + self.capacity = new_capacity; + } + + /* Convert list to array */ + pub fn to_array(&self) -> Vec { + // Elements enqueue + let mut arr = Vec::new(); + for i in 0..self.size { + arr.push(self.get(i)); + } + arr + } + } ``` === "C" ```c title="my_list.c" - [class]{MyList}-[func]{} + /* List class */ + typedef struct { + int *arr; // Array (stores list elements) + int capacity; // List capacity + int size; // List size + int extendRatio; // List expansion multiplier + } MyList; + + /* Constructor */ + MyList *newMyList() { + MyList *nums = malloc(sizeof(MyList)); + nums->capacity = 10; + nums->arr = malloc(sizeof(int) * nums->capacity); + nums->size = 0; + nums->extendRatio = 2; + return nums; + } + + /* Destructor */ + void delMyList(MyList *nums) { + free(nums->arr); + free(nums); + } + + /* Get list length */ + int size(MyList *nums) { + return nums->size; + } + + /* Get list capacity */ + int capacity(MyList *nums) { + return nums->capacity; + } + + /* Update element */ + int get(MyList *nums, int index) { + assert(index >= 0 && index < nums->size); + return nums->arr[index]; + } + + /* Add elements at the end */ + void set(MyList *nums, int index, int num) { + assert(index >= 0 && index < nums->size); + nums->arr[index] = num; + } + + /* Direct traversal of list elements */ + void add(MyList *nums, int num) { + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // Expand capacity + } + nums->arr[size(nums)] = num; + nums->size++; + } + + /* Sort list */ + void insert(MyList *nums, int index, int num) { + assert(index >= 0 && index < size(nums)); + // When the number of elements exceeds capacity, trigger the extension mechanism + if (size(nums) == capacity(nums)) { + extendCapacity(nums); // Expand capacity + } + for (int i = size(nums); i > index; --i) { + nums->arr[i] = nums->arr[i - 1]; + } + nums->arr[index] = num; + nums->size++; + } + + /* Remove element */ + // Note: stdio.h occupies the remove keyword + int removeItem(MyList *nums, int index) { + assert(index >= 0 && index < size(nums)); + int num = nums->arr[index]; + for (int i = index; i < size(nums) - 1; i++) { + nums->arr[i] = nums->arr[i + 1]; + } + nums->size--; + return num; + } + + /* Driver Code */ + void extendCapacity(MyList *nums) { + // Allocate space first + int newCapacity = capacity(nums) * nums->extendRatio; + int *extend = (int *)malloc(sizeof(int) * newCapacity); + int *temp = nums->arr; + + // Copy old data to new data + for (int i = 0; i < size(nums); i++) + extend[i] = nums->arr[i]; + + // Free old data + free(temp); + + // Update new data + nums->arr = extend; + nums->capacity = newCapacity; + } + + /* Convert list to Array for printing */ + int *toArray(MyList *nums) { + return nums->arr; + } ``` === "Kotlin" ```kotlin title="my_list.kt" - [class]{MyList}-[func]{} + /* List class */ + class MyList { + private var arr: IntArray = intArrayOf() // Array (stores list elements) + private var capacity: Int = 10 // List capacity + private var size: Int = 0 // List length (current number of elements) + private var extendRatio: Int = 2 // Multiple by which the list capacity is extended each time + + /* Constructor */ + init { + arr = IntArray(capacity) + } + + /* Get list length (current number of elements) */ + fun size(): Int { + return size + } + + /* Get list capacity */ + fun capacity(): Int { + return capacity + } + + /* Update element */ + fun get(index: Int): Int { + // If the index is out of bounds, throw an exception, as below + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("Index out of bounds") + return arr[index] + } + + /* Add elements at the end */ + fun set(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("Index out of bounds") + arr[index] = num + } + + /* Direct traversal of list elements */ + fun add(num: Int) { + // When the number of elements exceeds capacity, trigger the extension mechanism + if (size == capacity()) + extendCapacity() + arr[size] = num + // Update the number of elements + size++ + } + + /* Sort list */ + fun insert(index: Int, num: Int) { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("Index out of bounds") + // When the number of elements exceeds capacity, trigger the extension mechanism + if (size == capacity()) + extendCapacity() + // Move all elements after index index forward by one position + for (j in size - 1 downTo index) + arr[j + 1] = arr[j] + arr[index] = num + // Update the number of elements + size++ + } + + /* Remove element */ + fun remove(index: Int): Int { + if (index < 0 || index >= size) + throw IndexOutOfBoundsException("Index out of bounds") + val num = arr[index] + // Move all elements after index forward by one position + for (j in index..= size + @arr[index] + end + + ### Access element ### + def set(index, num) + raise IndexError, "Index out of bounds" if index < 0 || index >= size + @arr[index] = num + end + + ### Add element at end ### + def add(num) + # When the number of elements exceeds capacity, trigger the extension mechanism + extend_capacity if size == capacity + @arr[size] = num + + # Update the number of elements + @size += 1 + end + + ### Insert element in middle ### + def insert(index, num) + raise IndexError, "Index out of bounds" if index < 0 || index >= size + + # When the number of elements exceeds capacity, trigger the extension mechanism + extend_capacity if size == capacity + + # Move all elements after index index forward by one position + for j in (size - 1).downto(index) + @arr[j + 1] = @arr[j] + end + @arr[index] = num + + # Update the number of elements + @size += 1 + end + + ### Delete element ### + def remove(index) + raise IndexError, "Index out of bounds" if index < 0 || index >= size + num = @arr[index] + + # Move all elements after index forward by one position + for j in index...size + @arr[j] = @arr[j + 1] + end + + # Update the number of elements + @size -= 1 + + # Return the removed element + num + end + + ### Expand list capacity ### + def extend_capacity + # Create new array with length extend_ratio times original, copy original array to new array + arr = @arr.dup + Array.new(capacity * (@extend_ratio - 1)) + # Add elements at the end + @capacity = arr.length + end + + ### Convert list to array ### + def to_array + sz = size + # Elements enqueue + arr = Array.new(sz) + for i in 0...sz + arr[i] = get(i) + end + arr + end + end ``` diff --git a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md index 031a451a0..50be50078 100644 --- a/en/docs/chapter_array_and_linkedlist/ram_and_cache.md +++ b/en/docs/chapter_array_and_linkedlist/ram_and_cache.md @@ -2,82 +2,82 @@ comments: true --- -# 4.4   Memory and cache * +# 4.4   Random-Access Memory and Cache * -In the first two sections of this chapter, we explored arrays and linked lists, two fundamental data structures that represent "continuous storage" and "dispersed storage," respectively. +In the first two sections of this chapter, we explored arrays and linked lists, two fundamental and important data structures that represent "contiguous storage" and "distributed storage" as two physical structures, respectively. -In fact, **the physical structure largely determines how efficiently a program utilizes memory and cache**, which in turn affects the overall performance of the algorithm. +In fact, **physical structure largely determines the efficiency with which programs utilize memory and cache**, which in turn affects the overall performance of algorithmic programs. -## 4.4.1   Computer storage devices +## 4.4.1   Computer Storage Devices -There are three types of storage devices in computers: hard disk, random-access memory (RAM), and cache memory. The following table shows their respective roles and performance characteristics in computer systems. +Computers include three types of storage devices: hard disk, random-access memory (RAM), and cache memory. The following table shows their different roles and performance characteristics in a computer system. -

Table 4-2   Computer storage devices

+

Table 4-2   Computer Storage Devices

-| | Hard Disk | Memory | Cache | -| ----------- | -------------------------------------------------------------- | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | -| Usage | Long-term storage of data, including OS, programs, files, etc. | Temporary storage of currently running programs and data being processed | Stores frequently accessed data and instructions, reducing the number of CPU accesses to memory | -| Volatility | Data is not lost after power off | Data is lost after power off | Data is lost after power off | -| Capacity | Larger, TB level | Smaller, GB level | Very small, MB level | -| Speed | Slower, several hundred to thousands MB/s | Faster, several tens of GB/s | Very fast, several tens to hundreds of GB/s | -| Price (USD) | Cheaper, a few cents / GB | More expensive, a few dollars / GB | Very expensive, priced with CPU | +| | Hard Disk | RAM | Cache | +| -------------- | ------------------------------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------- | +| Purpose | Long-term storage of data, including operating systems, programs, and files | Temporary storage of currently running programs and data being processed | Storage of frequently accessed data and instructions to reduce CPU's accesses to memory | +| Volatility | Data is not lost after power-off | Data is lost after power-off | Data is lost after power-off | +| Capacity | Large, on the order of terabytes (TB) | Small, on the order of gigabytes (GB) | Very small, on the order of megabytes (MB) | +| Speed | Slow, hundreds to thousands of MB/s | Fast, tens of GB/s | Very fast, tens to hundreds of GB/s | +| Cost (USD/GB) | Inexpensive, fractions of a dollar to a few dollars per GB | Expensive, tens to hundreds of dollars per GB | Very expensive, priced as part of the CPU package |
-The computer storage system can be visualized as a pyramid, as shown in Figure 4-9. The storage devices at the top of the pyramid are faster, have smaller capacities, and are more expensive. This multi-level design is not accidental, but a deliberate outcome of careful consideration by computer scientists and engineers. +We can imagine the computer storage system as a pyramid structure as shown in the diagram below. Storage devices closer to the top of the pyramid are faster, have smaller capacity, and are more expensive. This multi-layered design is not by accident, but rather the result of careful consideration by computer scientists and engineers. -- **Replacing hard disks with memory is challenging**. Firstly, data in memory is lost after power off, making it unsuitable for long-term data storage; secondly, memory is significantly more expensive than hard disks, limiting its feasibility for widespread use in the consumer market. -- **Caches face a trade-off between large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size grows, increasing the distance from the CPU core. This results in longer data transfer times and higher access latency. With current technology, a multi-level cache structure provides the optimal balance between capacity, speed, and cost. +- **Hard disk cannot be easily replaced by RAM**. First, data in memory is lost after power-off, making it unsuitable for long-term data storage. Second, memory is tens of times more expensive than hard disk, which makes it difficult to popularize in the consumer market. +- **Cache cannot simultaneously achieve large capacity and high speed**. As the capacity of L1, L2, and L3 caches increases, their physical size becomes larger, and the physical distance between them and the CPU core increases, resulting in longer data transmission time and higher element access latency. With current technology, the multi-layered cache structure represents the best balance point between capacity, speed, and cost. -![Computer storage system](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } +![Computer Storage System](ram_and_cache.assets/storage_pyramid.png){ class="animation-figure" } -

Figure 4-9   Computer storage system

+

Figure 4-9   Computer Storage System

!!! tip - The storage hierarchy in computers reflects a careful balance between speed, capacity, and cost. This type of trade-off is common across various industries, where finding the optimal balance between benefits and limitations is essential. + The storage hierarchy of computers embodies a delicate balance among speed, capacity, and cost. In fact, such trade-offs are common across all industrial fields, requiring us to find the optimal balance point between different advantages and constraints. -Overall, **hard disks provide long-term storage for large volumes of data, memory serves as temporary storage for data being processed during program execution, and cache stores frequently accessed data and instructions to enhance execution efficiency**. Together, they ensure the efficient operation of computer systems. +In summary, **hard disk is used for long-term storage of large amounts of data, RAM is used for temporary storage of data being processed during program execution, and cache is used for storage of frequently accessed data and instructions**, to improve program execution efficiency. The three work together to ensure efficient operation of the computer system. -As shown in Figure 4-10, during program execution, data is read from the hard disk into memory for CPU computation. The cache, acting as an extension of the CPU, **intelligently preloads data from memory**, enabling faster data access for the CPU. This greatly improves program execution efficiency while reducing reliance on slower memory. +As shown in the diagram below, during program execution, data is read from the hard disk into RAM for CPU computation. Cache can be viewed as part of the CPU, **it intelligently loads data from RAM**, providing the CPU with high-speed data reading, thereby significantly improving program execution efficiency and reducing reliance on slower RAM. -![Data flow between hard disk, memory, and cache](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } +![Data Flow Among Hard Disk, RAM, and Cache](ram_and_cache.assets/computer_storage_devices.png){ class="animation-figure" } -

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

+

Figure 4-10   Data Flow Among Hard Disk, RAM, and Cache

-## 4.4.2   Memory efficiency of data structures +## 4.4.2   Memory Efficiency of Data Structures -In terms of memory space utilization, arrays and linked lists have their advantages and limitations. +In terms of memory space utilization, arrays and linked lists each have advantages and limitations. -On one hand, **memory is limited and cannot be shared by multiple programs**, so optimizing space usage in data structures is crucial. Arrays are space-efficient because their elements are tightly packed, without requiring extra memory for references (pointers) as in linked lists. However, arrays require pre-allocating a contiguous block of memory, which can lead to waste if the allocated space exceeds the actual need. Expanding an array also incurs additional time and space overhead. In contrast, linked lists allocate and free memory dynamically for each node, offering greater flexibility at the cost of additional memory for pointers. +On one hand, **memory is limited, and the same memory cannot be shared by multiple programs**, so we hope data structures can utilize space as efficiently as possible. Array elements are tightly packed and do not require additional space to store references (pointers) between linked list nodes, thus having higher space efficiency. However, arrays need to allocate sufficient contiguous memory space at once, which may lead to memory waste, and array expansion requires additional time and space costs. In comparison, linked lists perform dynamic memory allocation and deallocation on a "node" basis, providing greater flexibility. -On the other hand, during program execution, **repeated memory allocation and deallocation increase memory fragmentation**, reducing memory utilization efficiency. Arrays, due to their continuous storage method, are relatively less likely to cause memory fragmentation. In contrast, linked lists store elements in non-contiguous locations, and frequent insertions and deletions can exacerbate memory fragmentation. +On the other hand, during program execution, **as memory is repeatedly allocated and freed, the degree of fragmentation of free memory becomes increasingly severe**, leading to reduced memory utilization efficiency. Arrays, due to their contiguous storage approach, are relatively less prone to memory fragmentation. Conversely, linked list elements are distributed in storage, and frequent insertion and deletion operations are more likely to cause memory fragmentation. -## 4.4.3   Cache efficiency of data structures +## 4.4.3   Cache Efficiency of Data Structures -Although caches are much smaller in space capacity than memory, they are much faster and play a crucial role in program execution speed. Due to their limited capacity, caches can only store a subset of frequently accessed data. When the CPU attempts to access data not present in the cache, a cache miss occurs, requiring the CPU to retrieve the needed data from slower memory, which can impact performance. +Although cache has much smaller space capacity than memory, it is much faster than memory and plays a crucial role in program execution speed. Since cache capacity is limited and can only store a small portion of frequently accessed data, when the CPU attempts to access data that is not in the cache, a cache miss occurs, and the CPU must load the required data from the slower memory. -Clearly, **the fewer the cache misses, the higher the CPU's data read-write efficiency**, and the better the program performance. The proportion of successful data retrieval from the cache by the CPU is called the cache hit rate, a metric often used to measure cache efficiency. +Clearly, **the fewer "cache misses," the higher the efficiency of CPU data reads and writes**, and the better the program performance. We call the proportion of data that the CPU successfully obtains from the cache the cache hit rate, a metric typically used to measure cache efficiency. -To achieve higher efficiency, caches adopt the following data loading mechanisms. +To achieve the highest efficiency possible, cache employs the following data loading mechanisms. -- **Cache lines**: Caches operate by storing and loading data in units called cache lines, rather than individual bytes. This approach improves efficiency by transferring larger blocks of data at once. -- **Prefetch mechanism**: Processors predict data access patterns (e.g., sequential or fixed-stride access) and preload data into the cache based on these patterns to increase the cache hit rate. -- **Spatial locality**: When a specific piece of data is accessed, nearby data is likely to be accessed soon. To leverage this, caches load adjacent data along with the requested data, improving hit rates. -- **Temporal locality**: If data is accessed, it's likely to be accessed again in the near future. Caches use this principle to retain recently accessed data to improve the hit rate. +- **Cache lines**: The cache does not store and load data on a byte-by-byte basis, but rather as cache lines. Compared to byte-by-byte transmission, cache line transmission is more efficient. +- **Prefetching mechanism**: The processor attempts to predict data access patterns (e.g., sequential access, fixed-stride jumping access, etc.) and loads data into the cache according to specific patterns, thereby improving hit rate. +- **Spatial locality**: If a piece of data is accessed, nearby data may also be accessed in the near future. Therefore, when the cache loads a particular piece of data, it also loads nearby data to improve hit rate. +- **Temporal locality**: If a piece of data is accessed, it is likely to be accessed again in the near future. Cache leverages this principle by retaining recently accessed data to improve hit rate. -In fact, **arrays and linked lists have different cache utilization efficiencies**, which is mainly reflected in the following aspects. +In fact, **arrays and linked lists have different efficiencies in utilizing cache**, manifested in the following aspects. -- **Occupied space**: Linked list elements take up more space than array elements, resulting in less effective data being held in the cache. -- **Cache lines**: Linked list data is scattered throughout the memory, and cache is "loaded by row", so the proportion of invalid data loaded is higher. -- **Prefetch mechanism**: The data access pattern of arrays is more "predictable" than that of linked lists, that is, it is easier for the system to guess the data that is about to be loaded. -- **Spatial locality**: Arrays are stored in a continuous memory space, so data near the data being loaded is more likely to be accessed soon. +- **Space occupied**: Linked list elements occupy more space than array elements, resulting in fewer effective data in the cache. +- **Cache lines**: Linked list data are scattered throughout memory, while cache loads "by lines," so the proportion of invalid data loaded is higher. +- **Prefetching mechanism**: Arrays have more "predictable" data access patterns than linked lists, making it easier for the system to guess which data will be loaded next. +- **Spatial locality**: Arrays are stored in centralized memory space, so data near loaded data is more likely to be accessed soon. -Overall, **arrays have a higher cache hit rate and are generally more efficient in operation than linked lists**. This makes data structures based on arrays more popular in solving algorithmic problems. +Overall, **arrays have higher cache hit rates, thus they usually outperform linked lists in operation efficiency**. This makes data structures implemented based on arrays more popular when solving algorithmic problems. -It should be noted that **high cache efficiency does not mean that arrays are always better than linked lists**. The choice of data structure should depend on specific application requirements. For example, both arrays and linked lists can implement the "stack" data structure (which will be detailed in the next chapter), but they are suitable for different scenarios. +It is important to note that **high cache efficiency does not mean arrays are superior to linked lists in all cases**. In practical applications, which data structure to choose should be determined based on specific requirements. For example, both arrays and linked lists can implement the "stack" data structure (which will be discussed in detail in the next chapter), but they are suitable for different scenarios. -- In algorithm problems, we tend to choose stacks based on arrays because they provide higher operational efficiency and random access capabilities, with the only cost being the need to pre-allocate a certain amount of memory space for the array. -- If the data volume is very large, highly dynamic, and the expected size of the stack is difficult to estimate, then a stack based on a linked list is a better choice. Linked lists can distribute a large amount of data in different parts of the memory and avoid the additional overhead of array expansion. +- When solving algorithm problems, we tend to prefer stack implementations based on arrays, because they provide higher operation efficiency and the ability of random access, at the cost of needing to pre-allocate a certain amount of memory space for the array. +- If the data volume is very large, the dynamic nature is high, and the expected size of the stack is difficult to estimate, then a stack implementation based on linked lists is more suitable. Linked lists can distribute large amounts of data across different parts of memory and avoid the additional overhead produced by array expansion. diff --git a/en/docs/chapter_array_and_linkedlist/summary.md b/en/docs/chapter_array_and_linkedlist/summary.md index e2201dee2..ecdbb6350 100644 --- a/en/docs/chapter_array_and_linkedlist/summary.md +++ b/en/docs/chapter_array_and_linkedlist/summary.md @@ -4,82 +4,87 @@ comments: true # 4.5   Summary -### 1.   Key review +### 1.   Key Review -- Arrays and linked lists are two basic data structures, representing two storage methods in computer memory: contiguous space storage and non-contiguous space storage. Their characteristics complement each other. -- Arrays support random access and use less memory; however, they are inefficient in inserting and deleting elements and have a fixed length after initialization. -- Linked lists implement efficient node insertion and deletion through changing references (pointers) and can flexibly adjust their length; however, they have lower node access efficiency and consume more memory. -- Common types of linked lists include singly linked lists, circular linked lists, and doubly linked lists, each with its own application scenarios. -- Lists are ordered collections of elements that support addition, deletion, and modification, typically implemented based on dynamic arrays, retaining the advantages of arrays while allowing flexible length adjustment. -- The advent of lists significantly enhanced the practicality of arrays but may lead to some memory space wastage. -- During program execution, data is mainly stored in memory. Arrays provide higher memory space efficiency, while linked lists are more flexible in memory usage. -- Caches provide fast data access to CPUs through mechanisms like cache lines, prefetching, spatial locality, and temporal locality, significantly enhancing program execution efficiency. -- Due to higher cache hit rates, arrays are generally more efficient than linked lists. When choosing a data structure, the appropriate choice should be made based on specific needs and scenarios. +- Arrays and linked lists are two fundamental data structures, representing two different ways data can be stored in computer memory: contiguous memory storage and scattered memory storage. The characteristics of the two complement each other. +- Arrays support random access and use less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization. +- Linked lists achieve efficient insertion and deletion of nodes by modifying references (pointers), and can flexibly adjust length; however, node access is inefficient and memory consumption is higher. Common linked list types include singly linked lists, circular linked lists, and doubly linked lists. +- A list is an ordered collection of elements that supports insertion, deletion, search, and modification, typically implemented based on dynamic arrays. It retains the advantages of arrays while allowing flexible adjustment of length. +- The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space. +- During program execution, data is primarily stored in memory. Arrays provide higher memory space efficiency, while linked lists offer greater flexibility in memory usage. +- Caches provide fast data access to the CPU through mechanisms such as cache lines, prefetching, and spatial and temporal locality, significantly improving program execution efficiency. +- Because arrays have higher cache hit rates, they are generally more efficient than linked lists. When choosing a data structure, appropriate selection should be made based on specific requirements and scenarios. ### 2.   Q & A -**Q**: Does storing arrays on the stack versus the heap affect time and space efficiency? +**Q**: Does storing an array on the stack versus on the heap affect time efficiency and space efficiency? -Arrays stored on both the stack and heap are stored in contiguous memory spaces, and data operation efficiency is essentially the same. However, stacks and heaps have their own characteristics, leading to the following differences. +Arrays stored on the stack and on the heap are both stored in contiguous memory space, so data operation efficiency is basically the same. However, the stack and heap have their own characteristics, leading to the following differences. -1. Allocation and release efficiency: The stack is a smaller memory block, allocated automatically by the compiler; the heap memory is relatively larger and can be dynamically allocated in the code, more prone to fragmentation. Therefore, allocation and release operations on the heap are generally slower than on the stack. -2. Size limitation: Stack memory is relatively small, while the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays. -3. Flexibility: The size of arrays on the stack needs to be determined at compile-time, while the size of arrays on the heap can be dynamically determined at runtime. +1. Allocation and deallocation efficiency: The stack is a relatively small piece of memory, with allocation automatically handled by the compiler; the heap is relatively larger and can be dynamically allocated in code, more prone to fragmentation. Therefore, allocation and deallocation operations on the heap are usually slower than on the stack. +2. Size limitations: Stack memory is relatively small, and the heap size is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays. +3. Flexibility: The size of an array on the stack must be determined at compile time, while the size of an array on the heap can be determined dynamically at runtime. -**Q**: Why do arrays require elements of the same type, while linked lists do not emphasize same-type elements? +**Q**: Why do arrays require elements of the same type, while linked lists do not emphasize this requirement? -Linked lists consist of nodes connected by references (pointers), and each node can store data of different types, such as int, double, string, object, etc. +Linked lists are composed of nodes, with nodes connected through references (pointers), and each node can store different types of data, such as `int`, `double`, `string`, `object`, etc. -In contrast, array elements must be of the same type, allowing the calculation of offsets to access the corresponding element positions. For example, an array containing both int and long types, with single elements occupying 4 bytes and 8 bytes respectively, cannot use the following formula to calculate offsets, as the array contains elements of two different lengths. +In contrast, array elements must be of the same type, so that the corresponding element position can be obtained by calculating the offset. For example, if an array contains both `int` and `long` types, with individual elements occupying 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset, because the array contains two different "element lengths". ```shell -# Element memory address = array memory address + element length * element index +# Element Memory Address = Array Memory Address (first Element Memory address) + Element Length * Element Index ``` -**Q**: After deleting a node, is it necessary to set `P.next` to `None`? +**Q**: After deleting node `P`, do we need to set `P.next` to `None`? -Not modifying `P.next` is also acceptable. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been effectively removed from the list, and where `P` points no longer affects the list. +It is not necessary to modify `P.next`. From the perspective of the linked list, traversing from the head node to the tail node will no longer encounter `P`. This means that node `P` has been removed from the linked list, and it doesn't matter where node `P` points to at this time—it won't affect the linked list. -From a garbage collection perspective, for languages with automatic garbage collection mechanisms like Java, Python, and Go, whether node `P` is collected depends on whether there are still references pointing to it, not on the value of `P.next`. In languages like C and C++, we need to manually free the node's memory. +From a data structures and algorithms perspective (problem-solving), not disconnecting the pointer doesn't matter as long as the program logic is correct. From the perspective of standard libraries, disconnecting is safer and the logic is clearer. If not disconnected, assuming the deleted node is not properly reclaimed, it may affect the memory reclamation of its successor nodes. -**Q**: In linked lists, the time complexity for insertion and deletion operations is `O(1)`. But searching for the element before insertion or deletion takes `O(n)` time, so why isn't the time complexity `O(n)`? +**Q**: In a linked list, the time complexity of insertion and deletion operations is $O(1)$. However, both insertion and deletion require $O(n)$ time to find the element; why isn't the time complexity $O(n)$? -If an element is searched first and then deleted, the time complexity is indeed `O(n)`. However, the `O(1)` advantage of linked lists in insertion and deletion can be realized in other applications. For example, in the implementation of double-ended queues using linked lists, we maintain pointers always pointing to the head and tail nodes, making each insertion and deletion operation `O(1)`. +If the element is first found and then deleted, the time complexity is indeed $O(n)$. However, the advantage of $O(1)$ insertion and deletion in linked lists can be demonstrated in other applications. For example, a deque is well-suited for linked list implementation, where we maintain pointer variables always pointing to the head and tail nodes, with each insertion and deletion operation being $O(1)$. -**Q**: In the figure "Linked List Definition and Storage Method", do the light blue storage nodes occupy a single memory address, or do they share half with the node value? +**Q**: In the diagram "Linked List Definition and Storage Methods", does the light blue pointer node occupy a single memory address, or does it share equally with the node value? -The figure is just a qualitative representation; quantitative analysis depends on specific situations. +This diagram is a qualitative representation; a quantitative representation requires analysis based on the specific situation. -- Different types of node values occupy different amounts of space, such as int, long, double, and object instances. -- The memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes. +- Different types of node values occupy different amounts of space, such as `int`, `long`, `double`, and instance objects, etc. +- The amount of memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes. -**Q**: Is adding elements to the end of a list always `O(1)`? +**Q**: Is appending an element at the end of a list always $O(1)$? -If adding an element exceeds the list length, the list needs to be expanded first. The system will request a new memory block and move all elements of the original list over, in which case the time complexity becomes `O(n)`. +If appending an element exceeds the list length, the list must first be expanded before adding. The system allocates a new block of memory and moves all elements from the original list to it, in which case the time complexity becomes $O(n)$. -**Q**: The statement "The emergence of lists greatly improves the practicality of arrays, but may lead to some memory space wastage" - does this refer to the memory occupied by additional variables like capacity, length, and expansion multiplier? +**Q**: "The emergence of lists has greatly improved the practicality of arrays, but may result in some wasted memory space"—does this space waste refer to the memory occupied by additional variables such as capacity, length, and expansion factor? -The space wastage here mainly refers to two aspects: on the one hand, lists are set with an initial length, which we may not always need; on the other hand, to prevent frequent expansion, expansion usually multiplies by a coefficient, such as $\times 1.5$. This results in many empty slots, which we typically cannot fully fill. +This space waste mainly has two aspects: on one hand, lists typically set an initial length, which we may not need to fully utilize; on the other hand, to prevent frequent expansion, expansion generally multiplies by a coefficient, such as $\times 1.5$. As a result, there will be many empty positions that we typically cannot completely fill. -**Q**: In Python, after initializing `n = [1, 2, 3]`, the addresses of these 3 elements are contiguous, but initializing `m = [2, 1, 3]` shows that each element's `id` is not consecutive but identical to those in `n`. If the addresses of these elements are not contiguous, is `m` still an array? +**Q**: In Python, after initializing `n = [1, 2, 3]`, the addresses of these 3 elements are contiguous, but initializing `m = [2, 1, 3]` reveals that each element's id is not continuous; rather, they are the same as those in `n`. Since the addresses of these elements are not contiguous, is `m` still an array? -If we replace list elements with linked list nodes `n = [n1, n2, n3, n4, n5]`, these 5 node objects are also typically dispersed throughout memory. However, given a list index, we can still access the node's memory address in `O(1)` time, thereby accessing the corresponding node. This is because the array stores references to the nodes, not the nodes themselves. +If we replace list elements with linked list nodes `n = [n1, n2, n3, n4, n5]`, usually these 5 node objects are also scattered throughout memory. However, given a list index, we can still obtain the node memory address in $O(1)$ time, thereby accessing the corresponding node. This is because the array stores references to nodes, not the nodes themselves. -Unlike many languages, in Python, numbers are also wrapped as objects, and lists store references to these numbers, not the numbers themselves. Therefore, we find that the same number in two arrays has the same `id`, and these numbers' memory addresses need not be contiguous. +Unlike many languages, numbers in Python are wrapped as objects, and lists store not the numbers themselves, but references to the numbers. Therefore, we find that the same numbers in two arrays have the same id, and the memory addresses of these numbers need not be contiguous. -**Q**: The `std::list` in C++ STL has already implemented a doubly linked list, but it seems that some algorithm books don't directly use it. Is there any limitation? +**Q**: C++ STL has `std::list` which has already implemented a doubly linked list, but it seems that some algorithm books don't use it directly. Is there a limitation? -On the one hand, we often prefer to use arrays to implement algorithms, only using linked lists when necessary, mainly for two reasons. +On one hand, we often prefer to use arrays for implementing algorithms and only use linked lists when necessary, mainly for two reasons. -- Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next), `std::list` usually occupies more space than `std::vector`. -- Cache unfriendly: As the data is not stored continuously, `std::list` has a lower cache utilization rate. Generally, `std::vector` performs better. +- Space overhead: Since each element requires two additional pointers (one for the previous element and one for the next element), `std::list` typically consumes more space than `std::vector`. +- Cache unfriendliness: Since data is not stored contiguously, `std::list` has lower cache utilization. In general, `std::vector` has better performance. -On the other hand, linked lists are primarily necessary for binary trees and graphs. Stacks and queues are often implemented using the programming language's `stack` and `queue` classes, rather than linked lists. +On the other hand, cases where linked lists are necessary mainly involve binary trees and graphs. Stacks and queues usually use the `stack` and `queue` provided by the programming language, rather than linked lists. -**Q**: Does initializing a list `res = [0] * self.size()` result in each element of `res` referencing the same address? +**Q**: Does the operation `res = [[0]] * n` create a 2D list where each `[0]` is independent? -No. However, this issue arises with two-dimensional arrays, for example, initializing a two-dimensional list `res = [[0]] * self.size()` would reference the same list `[0]` multiple times. +No, they are not independent. In this 2D list, all the `[0]` are actually references to the same object. If we modify one element, we will find that all corresponding elements change accordingly. -**Q**: In deleting a node, is it necessary to break the reference to its successor node? +If we want each `[0]` in the 2D list to be independent, we can use `res = [[0] for _ in range(n)]` to achieve this. The principle of this approach is to initialize $n$ independent `[0]` list objects. -From the perspective of data structures and algorithms (problem-solving), it's okay not to break the link, as long as the program's logic is correct. From the perspective of standard libraries, breaking the link is safer and more logically clear. If the link is not broken, and the deleted node is not properly recycled, it could affect the recycling of the successor node's memory. +**Q**: Does the operation `res = [0] * n` create a list where each integer 0 is independent? + +In this list, all integer 0s are references to the same object. This is because Python uses a caching mechanism for small integers (typically -5 to 256) to maximize object reuse and improve performance. + +Although they point to the same object, we can still independently modify each element in the list. This is because Python integers are "immutable objects". When we modify an element, we are actually switching to a reference of another object, rather than changing the original object itself. + +However, when list elements are "mutable objects" (such as lists, dictionaries, or class instances), modifying an element directly changes the object itself, and all elements referencing that object will have the same change. diff --git a/en/docs/chapter_backtracking/backtracking_algorithm.md b/en/docs/chapter_backtracking/backtracking_algorithm.md index d7e073462..522e66bc6 100644 --- a/en/docs/chapter_backtracking/backtracking_algorithm.md +++ b/en/docs/chapter_backtracking/backtracking_algorithm.md @@ -2,23 +2,23 @@ comments: true --- -# 13.1   Backtracking algorithms +# 13.1   Backtracking Algorithm -Backtracking algorithm is a method to solve problems by exhaustive search. Its core concept is to start from an initial state and brutally search for all possible solutions. The algorithm records the correct ones until a solution is found or all possible solutions have been tried but no solution can be found. +The backtracking algorithm is a method for solving problems through exhaustive search. Its core idea is to start from an initial state and exhaustively search all possible solutions. When a correct solution is found, it is recorded. This process continues until a solution is found or all possible choices have been tried without finding a solution. -Backtracking typically employs "depth-first search" to traverse the solution space. In the "Binary tree" chapter, we mentioned that pre-order, in-order, and post-order traversals are all depth-first searches. Next, we are going to use pre-order traversal to solve a backtracking problem. This helps us to understand how the algorithm works gradually. +The backtracking algorithm typically employs "depth-first search" to traverse the solution space. In the "Binary Tree" chapter, we mentioned that preorder, inorder, and postorder traversals all belong to depth-first search. Next, we will construct a backtracking problem using preorder traversal to progressively understand how the backtracking algorithm works. -!!! question "Example One" +!!! question "Example 1" - Given a binary tree, search and record all nodes with a value of $7$ and return them in a list. + Given a binary tree, search and record all nodes with value $7$, and return a list of these nodes. -To solve this problem, we traverse this tree in pre-order and check if the current node's value is $7$. If it is, we add the node's value to the result list `res`. The process is shown in Figure 13-1: +For this problem, we perform a preorder traversal of the tree and check whether the current node's value is $7$. If it is, we add the node to the result list `res`. The relevant implementation is shown in the following figure and code: === "Python" ```python title="preorder_traversal_i_compact.py" def pre_order(root: TreeNode): - """Pre-order traversal: Example one""" + """Preorder traversal: Example 1""" if root is None: return if root.val == 7: @@ -31,7 +31,7 @@ To solve this problem, we traverse this tree in pre-order and check if the curre === "C++" ```cpp title="preorder_traversal_i_compact.cpp" - /* Pre-order traversal: Example one */ + /* Preorder traversal: Example 1 */ void preOrder(TreeNode *root) { if (root == nullptr) { return; @@ -48,7 +48,7 @@ To solve this problem, we traverse this tree in pre-order and check if the curre === "Java" ```java title="preorder_traversal_i_compact.java" - /* Pre-order traversal: Example one */ + /* Preorder traversal: Example 1 */ void preOrder(TreeNode root) { if (root == null) { return; @@ -65,92 +65,196 @@ To solve this problem, we traverse this tree in pre-order and check if the curre === "C#" ```csharp title="preorder_traversal_i_compact.cs" - [class]{preorder_traversal_i_compact}-[func]{PreOrder} + /* Preorder traversal: Example 1 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + if (root.val == 7) { + // Record solution + res.Add(root); + } + PreOrder(root.left); + PreOrder(root.right); + } ``` === "Go" ```go title="preorder_traversal_i_compact.go" - [class]{}-[func]{preOrderI} + /* Preorder traversal: Example 1 */ + func preOrderI(root *TreeNode, res *[]*TreeNode) { + if root == nil { + return + } + if (root.Val).(int) == 7 { + // Record solution + *res = append(*res, root) + } + preOrderI(root.Left, res) + preOrderI(root.Right, res) + } ``` === "Swift" ```swift title="preorder_traversal_i_compact.swift" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + if root.val == 7 { + // Record solution + res.append(root) + } + preOrder(root: root.left) + preOrder(root: root.right) + } ``` === "JS" ```javascript title="preorder_traversal_i_compact.js" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + function preOrder(root, res) { + if (root === null) { + return; + } + if (root.val === 7) { + // Record solution + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "TS" ```typescript title="preorder_traversal_i_compact.ts" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + function preOrder(root: TreeNode | null, res: TreeNode[]): void { + if (root === null) { + return; + } + if (root.val === 7) { + // Record solution + res.push(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "Dart" ```dart title="preorder_traversal_i_compact.dart" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + void preOrder(TreeNode? root, List res) { + if (root == null) { + return; + } + if (root.val == 7) { + // Record solution + res.add(root); + } + preOrder(root.left, res); + preOrder(root.right, res); + } ``` === "Rust" ```rust title="preorder_traversal_i_compact.rs" - [class]{}-[func]{pre_order} + /* Preorder traversal: Example 1 */ + fn pre_order(res: &mut Vec>>, root: Option<&Rc>>) { + if root.is_none() { + return; + } + if let Some(node) = root { + if node.borrow().val == 7 { + // Record solution + res.push(node.clone()); + } + pre_order(res, node.borrow().left.as_ref()); + pre_order(res, node.borrow().right.as_ref()); + } + } ``` === "C" ```c title="preorder_traversal_i_compact.c" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + if (root->val == 7) { + // Record solution + res[resSize++] = root; + } + preOrder(root->left); + preOrder(root->right); + } ``` === "Kotlin" ```kotlin title="preorder_traversal_i_compact.kt" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 1 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + if (root._val == 7) { + // Record solution + res!!.add(root) + } + preOrder(root.left) + preOrder(root.right) + } ``` === "Ruby" ```ruby title="preorder_traversal_i_compact.rb" - [class]{}-[func]{pre_order} + ### Pre-order traversal: example 1 ### + def pre_order(root) + return unless root + + # Record solution + $res << root if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + end ``` -=== "Zig" +![Search for nodes in preorder traversal](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" } - ```zig title="preorder_traversal_i_compact.zig" - [class]{}-[func]{preOrder} - ``` +

Figure 13-1   Search for nodes in preorder traversal

-![Searching nodes in pre-order traversal](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" } +## 13.1.1   Attempt and Backtrack -

Figure 13-1   Searching nodes in pre-order traversal

+**The reason it is called a backtracking algorithm is that it employs "attempt" and "backtrack" strategies when searching the solution space**. When the algorithm encounters a state where it cannot continue forward or cannot find a solution that satisfies the constraints, it will undo the previous choice, return to a previous state, and try other possible choices. -## 13.1.1   Trial and retreat +For Example 1, visiting each node represents an "attempt", while skipping over a leaf node or a function `return` from the parent node represents a "backtrack". -**It is called a backtracking algorithm because it uses a "trial" and "retreat" strategy when searching the solution space**. During the search, whenever it encounters a state where it can no longer proceed to obtain a satisfying solution, it undoes the previous choice and reverts to the previous state so that other possible choices can be chosen for the next attempt. +It is worth noting that **backtracking is not limited to function returns alone**. To illustrate this, let's extend Example 1 slightly. -In Example One, visiting each node starts a "trial". And passing a leaf node or the `return` statement to going back to the parent node suggests "retreat". +!!! question "Example 2" -It's worth noting that **retreat is not merely about function returns**. We'll expand slightly on Example One question to explain what it means. + In a binary tree, search all nodes with value $7$, **and return the paths from the root node to these nodes**. -!!! question "Example Two" - - In a binary tree, search for all nodes with a value of $7$ and for all matching nodes, **please return the paths from the root node to that node**. - -Based on the code from Example One, we need to use a list called `path` to record the visited node paths. When a node with a value of $7$ is reached, we copy `path` and add it to the result list `res`. After the traversal, `res` holds all the solutions. The code is as shown: +Based on the code from Example 1, we need to use a list `path` to record the visited node path. When we reach a node with value $7$, we copy `path` and add it to the result list `res`. After traversal is complete, `res` contains all the solutions. The code is as follows: === "Python" ```python title="preorder_traversal_ii_compact.py" def pre_order(root: TreeNode): - """Pre-order traversal: Example two""" + """Preorder traversal: Example 2""" if root is None: return # Attempt @@ -160,14 +264,14 @@ Based on the code from Example One, we need to use a list called `path` to recor res.append(list(path)) pre_order(root.left) pre_order(root.right) - # Retract + # Backtrack path.pop() ``` === "C++" ```cpp title="preorder_traversal_ii_compact.cpp" - /* Pre-order traversal: Example two */ + /* Preorder traversal: Example 2 */ void preOrder(TreeNode *root) { if (root == nullptr) { return; @@ -180,7 +284,7 @@ Based on the code from Example One, we need to use a list called `path` to recor } preOrder(root->left); preOrder(root->right); - // Retract + // Backtrack path.pop_back(); } ``` @@ -188,7 +292,7 @@ Based on the code from Example One, we need to use a list called `path` to recor === "Java" ```java title="preorder_traversal_ii_compact.java" - /* Pre-order traversal: Example two */ + /* Preorder traversal: Example 2 */ void preOrder(TreeNode root) { if (root == null) { return; @@ -201,7 +305,7 @@ Based on the code from Example One, we need to use a list called `path` to recor } preOrder(root.left); preOrder(root.right); - // Retract + // Backtrack path.remove(path.size() - 1); } ``` @@ -209,75 +313,237 @@ Based on the code from Example One, we need to use a list called `path` to recor === "C#" ```csharp title="preorder_traversal_ii_compact.cs" - [class]{preorder_traversal_ii_compact}-[func]{PreOrder} + /* Preorder traversal: Example 2 */ + void PreOrder(TreeNode? root) { + if (root == null) { + return; + } + // Attempt + path.Add(root); + if (root.val == 7) { + // Record solution + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // Backtrack + path.RemoveAt(path.Count - 1); + } ``` === "Go" ```go title="preorder_traversal_ii_compact.go" - [class]{}-[func]{preOrderII} + /* Preorder traversal: Example 2 */ + func preOrderII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + if root == nil { + return + } + // Attempt + *path = append(*path, root) + if root.Val.(int) == 7 { + // Record solution + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderII(root.Left, res, path) + preOrderII(root.Right, res, path) + // Backtrack + *path = (*path)[:len(*path)-1] + } ``` === "Swift" ```swift title="preorder_traversal_ii_compact.swift" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // Attempt + path.append(root) + if root.val == 7 { + // Record solution + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // Backtrack + path.removeLast() + } ``` === "JS" ```javascript title="preorder_traversal_ii_compact.js" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + function preOrder(root, path, res) { + if (root === null) { + return; + } + // Attempt + path.push(root); + if (root.val === 7) { + // Record solution + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.pop(); + } ``` === "TS" ```typescript title="preorder_traversal_ii_compact.ts" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + if (root === null) { + return; + } + // Attempt + path.push(root); + if (root.val === 7) { + // Record solution + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.pop(); + } ``` === "Dart" ```dart title="preorder_traversal_ii_compact.dart" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null) { + return; + } + + // Attempt + path.add(root); + if (root.val == 7) { + // Record solution + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.removeLast(); + } ``` === "Rust" ```rust title="preorder_traversal_ii_compact.rs" - [class]{}-[func]{pre_order} + /* Preorder traversal: Example 2 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option<&Rc>>, + ) { + if root.is_none() { + return; + } + if let Some(node) = root { + // Attempt + path.push(node.clone()); + if node.borrow().val == 7 { + // Record solution + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.as_ref()); + pre_order(res, path, node.borrow().right.as_ref()); + // Backtrack + path.pop(); + } + } ``` === "C" ```c title="preorder_traversal_ii_compact.c" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + void preOrder(TreeNode *root) { + if (root == NULL) { + return; + } + // Attempt + path[pathSize++] = root; + if (root->val == 7) { + // Record solution + for (int i = 0; i < pathSize; ++i) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // Backtrack + pathSize--; + } ``` === "Kotlin" ```kotlin title="preorder_traversal_ii_compact.kt" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 2 */ + fun preOrder(root: TreeNode?) { + if (root == null) { + return + } + // Attempt + path!!.add(root) + if (root._val == 7) { + // Record solution + res!!.add(path!!.toMutableList()) + } + preOrder(root.left) + preOrder(root.right) + // Backtrack + path!!.removeAt(path!!.size - 1) + } ``` === "Ruby" ```ruby title="preorder_traversal_ii_compact.rb" - [class]{}-[func]{pre_order} + ### Pre-order traversal: example 2 ### + def pre_order(root) + return unless root + + # Attempt + $path << root + + # Record solution + $res << $path.dup if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + + # Backtrack + $path.pop + end ``` -=== "Zig" +In each "attempt", we record the path by adding the current node to `path`; before "backtracking", we need to remove the node from `path`, **to restore the state before this attempt**. - ```zig title="preorder_traversal_ii_compact.zig" - [class]{}-[func]{preOrder} - ``` - -In each "trial", we record the path by adding the current node to `path`. Whenever we need to "retreat", we pop the node from `path` **to restore the state prior to this failed attempt**. - -By observing the process shown in Figure 13-2, **the trial is like "advancing", and retreat is like "undoing"**. The later pairs can be seen as a reverse operation to their counterpart. +Observing the process shown in the following figure, **we can understand attempt and backtrack as "advance" and "undo"**, two operations that are the reverse of each other. === "<1>" - ![Trying and retreating](backtracking_algorithm.assets/preorder_find_paths_step1.png){ class="animation-figure" } + ![Attempt and backtrack](backtracking_algorithm.assets/preorder_find_paths_step1.png){ class="animation-figure" } === "<2>" ![preorder_find_paths_step2](backtracking_algorithm.assets/preorder_find_paths_step2.png){ class="animation-figure" } @@ -309,23 +575,23 @@ By observing the process shown in Figure 13-2, **the trial is like "advancing", === "<11>" ![preorder_find_paths_step11](backtracking_algorithm.assets/preorder_find_paths_step11.png){ class="animation-figure" } -

Figure 13-2   Trying and retreating

+

Figure 13-2   Attempt and backtrack

-## 13.1.2   Prune +## 13.1.2   Pruning -Complex backtracking problems usually involve one or more constraints, **which are often used for "pruning"**. +Complex backtracking problems usually contain one or more constraints. **Constraints can typically be used for "pruning"**. -!!! question "Example Three" +!!! question "Example 3" - In a binary tree, search for all nodes with a value of $7$ and return the paths from the root to these nodes, **with restriction that the paths do not contain nodes with a value of $3$**. + In a binary tree, search all nodes with value $7$ and return the paths from the root node to these nodes, **but require that the paths do not contain nodes with value $3$**. -To meet the above constraints, **we need to add a pruning operation**: during the search process, if a node with a value of $3$ is encountered, it aborts further searching down through the path immediately. The code is as shown: +To satisfy the above constraints, **we need to add pruning operations**: during the search process, if we encounter a node with value $3$, we return early and do not continue searching. The code is as follows: === "Python" ```python title="preorder_traversal_iii_compact.py" def pre_order(root: TreeNode): - """Pre-order traversal: Example three""" + """Preorder traversal: Example 3""" # Pruning if root is None or root.val == 3: return @@ -336,14 +602,14 @@ To meet the above constraints, **we need to add a pruning operation**: during th res.append(list(path)) pre_order(root.left) pre_order(root.right) - # Retract + # Backtrack path.pop() ``` === "C++" ```cpp title="preorder_traversal_iii_compact.cpp" - /* Pre-order traversal: Example three */ + /* Preorder traversal: Example 3 */ void preOrder(TreeNode *root) { // Pruning if (root == nullptr || root->val == 3) { @@ -357,7 +623,7 @@ To meet the above constraints, **we need to add a pruning operation**: during th } preOrder(root->left); preOrder(root->right); - // Retract + // Backtrack path.pop_back(); } ``` @@ -365,7 +631,7 @@ To meet the above constraints, **we need to add a pruning operation**: during th === "Java" ```java title="preorder_traversal_iii_compact.java" - /* Pre-order traversal: Example three */ + /* Preorder traversal: Example 3 */ void preOrder(TreeNode root) { // Pruning if (root == null || root.val == 3) { @@ -379,7 +645,7 @@ To meet the above constraints, **we need to add a pruning operation**: during th } preOrder(root.left); preOrder(root.right); - // Retract + // Backtrack path.remove(path.size() - 1); } ``` @@ -387,100 +653,271 @@ To meet the above constraints, **we need to add a pruning operation**: during th === "C#" ```csharp title="preorder_traversal_iii_compact.cs" - [class]{preorder_traversal_iii_compact}-[func]{PreOrder} + /* Preorder traversal: Example 3 */ + void PreOrder(TreeNode? root) { + // Pruning + if (root == null || root.val == 3) { + return; + } + // Attempt + path.Add(root); + if (root.val == 7) { + // Record solution + res.Add(new List(path)); + } + PreOrder(root.left); + PreOrder(root.right); + // Backtrack + path.RemoveAt(path.Count - 1); + } ``` === "Go" ```go title="preorder_traversal_iii_compact.go" - [class]{}-[func]{preOrderIII} + /* Preorder traversal: Example 3 */ + func preOrderIII(root *TreeNode, res *[][]*TreeNode, path *[]*TreeNode) { + // Pruning + if root == nil || root.Val == 3 { + return + } + // Attempt + *path = append(*path, root) + if root.Val.(int) == 7 { + // Record solution + *res = append(*res, append([]*TreeNode{}, *path...)) + } + preOrderIII(root.Left, res, path) + preOrderIII(root.Right, res, path) + // Backtrack + *path = (*path)[:len(*path)-1] + } ``` === "Swift" ```swift title="preorder_traversal_iii_compact.swift" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + func preOrder(root: TreeNode?) { + // Pruning + guard let root = root, root.val != 3 else { + return + } + // Attempt + path.append(root) + if root.val == 7 { + // Record solution + res.append(path) + } + preOrder(root: root.left) + preOrder(root: root.right) + // Backtrack + path.removeLast() + } ``` === "JS" ```javascript title="preorder_traversal_iii_compact.js" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + function preOrder(root, path, res) { + // Pruning + if (root === null || root.val === 3) { + return; + } + // Attempt + path.push(root); + if (root.val === 7) { + // Record solution + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.pop(); + } ``` === "TS" ```typescript title="preorder_traversal_iii_compact.ts" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + function preOrder( + root: TreeNode | null, + path: TreeNode[], + res: TreeNode[][] + ): void { + // Pruning + if (root === null || root.val === 3) { + return; + } + // Attempt + path.push(root); + if (root.val === 7) { + // Record solution + res.push([...path]); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.pop(); + } ``` === "Dart" ```dart title="preorder_traversal_iii_compact.dart" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + void preOrder( + TreeNode? root, + List path, + List> res, + ) { + if (root == null || root.val == 3) { + return; + } + + // Attempt + path.add(root); + if (root.val == 7) { + // Record solution + res.add(List.from(path)); + } + preOrder(root.left, path, res); + preOrder(root.right, path, res); + // Backtrack + path.removeLast(); + } ``` === "Rust" ```rust title="preorder_traversal_iii_compact.rs" - [class]{}-[func]{pre_order} + /* Preorder traversal: Example 3 */ + fn pre_order( + res: &mut Vec>>>, + path: &mut Vec>>, + root: Option<&Rc>>, + ) { + // Pruning + if root.is_none() || root.as_ref().unwrap().borrow().val == 3 { + return; + } + if let Some(node) = root { + // Attempt + path.push(node.clone()); + if node.borrow().val == 7 { + // Record solution + res.push(path.clone()); + } + pre_order(res, path, node.borrow().left.as_ref()); + pre_order(res, path, node.borrow().right.as_ref()); + // Backtrack + path.pop(); + } + } ``` === "C" ```c title="preorder_traversal_iii_compact.c" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + void preOrder(TreeNode *root) { + // Pruning + if (root == NULL || root->val == 3) { + return; + } + // Attempt + path[pathSize++] = root; + if (root->val == 7) { + // Record solution + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } + preOrder(root->left); + preOrder(root->right); + // Backtrack + pathSize--; + } ``` === "Kotlin" ```kotlin title="preorder_traversal_iii_compact.kt" - [class]{}-[func]{preOrder} + /* Preorder traversal: Example 3 */ + fun preOrder(root: TreeNode?) { + // Pruning + if (root == null || root._val == 3) { + return + } + // Attempt + path!!.add(root) + if (root._val == 7) { + // Record solution + res!!.add(path!!.toMutableList()) + } + preOrder(root.left) + preOrder(root.right) + // Backtrack + path!!.removeAt(path!!.size - 1) + } ``` === "Ruby" ```ruby title="preorder_traversal_iii_compact.rb" - [class]{}-[func]{pre_order} + ### Pre-order traversal: example 3 ### + def pre_order(root) + # Pruning + return if !root || root.val == 3 + + # Attempt + $path.append(root) + + # Record solution + $res << $path.dup if root.val == 7 + + pre_order(root.left) + pre_order(root.right) + + # Backtrack + $path.pop + end ``` -=== "Zig" +"Pruning" is a vivid term. As shown in the following figure, during the search process, **we "prune" search branches that do not satisfy the constraints**, avoiding many meaningless attempts and thus improving search efficiency. - ```zig title="preorder_traversal_iii_compact.zig" - [class]{}-[func]{preOrder} - ``` +![Pruning according to constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } -"Pruning" is a very vivid noun. As shown in Figure 13-3, in the search process, **we "cut off" the search branches that do not meet the constraints**. It avoids further unnecessary attempts, thus enhances the search efficiency. +

Figure 13-3   Pruning according to constraints

-![Pruning based on constraints](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } +## 13.1.3   Framework Code -

Figure 13-3   Pruning based on constraints

+Next, we attempt to extract the main framework of backtracking's "attempt, backtrack, and pruning", to improve code generality. -## 13.1.3   Framework code - -Now, let's try to distill the main framework of "trial, retreat, and prune" from backtracking to enhance the code's universality. - -In the following framework code, `state` represents the current state of the problem, `choices` represents the choices available under the current state: +In the following framework code, `state` represents the current state of the problem, and `choices` represents the choices available in the current state: === "Python" ```python title="" def backtrack(state: State, choices: list[choice], res: list[state]): """Backtracking algorithm framework""" - # Check if it's a solution + # Check if it is a solution if is_solution(state): # Record the solution record_solution(state, res) # Stop searching return - # Iterate through all choices + # Traverse all choices for choice in choices: - # Prune: check if the choice is valid + # Pruning: check if the choice is valid if is_valid(state, choice): - # Trial: make a choice, update the state + # Attempt: make a choice and update the state make_choice(state, choice) backtrack(state, choices, res) - # Retreat: undo the choice, revert to the previous state + # Backtrack: undo the choice and restore to the previous state undo_choice(state, choice) ``` @@ -489,21 +926,21 @@ In the following framework code, `state` represents the current state of the pro ```cpp title="" /* Backtracking algorithm framework */ void backtrack(State *state, vector &choices, vector &res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (Choice choice : choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice); } } @@ -515,21 +952,21 @@ In the following framework code, `state` represents the current state of the pro ```java title="" /* Backtracking algorithm framework */ void backtrack(State state, List choices, List res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (Choice choice : choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice); } } @@ -541,21 +978,21 @@ In the following framework code, `state` represents the current state of the pro ```csharp title="" /* Backtracking algorithm framework */ void Backtrack(State state, List choices, List res) { - // Check if it's a solution + // Check if it is a solution if (IsSolution(state)) { // Record the solution RecordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices foreach (Choice choice in choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (IsValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state MakeChoice(state, choice); Backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state UndoChoice(state, choice); } } @@ -567,21 +1004,21 @@ In the following framework code, `state` represents the current state of the pro ```go title="" /* Backtracking algorithm framework */ func backtrack(state *State, choices []Choice, res *[]State) { - // Check if it's a solution + // Check if it is a solution if isSolution(state) { // Record the solution recordSolution(state, res) // Stop searching return } - // Iterate through all choices + // Traverse all choices for _, choice := range choices { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if isValid(state, choice) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice) backtrack(state, choices, res) - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice) } } @@ -593,21 +1030,21 @@ In the following framework code, `state` represents the current state of the pro ```swift title="" /* Backtracking algorithm framework */ func backtrack(state: inout State, choices: [Choice], res: inout [State]) { - // Check if it's a solution + // Check if it is a solution if isSolution(state: state) { // Record the solution recordSolution(state: state, res: &res) // Stop searching return } - // Iterate through all choices + // Traverse all choices for choice in choices { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if isValid(state: state, choice: choice) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state: &state, choice: choice) backtrack(state: &state, choices: choices, res: &res) - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state: &state, choice: choice) } } @@ -619,21 +1056,21 @@ In the following framework code, `state` represents the current state of the pro ```javascript title="" /* Backtracking algorithm framework */ function backtrack(state, choices, res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (let choice of choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice); } } @@ -645,21 +1082,21 @@ In the following framework code, `state` represents the current state of the pro ```typescript title="" /* Backtracking algorithm framework */ function backtrack(state: State, choices: Choice[], res: State[]): void { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (let choice of choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice); } } @@ -671,21 +1108,21 @@ In the following framework code, `state` represents the current state of the pro ```dart title="" /* Backtracking algorithm framework */ void backtrack(State state, List, List res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (Choice choice in choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice); } } @@ -697,21 +1134,21 @@ In the following framework code, `state` represents the current state of the pro ```rust title="" /* Backtracking algorithm framework */ fn backtrack(state: &mut State, choices: &Vec, res: &mut Vec) { - // Check if it's a solution + // Check if it is a solution if is_solution(state) { // Record the solution record_solution(state, res); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for choice in choices { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if is_valid(state, choice) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state make_choice(state, choice); backtrack(state, choices, res); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undo_choice(state, choice); } } @@ -723,21 +1160,21 @@ In the following framework code, `state` represents the current state of the pro ```c title="" /* Backtracking algorithm framework */ void backtrack(State *state, Choice *choices, int numChoices, State *res, int numRes) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res, numRes); // Stop searching return; } - // Iterate through all choices + // Traverse all choices for (int i = 0; i < numChoices; i++) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, &choices[i])) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, &choices[i]); backtrack(state, choices, numChoices, res, numRes); - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, &choices[i]); } } @@ -749,21 +1186,21 @@ In the following framework code, `state` represents the current state of the pro ```kotlin title="" /* Backtracking algorithm framework */ fun backtrack(state: State?, choices: List, res: List?) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record the solution recordSolution(state, res) // Stop searching return } - // Iterate through all choices + // Traverse all choices for (choice in choices) { - // Prune: check if the choice is valid + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Trial: make a choice, update the state + // Attempt: make a choice and update the state makeChoice(state, choice) backtrack(state, choices, res) - // Retreat: undo the choice, revert to the previous state + // Backtrack: undo the choice and restore to the previous state undoChoice(state, choice) } } @@ -773,22 +1210,36 @@ In the following framework code, `state` represents the current state of the pro === "Ruby" ```ruby title="" + ### Backtracking algorithm framework ### + def backtrack(state, choices, res) + # Check if it is a solution + if is_solution?(state) + # Record the solution + record_solution(state, res) + return + end + # Traverse all choices + for choice in choices + # Pruning: check if the choice is valid + if is_valid?(state, choice) + # Attempt: make a choice and update the state + make_choice(state, choice) + backtrack(state, choices, res) + # Backtrack: undo the choice and restore to the previous state + undo_choice(state, choice) + end + end + end ``` -=== "Zig" - - ```zig title="" - - ``` - -Now, we are able to solve Example Three using the framework code. The `state` is the node traversal path, `choices` are the current node's left and right children, and the result `res` is the list of paths: +Next, we solve Example 3 based on the framework code. The state `state` is the node traversal path, the choices `choices` are the left and right child nodes of the current node, and the result `res` is a list of paths: === "Python" ```python title="preorder_traversal_iii_template.py" def is_solution(state: list[TreeNode]) -> bool: - """Determine if the current state is a solution""" + """Check if the current state is a solution""" return state and state[-1].val == 7 def record_solution(state: list[TreeNode], res: list[list[TreeNode]]): @@ -796,7 +1247,7 @@ Now, we are able to solve Example Three using the framework code. The `state` is res.append(list(state)) def is_valid(state: list[TreeNode], choice: TreeNode) -> bool: - """Determine if the choice is legal under the current state""" + """Check if the choice is valid under the current state""" return choice is not None and choice.val != 3 def make_choice(state: list[TreeNode], choice: TreeNode): @@ -810,27 +1261,27 @@ Now, we are able to solve Example Three using the framework code. The `state` is def backtrack( state: list[TreeNode], choices: list[TreeNode], res: list[list[TreeNode]] ): - """Backtracking algorithm: Example three""" - # Check if it's a solution + """Backtracking algorithm: Example 3""" + # Check if it is a solution if is_solution(state): # Record solution record_solution(state, res) # Traverse all choices for choice in choices: - # Pruning: check if the choice is legal + # Pruning: check if the choice is valid if is_valid(state, choice): - # Attempt: make a choice, update the state + # Attempt: make choice, update state make_choice(state, choice) # Proceed to the next round of selection backtrack(state, [choice.left, choice.right], res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state undo_choice(state, choice) ``` === "C++" ```cpp title="preorder_traversal_iii_template.cpp" - /* Determine if the current state is a solution */ + /* Check if the current state is a solution */ bool isSolution(vector &state) { return !state.empty() && state.back()->val == 7; } @@ -840,7 +1291,7 @@ Now, we are able to solve Example Three using the framework code. The `state` is res.push_back(state); } - /* Determine if the choice is legal under the current state */ + /* Check if the choice is valid under the current state */ bool isValid(vector &state, TreeNode *choice) { return choice != nullptr && choice->val != 3; } @@ -855,23 +1306,23 @@ Now, we are able to solve Example Three using the framework code. The `state` is state.pop_back(); } - /* Backtracking algorithm: Example three */ + /* Backtracking algorithm: Example 3 */ void backtrack(vector &state, vector &choices, vector> &res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record solution recordSolution(state, res); } // Traverse all choices for (TreeNode *choice : choices) { - // Pruning: check if the choice is legal + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Attempt: make a choice, update the state + // Attempt: make choice, update state makeChoice(state, choice); // Proceed to the next round of selection vector nextChoices{choice->left, choice->right}; backtrack(state, nextChoices, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state undoChoice(state, choice); } } @@ -881,7 +1332,7 @@ Now, we are able to solve Example Three using the framework code. The `state` is === "Java" ```java title="preorder_traversal_iii_template.java" - /* Determine if the current state is a solution */ + /* Check if the current state is a solution */ boolean isSolution(List state) { return !state.isEmpty() && state.get(state.size() - 1).val == 7; } @@ -891,7 +1342,7 @@ Now, we are able to solve Example Three using the framework code. The `state` is res.add(new ArrayList<>(state)); } - /* Determine if the choice is legal under the current state */ + /* Check if the choice is valid under the current state */ boolean isValid(List state, TreeNode choice) { return choice != null && choice.val != 3; } @@ -906,22 +1357,22 @@ Now, we are able to solve Example Three using the framework code. The `state` is state.remove(state.size() - 1); } - /* Backtracking algorithm: Example three */ + /* Backtracking algorithm: Example 3 */ void backtrack(List state, List choices, List> res) { - // Check if it's a solution + // Check if it is a solution if (isSolution(state)) { // Record solution recordSolution(state, res); } // Traverse all choices for (TreeNode choice : choices) { - // Pruning: check if the choice is legal + // Pruning: check if the choice is valid if (isValid(state, choice)) { - // Attempt: make a choice, update the state + // Attempt: make choice, update state makeChoice(state, choice); // Proceed to the next round of selection backtrack(state, Arrays.asList(choice.left, choice.right), res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state undoChoice(state, choice); } } @@ -931,248 +1382,602 @@ Now, we are able to solve Example Three using the framework code. The `state` is === "C#" ```csharp title="preorder_traversal_iii_template.cs" - [class]{preorder_traversal_iii_template}-[func]{IsSolution} + /* Check if the current state is a solution */ + bool IsSolution(List state) { + return state.Count != 0 && state[^1].val == 7; + } - [class]{preorder_traversal_iii_template}-[func]{RecordSolution} + /* Record solution */ + void RecordSolution(List state, List> res) { + res.Add(new List(state)); + } - [class]{preorder_traversal_iii_template}-[func]{IsValid} + /* Check if the choice is valid under the current state */ + bool IsValid(List state, TreeNode choice) { + return choice != null && choice.val != 3; + } - [class]{preorder_traversal_iii_template}-[func]{MakeChoice} + /* Update state */ + void MakeChoice(List state, TreeNode choice) { + state.Add(choice); + } - [class]{preorder_traversal_iii_template}-[func]{UndoChoice} + /* Restore state */ + void UndoChoice(List state, TreeNode choice) { + state.RemoveAt(state.Count - 1); + } - [class]{preorder_traversal_iii_template}-[func]{Backtrack} + /* Backtracking algorithm: Example 3 */ + void Backtrack(List state, List choices, List> res) { + // Check if it is a solution + if (IsSolution(state)) { + // Record solution + RecordSolution(state, res); + } + // Traverse all choices + foreach (TreeNode choice in choices) { + // Pruning: check if the choice is valid + if (IsValid(state, choice)) { + // Attempt: make choice, update state + MakeChoice(state, choice); + // Proceed to the next round of selection + Backtrack(state, [choice.left!, choice.right!], res); + // Backtrack: undo choice, restore to previous state + UndoChoice(state, choice); + } + } + } ``` === "Go" ```go title="preorder_traversal_iii_template.go" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + func isSolution(state *[]*TreeNode) bool { + return len(*state) != 0 && (*state)[len(*state)-1].Val == 7 + } - [class]{}-[func]{recordSolution} + /* Record solution */ + func recordSolution(state *[]*TreeNode, res *[][]*TreeNode) { + *res = append(*res, append([]*TreeNode{}, *state...)) + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + func isValid(state *[]*TreeNode, choice *TreeNode) bool { + return choice != nil && choice.Val != 3 + } - [class]{}-[func]{makeChoice} + /* Update state */ + func makeChoice(state *[]*TreeNode, choice *TreeNode) { + *state = append(*state, choice) + } - [class]{}-[func]{undoChoice} + /* Restore state */ + func undoChoice(state *[]*TreeNode, choice *TreeNode) { + *state = (*state)[:len(*state)-1] + } - [class]{}-[func]{backtrackIII} + /* Backtracking algorithm: Example 3 */ + func backtrackIII(state *[]*TreeNode, choices *[]*TreeNode, res *[][]*TreeNode) { + // Check if it is a solution + if isSolution(state) { + // Record solution + recordSolution(state, res) + } + // Traverse all choices + for _, choice := range *choices { + // Pruning: check if the choice is valid + if isValid(state, choice) { + // Attempt: make choice, update state + makeChoice(state, choice) + // Proceed to the next round of selection + temp := make([]*TreeNode, 0) + temp = append(temp, choice.Left, choice.Right) + backtrackIII(state, &temp, res) + // Backtrack: undo choice, restore to previous state + undoChoice(state, choice) + } + } + } ``` === "Swift" ```swift title="preorder_traversal_iii_template.swift" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + func isSolution(state: [TreeNode]) -> Bool { + !state.isEmpty && state.last!.val == 7 + } - [class]{}-[func]{recordSolution} + /* Record solution */ + func recordSolution(state: [TreeNode], res: inout [[TreeNode]]) { + res.append(state) + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + func isValid(state: [TreeNode], choice: TreeNode?) -> Bool { + choice != nil && choice!.val != 3 + } - [class]{}-[func]{makeChoice} + /* Update state */ + func makeChoice(state: inout [TreeNode], choice: TreeNode) { + state.append(choice) + } - [class]{}-[func]{undoChoice} + /* Restore state */ + func undoChoice(state: inout [TreeNode], choice: TreeNode) { + state.removeLast() + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + func backtrack(state: inout [TreeNode], choices: [TreeNode], res: inout [[TreeNode]]) { + // Check if it is a solution + if isSolution(state: state) { + recordSolution(state: state, res: &res) + } + // Traverse all choices + for choice in choices { + // Pruning: check if the choice is valid + if isValid(state: state, choice: choice) { + // Attempt: make choice, update state + makeChoice(state: &state, choice: choice) + // Proceed to the next round of selection + backtrack(state: &state, choices: [choice.left, choice.right].compactMap { $0 }, res: &res) + // Backtrack: undo choice, restore to previous state + undoChoice(state: &state, choice: choice) + } + } + } ``` === "JS" ```javascript title="preorder_traversal_iii_template.js" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + function isSolution(state) { + return state && state[state.length - 1]?.val === 7; + } - [class]{}-[func]{recordSolution} + /* Record solution */ + function recordSolution(state, res) { + res.push([...state]); + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + function isValid(state, choice) { + return choice !== null && choice.val !== 3; + } - [class]{}-[func]{makeChoice} + /* Update state */ + function makeChoice(state, choice) { + state.push(choice); + } - [class]{}-[func]{undoChoice} + /* Restore state */ + function undoChoice(state) { + state.pop(); + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + function backtrack(state, choices, res) { + // Check if it is a solution + if (isSolution(state)) { + // Record solution + recordSolution(state, res); + } + // Traverse all choices + for (const choice of choices) { + // Pruning: check if the choice is valid + if (isValid(state, choice)) { + // Attempt: make choice, update state + makeChoice(state, choice); + // Proceed to the next round of selection + backtrack(state, [choice.left, choice.right], res); + // Backtrack: undo choice, restore to previous state + undoChoice(state); + } + } + } ``` === "TS" ```typescript title="preorder_traversal_iii_template.ts" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + function isSolution(state: TreeNode[]): boolean { + return state && state[state.length - 1]?.val === 7; + } - [class]{}-[func]{recordSolution} + /* Record solution */ + function recordSolution(state: TreeNode[], res: TreeNode[][]): void { + res.push([...state]); + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + function isValid(state: TreeNode[], choice: TreeNode): boolean { + return choice !== null && choice.val !== 3; + } - [class]{}-[func]{makeChoice} + /* Update state */ + function makeChoice(state: TreeNode[], choice: TreeNode): void { + state.push(choice); + } - [class]{}-[func]{undoChoice} + /* Restore state */ + function undoChoice(state: TreeNode[]): void { + state.pop(); + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + function backtrack( + state: TreeNode[], + choices: TreeNode[], + res: TreeNode[][] + ): void { + // Check if it is a solution + if (isSolution(state)) { + // Record solution + recordSolution(state, res); + } + // Traverse all choices + for (const choice of choices) { + // Pruning: check if the choice is valid + if (isValid(state, choice)) { + // Attempt: make choice, update state + makeChoice(state, choice); + // Proceed to the next round of selection + backtrack(state, [choice.left, choice.right], res); + // Backtrack: undo choice, restore to previous state + undoChoice(state); + } + } + } ``` === "Dart" ```dart title="preorder_traversal_iii_template.dart" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + bool isSolution(List state) { + return state.isNotEmpty && state.last.val == 7; + } - [class]{}-[func]{recordSolution} + /* Record solution */ + void recordSolution(List state, List> res) { + res.add(List.from(state)); + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + bool isValid(List state, TreeNode? choice) { + return choice != null && choice.val != 3; + } - [class]{}-[func]{makeChoice} + /* Update state */ + void makeChoice(List state, TreeNode? choice) { + state.add(choice!); + } - [class]{}-[func]{undoChoice} + /* Restore state */ + void undoChoice(List state, TreeNode? choice) { + state.removeLast(); + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + void backtrack( + List state, + List choices, + List> res, + ) { + // Check if it is a solution + if (isSolution(state)) { + // Record solution + recordSolution(state, res); + } + // Traverse all choices + for (TreeNode? choice in choices) { + // Pruning: check if the choice is valid + if (isValid(state, choice)) { + // Attempt: make choice, update state + makeChoice(state, choice); + // Proceed to the next round of selection + backtrack(state, [choice!.left, choice.right], res); + // Backtrack: undo choice, restore to previous state + undoChoice(state, choice); + } + } + } ``` === "Rust" ```rust title="preorder_traversal_iii_template.rs" - [class]{}-[func]{is_solution} + /* Check if the current state is a solution */ + fn is_solution(state: &mut Vec>>) -> bool { + return !state.is_empty() && state.last().unwrap().borrow().val == 7; + } - [class]{}-[func]{record_solution} + /* Record solution */ + fn record_solution( + state: &mut Vec>>, + res: &mut Vec>>>, + ) { + res.push(state.clone()); + } - [class]{}-[func]{is_valid} + /* Check if the choice is valid under the current state */ + fn is_valid(_: &mut Vec>>, choice: Option<&Rc>>) -> bool { + return choice.is_some() && choice.unwrap().borrow().val != 3; + } - [class]{}-[func]{make_choice} + /* Update state */ + fn make_choice(state: &mut Vec>>, choice: Rc>) { + state.push(choice); + } - [class]{}-[func]{undo_choice} + /* Restore state */ + fn undo_choice(state: &mut Vec>>, _: Rc>) { + state.pop(); + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + fn backtrack( + state: &mut Vec>>, + choices: &Vec>>>, + res: &mut Vec>>>, + ) { + // Check if it is a solution + if is_solution(state) { + // Record solution + record_solution(state, res); + } + // Traverse all choices + for &choice in choices.iter() { + // Pruning: check if the choice is valid + if is_valid(state, choice) { + // Attempt: make choice, update state + make_choice(state, choice.unwrap().clone()); + // Proceed to the next round of selection + backtrack( + state, + &vec![ + choice.unwrap().borrow().left.as_ref(), + choice.unwrap().borrow().right.as_ref(), + ], + res, + ); + // Backtrack: undo choice, restore to previous state + undo_choice(state, choice.unwrap().clone()); + } + } + } ``` === "C" ```c title="preorder_traversal_iii_template.c" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + bool isSolution(void) { + return pathSize > 0 && path[pathSize - 1]->val == 7; + } - [class]{}-[func]{recordSolution} + /* Record solution */ + void recordSolution(void) { + for (int i = 0; i < pathSize; i++) { + res[resSize][i] = path[i]; + } + resSize++; + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + bool isValid(TreeNode *choice) { + return choice != NULL && choice->val != 3; + } - [class]{}-[func]{makeChoice} + /* Update state */ + void makeChoice(TreeNode *choice) { + path[pathSize++] = choice; + } - [class]{}-[func]{undoChoice} + /* Restore state */ + void undoChoice(void) { + pathSize--; + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + void backtrack(TreeNode *choices[2]) { + // Check if it is a solution + if (isSolution()) { + // Record solution + recordSolution(); + } + // Traverse all choices + for (int i = 0; i < 2; i++) { + TreeNode *choice = choices[i]; + // Pruning: check if the choice is valid + if (isValid(choice)) { + // Attempt: make choice, update state + makeChoice(choice); + // Proceed to the next round of selection + TreeNode *nextChoices[2] = {choice->left, choice->right}; + backtrack(nextChoices); + // Backtrack: undo choice, restore to previous state + undoChoice(); + } + } + } ``` === "Kotlin" ```kotlin title="preorder_traversal_iii_template.kt" - [class]{}-[func]{isSolution} + /* Check if the current state is a solution */ + fun isSolution(state: MutableList): Boolean { + return state.isNotEmpty() && state[state.size - 1]?._val == 7 + } - [class]{}-[func]{recordSolution} + /* Record solution */ + fun recordSolution(state: MutableList?, res: MutableList?>) { + res.add(state!!.toMutableList()) + } - [class]{}-[func]{isValid} + /* Check if the choice is valid under the current state */ + fun isValid(state: MutableList?, choice: TreeNode?): Boolean { + return choice != null && choice._val != 3 + } - [class]{}-[func]{makeChoice} + /* Update state */ + fun makeChoice(state: MutableList, choice: TreeNode?) { + state.add(choice) + } - [class]{}-[func]{undoChoice} + /* Restore state */ + fun undoChoice(state: MutableList, choice: TreeNode?) { + state.removeLast() + } - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Example 3 */ + fun backtrack( + state: MutableList, + choices: MutableList, + res: MutableList?> + ) { + // Check if it is a solution + if (isSolution(state)) { + // Record solution + recordSolution(state, res) + } + // Traverse all choices + for (choice in choices) { + // Pruning: check if the choice is valid + if (isValid(state, choice)) { + // Attempt: make choice, update state + makeChoice(state, choice) + // Proceed to the next round of selection + backtrack(state, mutableListOf(choice!!.left, choice.right), res) + // Backtrack: undo choice, restore to previous state + undoChoice(state, choice) + } + } + } ``` === "Ruby" ```ruby title="preorder_traversal_iii_template.rb" - [class]{}-[func]{is_solution} + ### Check if current state is solution ### + def is_solution?(state) + !state.empty? && state.last.val == 7 + end - [class]{}-[func]{record_solution} + ### Record solution ### + def record_solution(state, res) + res << state.dup + end - [class]{}-[func]{is_valid} + ### Check if choice is valid in current state ### + def is_valid?(state, choice) + choice && choice.val != 3 + end - [class]{}-[func]{make_choice} + ### Update state ### + def make_choice(state, choice) + state << choice + end - [class]{}-[func]{undo_choice} + ### Restore state ### + def undo_choice(state, choice) + state.pop + end - [class]{}-[func]{backtrack} + ### Backtracking: example 3 ### + def backtrack(state, choices, res) + # Check if it is a solution + record_solution(state, res) if is_solution?(state) + + # Traverse all choices + for choice in choices + # Pruning: check if the choice is valid + if is_valid?(state, choice) + # Attempt: make choice, update state + make_choice(state, choice) + # Proceed to the next round of selection + backtrack(state, [choice.left, choice.right], res) + # Backtrack: undo choice, restore to previous state + undo_choice(state, choice) + end + end + end ``` -=== "Zig" +As per the problem statement, we should continue searching after finding a node with value $7$. **Therefore, we need to remove the `return` statement after recording the solution**. The following figure compares the search process with and without the `return` statement. - ```zig title="preorder_traversal_iii_template.zig" - [class]{}-[func]{isSolution} +![Comparison of search process with and without return statement](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } - [class]{}-[func]{recordSolution} +

Figure 13-4   Comparison of search process with and without return statement

- [class]{}-[func]{isValid} +Compared to code based on preorder traversal, code based on the backtracking algorithm framework appears more verbose, but has better generality. In fact, **many backtracking problems can be solved within this framework**. We only need to define `state` and `choices` for the specific problem and implement each method in the framework. - [class]{}-[func]{makeChoice} +## 13.1.4   Common Terminology - [class]{}-[func]{undoChoice} +To analyze algorithmic problems more clearly, we summarize the meanings of common terminology used in backtracking algorithms and provide corresponding examples from Example 3, as shown in the following table. - [class]{}-[func]{backtrack} - ``` - -As per the requirements, after finding a node with a value of $7$, the search should continue. **As a result, the `return` statement after recording the solution should be removed**. Figure 13-4 compares the search processes with and without retaining the `return` statement. - -![Comparison of retaining and removing the return in the search process](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } - -

Figure 13-4   Comparison of retaining and removing the return in the search process

- -Compared to the implementation based on pre-order traversal, the code using the backtracking algorithm framework seems verbose. However, it has better universality. In fact, **many backtracking problems can be solved within this framework**. We just need to define `state` and `choices` according to the specific problem and implement the methods in the framework. - -## 13.1.4   Common terminology - -To analyze algorithmic problems more clearly, we summarize the meanings of commonly used terminology in backtracking algorithms and provide corresponding examples from Example Three as shown in Table 13-1. - -

Table 13-1   Common backtracking algorithm terminology

+

Table 13-1   Common Backtracking Algorithm Terminology

-| Term | Definition | Example Three | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Solution | A solution is an answer that satisfies specific conditions of the problem, which may have one or more | All paths from the root node to node $7$ that meet the constraint | -| Constraint | Constraints are conditions in the problem that limit the feasibility of solutions, often used for pruning | Paths do not contain node $3$ | -| State | State represents the situation of the problem at a certain moment, including choices made | Current visited node path, i.e., `path` node list | -| Trial | A trial is the process of exploring the solution space based on available choices, including making choices, updating the state, and checking if it's a solution | Recursively visiting left (right) child nodes, adding nodes to `path`, checking if the node's value is $7$ | -| Retreat | Retreat refers to the action of undoing previous choices and returning to the previous state when encountering states that do not meet the constraints | When passing leaf nodes, ending node visits, encountering nodes with a value of $3$, terminating the search, and the recursion function returns | -| Prune | Prune is a method to avoid meaningless search paths based on the characteristics and constraints of the problem, which can enhance search efficiency | When encountering a node with a value of $3$, no further search is required | +| Term | Definition | Example 3 | +| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| Solution (solution) | A solution is an answer that satisfies the specific conditions of a problem; there may be one or more solutions | All paths from root to nodes with value $7$ that satisfy the constraint | +| Constraint (constraint) | A constraint is a condition in the problem that limits the feasibility of solutions, typically used for pruning | Paths do not contain nodes with value $3$ | +| State (state) | State represents the situation of a problem at a certain moment, including the choices already made | The currently visited node path, i.e., the `path` list of nodes | +| Attempt (attempt) | An attempt is the process of exploring the solution space according to available choices, including making choices, updating state, and checking if it is a solution | Recursively visit left (right) child nodes, add nodes to `path`, check if node value is $7$ | +| Backtrack (backtracking) | Backtracking refers to undoing previous choices and returning to a previous state when encountering a state that does not satisfy constraints | Stop searching when passing over leaf nodes, ending node visits, or encountering nodes with value $3$; function returns | +| Pruning (pruning) | Pruning is a method of avoiding meaningless search paths according to problem characteristics and constraints, which can improve search efficiency | When encountering a node with value $3$, do not continue searching |
!!! tip - Concepts like problems, solutions, states, etc., are universal, and are involved in divide and conquer, backtracking, dynamic programming, and greedy algorithms, among others. + The concepts of problem, solution, state, etc. are universal and are involved in divide-and-conquer, backtracking, dynamic programming, greedy and other algorithms. -## 13.1.5   Advantages and limitations +## 13.1.5   Advantages and Limitations -The backtracking algorithm is essentially a depth-first search algorithm that attempts all possible solutions until a satisfying solution is found. The advantage of this method is that it can find all possible solutions, and with reasonable pruning operations, it can be highly efficient. +The backtracking algorithm is essentially a depth-first search algorithm that tries all possible solutions until it finds one that satisfies the conditions. The advantage of this approach is that it can find all possible solutions, and with reasonable pruning operations, it achieves high efficiency. -However, when dealing with large-scale or complex problems, **the running efficiency of backtracking algorithm may not be acceptable**. +However, when dealing with large-scale or complex problems, **the running efficiency of the backtracking algorithm may be unacceptable**. -- **Time complexity**: Backtracking algorithms usually need to traverse all possible states in the state space, which can reach exponential or factorial time complexity. -- **Space complexity**: In recursive calls, it is necessary to save the current state (such as paths, auxiliary variables for pruning, etc.). When the depth is very large, the space need may become significantly bigger. +- **Time**: The backtracking algorithm usually needs to traverse all possibilities in the solution space, and the time complexity can reach exponential or factorial order. +- **Space**: During recursive calls, the current state needs to be saved (such as paths, auxiliary variables used for pruning, etc.), and when the depth is large, the space requirement can become very large. -Even so, **backtracking remains the best solution for certain search problems and constraint satisfaction problems**. For these problems, there is no way to predict which choices can generate valid solutions. We have to traverse all possible choices. In this case, **the key is about how to optimize the efficiency**. There are two common efficiency optimization methods. +Nevertheless, **the backtracking algorithm is still the best solution for certain search problems and constraint satisfaction problems**. For these problems, since we cannot predict which choices will generate valid solutions, we must traverse all possible choices. In this case, **the key is how to optimize efficiency**. There are two common efficiency optimization methods. -- **Prune**: Avoid searching paths that definitely will not produce a solution, thus saving time and space. -- **Heuristic search**: Introduce some strategies or estimates during the search process to prioritize the paths that are most likely to produce valid solutions. +- **Pruning**: Avoid searching paths that are guaranteed not to produce solutions, thereby saving time and space. +- **Heuristic search**: Introduce certain strategies or estimation values during the search process to prioritize searching paths that are most likely to produce valid solutions. -## 13.1.6   Typical backtracking problems +## 13.1.6   Typical Backtracking Examples -Backtracking algorithms can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems. +The backtracking algorithm can be used to solve many search problems, constraint satisfaction problems, and combinatorial optimization problems. -**Search problems**: The goal of these problems is to find solutions that meet specific conditions. +**Search problems**: The goal of these problems is to find solutions that satisfy specific conditions. -- Full permutation problem: Given a set, find all possible permutations and combinations of it. -- Subset sum problem: Given a set and a target sum, find all subsets of the set that sum to the target. -- Tower of Hanoi problem: Given three rods and a series of different-sized discs, the goal is to move all the discs from one rod to another, moving only one disc at a time, and never placing a larger disc on a smaller one. +- Permutation problem: Given a set, find all possible permutations and combinations. +- Subset sum problem: Given a set and a target sum, find all subsets in the set whose elements sum to the target. +- Tower of Hanoi: Given three pegs and a series of disks of different sizes, move all disks from one peg to another, moving only one disk at a time, and never placing a larger disk on a smaller disk. -**Constraint satisfaction problems**: The goal of these problems is to find solutions that satisfy all the constraints. +**Constraint satisfaction problems**: The goal of these problems is to find solutions that satisfy all constraints. -- $n$ queens: Place $n$ queens on an $n \times n$ chessboard so that they do not attack each other. -- Sudoku: Fill a $9 \times 9$ grid with the numbers $1$ to $9$, ensuring that the numbers do not repeat in each row, each column, and each $3 \times 3$ subgrid. -- Graph coloring problem: Given an undirected graph, color each vertex with the fewest possible colors so that adjacent vertices have different colors. +- N-Queens: Place $n$ queens on an $n \times n$ chessboard such that they do not attack each other. +- Sudoku: Fill numbers $1$ to $9$ in a $9 \times 9$ grid such that each row, column, and $3 \times 3$ subgrid contains no repeated digits. +- Graph coloring: Given an undirected graph, color each vertex with the minimum number of colors such that adjacent vertices have different colors. -**Combinatorial optimization problems**: The goal of these problems is to find the optimal solution within a combination space that meets certain conditions. +**Combinatorial optimization problems**: The goal of these problems is to find an optimal solution that satisfies certain conditions in a combinatorial space. -- 0-1 knapsack problem: Given a set of items and a backpack, each item has a certain value and weight. The goal is to choose items to maximize the total value within the backpack's capacity limit. -- Traveling salesman problem: In a graph, starting from one point, visit all other points exactly once and then return to the starting point, seeking the shortest path. -- Maximum clique problem: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge. +- 0-1 Knapsack: Given a set of items and a knapsack, each item has a value and weight. Under the knapsack capacity constraint, select items to maximize total value. +- Traveling Salesman Problem: Starting from a point in a graph, visit all other points exactly once and return to the starting point, finding the shortest path. +- Maximum Clique: Given an undirected graph, find the largest complete subgraph, i.e., a subgraph where any two vertices are connected by an edge. -Please note that for many combinatorial optimization problems, backtracking is not the optimal solution. +Note that for many combinatorial optimization problems, backtracking is not the optimal solution. -- The 0-1 knapsack problem is usually solved using dynamic programming to achieve higher time efficiency. -- The traveling salesman is a well-known NP-Hard problem, commonly solved using genetic algorithms and ant colony algorithms, among others. -- The maximum clique problem is a classic problem in graph theory, which can be solved using greedy algorithms and other heuristic methods. +- The 0-1 Knapsack problem is usually solved using dynamic programming to achieve higher time efficiency. +- The Traveling Salesman Problem is a famous NP-Hard problem; common solutions include genetic algorithms and ant colony algorithms. +- The Maximum Clique problem is a classical problem in graph theory and can be solved using heuristic algorithms such as greedy algorithms. diff --git a/en/docs/chapter_backtracking/index.md b/en/docs/chapter_backtracking/index.md index ec7a10f76..4f2c35f24 100644 --- a/en/docs/chapter_backtracking/index.md +++ b/en/docs/chapter_backtracking/index.md @@ -9,14 +9,14 @@ icon: material/map-marker-path !!! abstract - Like explorers in a maze, we may encounter obstacles on our path forward. + We are like explorers in a maze, and may encounter difficulties on the path forward. - The power of backtracking lets us begin anew, keep trying, and eventually find the exit leading to the light. + The power of backtracking allows us to start over, keep trying, and eventually find the exit leading to light. ## Chapter contents -- [13.1   Backtracking algorithms](backtracking_algorithm.md) -- [13.2   Permutation problem](permutations_problem.md) -- [13.3   Subset sum problem](subset_sum_problem.md) -- [13.4   n queens problem](n_queens_problem.md) +- [13.1   Backtracking Algorithm](backtracking_algorithm.md) +- [13.2   Permutations Problem](permutations_problem.md) +- [13.3   Subset-Sum Problem](subset_sum_problem.md) +- [13.4   N-Queens Problem](n_queens_problem.md) - [13.5   Summary](summary.md) diff --git a/en/docs/chapter_backtracking/n_queens_problem.md b/en/docs/chapter_backtracking/n_queens_problem.md index 12daa0f25..75b12ef73 100644 --- a/en/docs/chapter_backtracking/n_queens_problem.md +++ b/en/docs/chapter_backtracking/n_queens_problem.md @@ -2,59 +2,59 @@ comments: true --- -# 13.4   n queens problem +# 13.4   N-Queens Problem !!! question - According to the rules of chess, a queen can attack pieces in the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find arrangements where no two queens can attack each other. + According to the rules of chess, a queen can attack pieces that share the same row, column, or diagonal line. Given $n$ queens and an $n \times n$ chessboard, find a placement scheme such that no two queens can attack each other. -As shown in Figure 13-15, there are two solutions when $n = 4$. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, presenting all possible choices `choices`. The state of the chessboard `state` changes continuously as each queen is placed. +As shown in Figure 13-15, when $n = 4$, there are two solutions that can be found. From the perspective of the backtracking algorithm, an $n \times n$ chessboard has $n^2$ squares, which provide all the choices `choices`. During the process of placing queens one by one, the chessboard state changes continuously, and the chessboard at each moment represents the state `state`. -![Solution to the 4 queens problem](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } +![Solution to the 4-queens problem](n_queens_problem.assets/solution_4_queens.png){ class="animation-figure" } -

Figure 13-15   Solution to the 4 queens problem

+

Figure 13-15   Solution to the 4-queens problem

-Figure 13-16 shows the three constraints of this problem: **multiple queens cannot occupy the same row, column, or diagonal**. It is important to note that diagonals are divided into the main diagonal `\` and the secondary diagonal `/`. +Figure 13-16 illustrates the three constraints of this problem: **multiple queens cannot be in the same row, the same column, or on the same diagonal**. It is worth noting that diagonals are divided into two types: the main diagonal `\` and the anti-diagonal `/`. -![Constraints of the n queens problem](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" } +![Constraints of the n-queens problem](n_queens_problem.assets/n_queens_constraints.png){ class="animation-figure" } -

Figure 13-16   Constraints of the n queens problem

+

Figure 13-16   Constraints of the n-queens problem

-### 1.   Row-by-row placing strategy +### 1.   Row-By-Row Placement Strategy -As the number of queens equals the number of rows on the chessboard, both being $n$, it is easy to conclude that **each row on the chessboard allows and only allows one queen to be placed**. +Since both the number of queens and the number of rows on the chessboard are $n$, we can easily derive a conclusion: **each row of the chessboard allows and only allows exactly one queen to be placed**. -This means that we can adopt a row-by-row placing strategy: starting from the first row, place one queen per row until the last row is reached. +This means we can adopt a row-by-row placement strategy: starting from the first row, place one queen in each row until the last row is completed. -Figure 13-17 shows the row-by-row placing process for the 4 queens problem. Due to space limitations, the figure only expands one search branch of the first row, and prunes any placements that do not meet the column and diagonal constraints. +Figure 13-17 shows the row-by-row placement process for the 4-queens problem. Due to space limitations, the figure only expands one search branch of the first row, and all schemes that do not satisfy the column constraint and diagonal constraints are pruned. -![Row-by-row placing strategy](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" } +![Row-by-row placement strategy](n_queens_problem.assets/n_queens_placing.png){ class="animation-figure" } -

Figure 13-17   Row-by-row placing strategy

+

Figure 13-17   Row-by-row placement strategy

-Essentially, **the row-by-row placing strategy serves as a pruning function**, eliminating all search branches that would place multiple queens in the same row. +Essentially, **the row-by-row placement strategy serves a pruning function**, as it avoids all search branches where multiple queens appear in the same row. -### 2.   Column and diagonal pruning +### 2.   Column and Diagonal Pruning -To satisfy column constraints, we can use a boolean array `cols` of length $n$ to track whether a queen occupies each column. Before each placement decision, `cols` is used to prune the columns that already have queens, and it is dynamically updated during backtracking. +To satisfy the column constraint, we can use a boolean array `cols` of length $n$ to record whether each column has a queen. Before each placement decision, we use `cols` to prune columns that already have queens, and dynamically update the state of `cols` during backtracking. !!! tip - Note that the origin of the matrix is located in the upper left corner, where the row index increases from top to bottom, and the column index increases from left to right. + Please note that the origin of the matrix is located in the upper-left corner, where the row index increases from top to bottom, and the column index increases from left to right. -How about the diagonal constraints? Let the row and column indices of a certain cell on the chessboard be $(row, col)$. By selecting a specific main diagonal, we notice that the difference $row - col$ is the same for all cells on that diagonal, **meaning that $row - col$ is a constant value on the main diagonal**. +So how do we handle diagonal constraints? Consider a square on the chessboard with row and column indices $(row, col)$. If we select a specific main diagonal in the matrix, we find that all squares on that diagonal have the same difference between their row and column indices, **meaning that $row - col$ is a constant value for all squares on the main diagonal**. -In other words, if two cells satisfy $row_1 - col_1 = row_2 - col_2$, they are definitely on the same main diagonal. Using this pattern, we can utilize the array `diags1` shown in Figure 13-18 to track whether a queen is on any main diagonal. +In other words, if two squares satisfy $row_1 - col_1 = row_2 - col_2$, they must be on the same main diagonal. Using this pattern, we can use the array `diags1` shown in Figure 13-18 to record whether there is a queen on each main diagonal. -Similarly, **the sum of $row + col$ is a constant value for all cells on the secondary diagonal**. We can also use the array `diags2` to handle secondary diagonal constraints. +Similarly, **for all squares on an anti-diagonal, the sum $row + col$ is a constant value**. We can likewise use the array `diags2` to handle anti-diagonal constraints. ![Handling column and diagonal constraints](n_queens_problem.assets/n_queens_cols_diagonals.png){ class="animation-figure" }

Figure 13-18   Handling column and diagonal constraints

-### 3.   Code implementation +### 3.   Code Implementation -Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Consequently, the number of both main and secondary diagonals is $2n - 1$, meaning the length of the arrays `diags1` and `diags2` is $2n - 1$. +Please note that in an $n$-dimensional square matrix, the range of $row - col$ is $[-n + 1, n - 1]$, and the range of $row + col$ is $[0, 2n - 2]$. Therefore, the number of both main diagonals and anti-diagonals is $2n - 1$, meaning the length of both arrays `diags1` and `diags2` is $2n - 1$. === "Python" @@ -68,34 +68,34 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ diags1: list[bool], diags2: list[bool], ): - """Backtracking algorithm: n queens""" + """Backtracking algorithm: N queens""" # When all rows are placed, record the solution if row == n: res.append([list(row) for row in state]) return # Traverse all columns for col in range(n): - # Calculate the main and minor diagonals corresponding to the cell + # Calculate the main diagonal and anti-diagonal corresponding to this cell diag1 = row - col + n - 1 diag2 = row + col - # Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell + # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell if not cols[col] and not diags1[diag1] and not diags2[diag2]: - # Attempt: place the queen in the cell + # Attempt: place the queen in this cell state[row][col] = "Q" cols[col] = diags1[diag1] = diags2[diag2] = True # Place the next row backtrack(row + 1, n, state, res, cols, diags1, diags2) - # Retract: restore the cell to an empty spot + # Backtrack: restore this cell to an empty cell state[row][col] = "#" cols[col] = diags1[diag1] = diags2[diag2] = False def n_queens(n: int) -> list[list[list[str]]]: - """Solve n queens""" - # Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot + """Solve N queens""" + # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell state = [["#" for _ in range(n)] for _ in range(n)] - cols = [False] * n # Record columns with queens - diags1 = [False] * (2 * n - 1) # Record main diagonals with queens - diags2 = [False] * (2 * n - 1) # Record minor diagonals with queens + cols = [False] * n # Record whether there is a queen in the column + diags1 = [False] * (2 * n - 1) # Record whether there is a queen on the main diagonal + diags2 = [False] * (2 * n - 1) # Record whether there is a queen on the anti-diagonal res = [] backtrack(0, n, state, res, cols, diags1, diags2) @@ -105,7 +105,7 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ === "C++" ```cpp title="n_queens.cpp" - /* Backtracking algorithm: n queens */ + /* Backtracking algorithm: N queens */ void backtrack(int row, int n, vector> &state, vector>> &res, vector &cols, vector &diags1, vector &diags2) { // When all rows are placed, record the solution @@ -115,30 +115,30 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ } // Traverse all columns for (int col = 0; col < n; col++) { - // Calculate the main and minor diagonals corresponding to the cell + // Calculate the main diagonal and anti-diagonal corresponding to this cell int diag1 = row - col + n - 1; int diag2 = row + col; - // Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { - // Attempt: place the queen in the cell + // Attempt: place the queen in this cell state[row][col] = "Q"; cols[col] = diags1[diag1] = diags2[diag2] = true; // Place the next row backtrack(row + 1, n, state, res, cols, diags1, diags2); - // Retract: restore the cell to an empty spot + // Backtrack: restore this cell to an empty cell state[row][col] = "#"; cols[col] = diags1[diag1] = diags2[diag2] = false; } } } - /* Solve n queens */ + /* Solve N queens */ vector>> nQueens(int n) { - // Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell vector> state(n, vector(n, "#")); - vector cols(n, false); // Record columns with queens - vector diags1(2 * n - 1, false); // Record main diagonals with queens - vector diags2(2 * n - 1, false); // Record minor diagonals with queens + vector cols(n, false); // Record whether there is a queen in the column + vector diags1(2 * n - 1, false); // Record whether there is a queen on the main diagonal + vector diags2(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal vector>> res; backtrack(0, n, state, res, cols, diags1, diags2); @@ -150,7 +150,7 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ === "Java" ```java title="n_queens.java" - /* Backtracking algorithm: n queens */ + /* Backtracking algorithm: N queens */ void backtrack(int row, int n, List> state, List>> res, boolean[] cols, boolean[] diags1, boolean[] diags2) { // When all rows are placed, record the solution @@ -164,26 +164,26 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ } // Traverse all columns for (int col = 0; col < n; col++) { - // Calculate the main and minor diagonals corresponding to the cell + // Calculate the main diagonal and anti-diagonal corresponding to this cell int diag1 = row - col + n - 1; int diag2 = row + col; - // Pruning: do not allow queens on the column, main diagonal, or minor diagonal of the cell + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { - // Attempt: place the queen in the cell + // Attempt: place the queen in this cell state.get(row).set(col, "Q"); cols[col] = diags1[diag1] = diags2[diag2] = true; // Place the next row backtrack(row + 1, n, state, res, cols, diags1, diags2); - // Retract: restore the cell to an empty spot + // Backtrack: restore this cell to an empty cell state.get(row).set(col, "#"); cols[col] = diags1[diag1] = diags2[diag2] = false; } } } - /* Solve n queens */ + /* Solve N queens */ List>> nQueens(int n) { - // Initialize an n*n size chessboard, where 'Q' represents the queen and '#' represents an empty spot + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell List> state = new ArrayList<>(); for (int i = 0; i < n; i++) { List row = new ArrayList<>(); @@ -192,9 +192,9 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ } state.add(row); } - boolean[] cols = new boolean[n]; // Record columns with queens - boolean[] diags1 = new boolean[2 * n - 1]; // Record main diagonals with queens - boolean[] diags2 = new boolean[2 * n - 1]; // Record minor diagonals with queens + boolean[] cols = new boolean[n]; // Record whether there is a queen in the column + boolean[] diags1 = new boolean[2 * n - 1]; // Record whether there is a queen on the main diagonal + boolean[] diags2 = new boolean[2 * n - 1]; // Record whether there is a queen on the anti-diagonal List>> res = new ArrayList<>(); backtrack(0, n, state, res, cols, diags1, diags2); @@ -206,91 +206,544 @@ Please note, in an $n$-dimensional square matrix, the range of $row - col$ is $[ === "C#" ```csharp title="n_queens.cs" - [class]{n_queens}-[func]{Backtrack} + /* Backtracking algorithm: N queens */ + void Backtrack(int row, int n, List> state, List>> res, + bool[] cols, bool[] diags1, bool[] diags2) { + // When all rows are placed, record the solution + if (row == n) { + List> copyState = []; + foreach (List sRow in state) { + copyState.Add(new List(sRow)); + } + res.Add(copyState); + return; + } + // Traverse all columns + for (int col = 0; col < n; col++) { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + int diag1 = row - col + n - 1; + int diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // Attempt: place the queen in this cell + state[row][col] = "Q"; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // Place the next row + Backtrack(row + 1, n, state, res, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = "#"; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{n_queens}-[func]{NQueens} + /* Solve N queens */ + List>> NQueens(int n) { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + List> state = []; + for (int i = 0; i < n; i++) { + List row = []; + for (int j = 0; j < n; j++) { + row.Add("#"); + } + state.Add(row); + } + bool[] cols = new bool[n]; // Record whether there is a queen in the column + bool[] diags1 = new bool[2 * n - 1]; // Record whether there is a queen on the main diagonal + bool[] diags2 = new bool[2 * n - 1]; // Record whether there is a queen on the anti-diagonal + List>> res = []; + + Backtrack(0, n, state, res, cols, diags1, diags2); + + return res; + } ``` === "Go" ```go title="n_queens.go" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + func backtrack(row, n int, state *[][]string, res *[][][]string, cols, diags1, diags2 *[]bool) { + // When all rows are placed, record the solution + if row == n { + newState := make([][]string, len(*state)) + for i, _ := range newState { + newState[i] = make([]string, len((*state)[0])) + copy(newState[i], (*state)[i]) - [class]{}-[func]{nQueens} + } + *res = append(*res, newState) + return + } + // Traverse all columns + for col := 0; col < n; col++ { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + diag1 := row - col + n - 1 + diag2 := row + col + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if !(*cols)[col] && !(*diags1)[diag1] && !(*diags2)[diag2] { + // Attempt: place the queen in this cell + (*state)[row][col] = "Q" + (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = true, true, true + // Place the next row + backtrack(row+1, n, state, res, cols, diags1, diags2) + // Backtrack: restore this cell to an empty cell + (*state)[row][col] = "#" + (*cols)[col], (*diags1)[diag1], (*diags2)[diag2] = false, false, false + } + } + } + + /* Solve N queens */ + func nQueens(n int) [][][]string { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + 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 + } + // Record whether there is a queen in the column + 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + func backtrack(row: Int, n: Int, state: inout [[String]], res: inout [[[String]]], cols: inout [Bool], diags1: inout [Bool], diags2: inout [Bool]) { + // When all rows are placed, record the solution + if row == n { + res.append(state) + return + } + // Traverse all columns + for col in 0 ..< n { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + let diag1 = row - col + n - 1 + let diag2 = row + col + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if !cols[col] && !diags1[diag1] && !diags2[diag2] { + // Attempt: place the queen in this cell + state[row][col] = "Q" + cols[col] = true + diags1[diag1] = true + diags2[diag2] = true + // Place the next row + backtrack(row: row + 1, n: n, state: &state, res: &res, cols: &cols, diags1: &diags1, diags2: &diags2) + // Backtrack: restore this cell to an empty cell + state[row][col] = "#" + cols[col] = false + diags1[diag1] = false + diags2[diag2] = false + } + } + } - [class]{}-[func]{nQueens} + /* Solve N queens */ + func nQueens(n: Int) -> [[[String]]] { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + var state = Array(repeating: Array(repeating: "#", count: n), count: n) + var cols = Array(repeating: false, count: n) // Record whether there is a queen in the column + var diags1 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the main diagonal + var diags2 = Array(repeating: false, count: 2 * n - 1) // Record whether there is a queen on the anti-diagonal + 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + function backtrack(row, n, state, res, cols, diags1, diags2) { + // When all rows are placed, record the solution + if (row === n) { + res.push(state.map((row) => row.slice())); + return; + } + // Traverse all columns + for (let col = 0; col < n; col++) { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + const diag1 = row - col + n - 1; + const diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // Attempt: place the queen in this cell + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // Place the next row + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* Solve N queens */ + function nQueens(n) { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + const state = Array.from({ length: n }, () => Array(n).fill('#')); + const cols = Array(n).fill(false); // Record whether there is a queen in the column + const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal + const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal + const res = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + return res; + } ``` === "TS" ```typescript title="n_queens.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + function backtrack( + row: number, + n: number, + state: string[][], + res: string[][][], + cols: boolean[], + diags1: boolean[], + diags2: boolean[] + ): void { + // When all rows are placed, record the solution + if (row === n) { + res.push(state.map((row) => row.slice())); + return; + } + // Traverse all columns + for (let col = 0; col < n; col++) { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + const diag1 = row - col + n - 1; + const diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // Attempt: place the queen in this cell + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // Place the next row + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* Solve N queens */ + function nQueens(n: number): string[][][] { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + const state = Array.from({ length: n }, () => Array(n).fill('#')); + const cols = Array(n).fill(false); // Record whether there is a queen in the column + const diags1 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the main diagonal + const diags2 = Array(2 * n - 1).fill(false); // Record whether there is a queen on the anti-diagonal + const res: string[][][] = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + return res; + } ``` === "Dart" ```dart title="n_queens.dart" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + void backtrack( + int row, + int n, + List> state, + List>> res, + List cols, + List diags1, + List diags2, + ) { + // When all rows are placed, record the solution + if (row == n) { + List> copyState = []; + for (List sRow in state) { + copyState.add(List.from(sRow)); + } + res.add(copyState); + return; + } + // Traverse all columns + for (int col = 0; col < n; col++) { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + int diag1 = row - col + n - 1; + int diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // Attempt: place the queen in this cell + state[row][col] = "Q"; + cols[col] = true; + diags1[diag1] = true; + diags2[diag2] = true; + // Place the next row + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = "#"; + cols[col] = false; + diags1[diag1] = false; + diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* Solve N queens */ + List>> nQueens(int n) { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + List> state = List.generate(n, (index) => List.filled(n, "#")); + List cols = List.filled(n, false); // Record whether there is a queen in the column + List diags1 = List.filled(2 * n - 1, false); // Record whether there is a queen on the main diagonal + List diags2 = List.filled(2 * n - 1, false); // Record whether there is a queen on the anti-diagonal + List>> res = []; + + backtrack(0, n, state, res, cols, diags1, diags2); + + return res; + } ``` === "Rust" ```rust title="n_queens.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + fn backtrack( + row: usize, + n: usize, + state: &mut Vec>, + res: &mut Vec>>, + cols: &mut [bool], + diags1: &mut [bool], + diags2: &mut [bool], + ) { + // When all rows are placed, record the solution + if row == n { + res.push(state.clone()); + return; + } + // Traverse all columns + for col in 0..n { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + let diag1 = row + n - 1 - col; + let diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if !cols[col] && !diags1[diag1] && !diags2[diag2] { + // Attempt: place the queen in this cell + state[row][col] = "Q".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (true, true, true); + // Place the next row + backtrack(row + 1, n, state, res, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = "#".into(); + (cols[col], diags1[diag1], diags2[diag2]) = (false, false, false); + } + } + } - [class]{}-[func]{n_queens} + /* Solve N queens */ + fn n_queens(n: usize) -> Vec>> { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + let mut state: Vec> = vec![vec!["#".to_string(); n]; n]; + let mut cols = vec![false; n]; // Record whether there is a queen in the column + let mut diags1 = vec![false; 2 * n - 1]; // Record whether there is a queen on the main diagonal + let mut diags2 = vec![false; 2 * n - 1]; // Record whether there is a queen on the anti-diagonal + 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} + /* Backtracking algorithm: N queens */ + void backtrack(int row, int n, char state[MAX_SIZE][MAX_SIZE], char ***res, int *resSize, bool cols[MAX_SIZE], + bool diags1[2 * MAX_SIZE - 1], bool diags2[2 * MAX_SIZE - 1]) { + // When all rows are placed, record the solution + if (row == n) { + res[*resSize] = (char **)malloc(sizeof(char *) * n); + for (int i = 0; i < n; ++i) { + res[*resSize][i] = (char *)malloc(sizeof(char) * (n + 1)); + strcpy(res[*resSize][i], state[i]); + } + (*resSize)++; + return; + } + // Traverse all columns + for (int col = 0; col < n; col++) { + // Calculate the main diagonal and anti-diagonal corresponding to this cell + int diag1 = row - col + n - 1; + int diag2 = row + col; + // Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if (!cols[col] && !diags1[diag1] && !diags2[diag2]) { + // Attempt: place the queen in this cell + state[row][col] = 'Q'; + cols[col] = diags1[diag1] = diags2[diag2] = true; + // Place the next row + backtrack(row + 1, n, state, res, resSize, cols, diags1, diags2); + // Backtrack: restore this cell to an empty cell + state[row][col] = '#'; + cols[col] = diags1[diag1] = diags2[diag2] = false; + } + } + } - [class]{}-[func]{nQueens} + /* Solve N queens */ + char ***nQueens(int n, int *returnSize) { + char state[MAX_SIZE][MAX_SIZE]; + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + state[i][j] = '#'; + } + state[i][n] = '\0'; + } + bool cols[MAX_SIZE] = {false}; // Record whether there is a queen in the column + bool diags1[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the main diagonal + bool diags2[2 * MAX_SIZE - 1] = {false}; // Record whether there is a queen on the anti-diagonal + + char ***res = (char ***)malloc(sizeof(char **) * MAX_SIZE); + *returnSize = 0; + backtrack(0, n, state, res, returnSize, cols, diags1, diags2); + return res; + } ``` === "Kotlin" ```kotlin title="n_queens.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: N queens */ + fun backtrack( + row: Int, + n: Int, + state: MutableList>, + res: MutableList>?>, + cols: BooleanArray, + diags1: BooleanArray, + diags2: BooleanArray + ) { + // When all rows are placed, record the solution + if (row == n) { + val copyState = mutableListOf>() + for (sRow in state) { + copyState.add(sRow.toMutableList()) + } + res.add(copyState) + return + } + // Traverse all columns + for (col in 0..>?> { + // Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + val state = mutableListOf>() + for (i in 0..() + for (j in 0..>?>() + + backtrack(0, n, state, res, cols, diags1, diags2) + + return res + } ``` === "Ruby" ```ruby title="n_queens.rb" - [class]{}-[func]{backtrack} + ### Backtracking: n queens ### + def backtrack(row, n, state, res, cols, diags1, diags2) + # When all rows are placed, record the solution + if row == n + res << state.map { |row| row.dup } + return + end - [class]{}-[func]{n_queens} + # Traverse all columns + for col in 0...n + # Calculate the main diagonal and anti-diagonal corresponding to this cell + diag1 = row - col + n - 1 + diag2 = row + col + # Pruning: do not allow queens to exist in the column, main diagonal, and anti-diagonal of this cell + if !cols[col] && !diags1[diag1] && !diags2[diag2] + # Attempt: place the queen in this cell + state[row][col] = "Q" + cols[col] = diags1[diag1] = diags2[diag2] = true + # Place the next row + backtrack(row + 1, n, state, res, cols, diags1, diags2) + # Backtrack: restore this cell to an empty cell + state[row][col] = "#" + cols[col] = diags1[diag1] = diags2[diag2] = false + end + end + end + + ### Solve n queens ### + def n_queens(n) + # Initialize an n*n chessboard, where 'Q' represents a queen and '#' represents an empty cell + state = Array.new(n) { Array.new(n, "#") } + cols = Array.new(n, false) # Record whether there is a queen in the column + diags1 = Array.new(2 * n - 1, false) # Record whether there is a queen on the main diagonal + diags2 = Array.new(2 * n - 1, false) # Record whether there is a queen on the anti-diagonal + res = [] + backtrack(0, n, state, res, cols, diags1, diags2) + + res + end ``` -=== "Zig" +Placing $n$ queens row by row, considering the column constraint, from the first row to the last row there are $n$, $n-1$, $\dots$, $2$, $1$ choices, using $O(n!)$ time. When recording a solution, it is necessary to copy the matrix `state` and add it to `res`, and the copy operation uses $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can also significantly reduce the search space, so the search efficiency is often better than the time complexity mentioned above. - ```zig title="n_queens.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - -Placing $n$ queens row-by-row, considering column constraints, from the first row to the last row, there are $n$, $n-1$, $\dots$, $2$, $1$ choices, using $O(n!)$ time. When recording a solution, it is necessary to copy the matrix `state` and add it to `res`, with the copying operation using $O(n^2)$ time. Therefore, **the overall time complexity is $O(n! \cdot n^2)$**. In practice, pruning based on diagonal constraints can significantly reduce the search space, thus often the search efficiency is better than the aforementioned time complexity. - -Array `state` uses $O(n^2)$ space, and arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space as well. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**. +The array `state` uses $O(n^2)$ space, and the arrays `cols`, `diags1`, and `diags2` each use $O(n)$ space. The maximum recursion depth is $n$, using $O(n)$ stack frame space. Therefore, **the space complexity is $O(n^2)$**. diff --git a/en/docs/chapter_backtracking/permutations_problem.md b/en/docs/chapter_backtracking/permutations_problem.md index 54c2a5be8..f594c84ec 100644 --- a/en/docs/chapter_backtracking/permutations_problem.md +++ b/en/docs/chapter_backtracking/permutations_problem.md @@ -2,17 +2,17 @@ comments: true --- -# 13.2   Permutation problem +# 13.2   Permutations Problem -The permutation problem is a typical application of the backtracking algorithm. It involves finding all possible arrangements (permutations) of elements from a given set, such as an array or a string. +The permutations problem is a classic application of backtracking algorithms. It is defined as finding all possible arrangements of elements in a given collection (such as an array or string). -Table 13-2 shows several examples, including input arrays and their corresponding permutations. +Table 13-2 shows several example datasets, including input arrays and their corresponding permutations. -

Table 13-2   Permutation examples

+

Table 13-2   Permutations Examples

-| Input array | Permutations | +| Input Array | All Permutations | | :---------- | :----------------------------------------------------------------- | | $[1]$ | $[1]$ | | $[1, 2]$ | $[1, 2], [2, 1]$ | @@ -20,40 +20,40 @@ Table 13-2 shows several examples, including input arrays and their correspondin
-## 13.2.1   Cases without duplicate elements +## 13.2.1   Case with Distinct Elements !!! question Given an integer array with no duplicate elements, return all possible permutations. -From a backtracking perspective, **we can view the process of generating permutations as a series of choices.** Suppose the input array is $[1, 2, 3]$. If we choose $1$ first, then $3$, and finally $2$, we get the permutation $[1, 3, 2]$. "Backtracking" means undoing a previous choice and exploring alternative options. +From the perspective of backtracking algorithms, **we can imagine the process of generating permutations as the result of a series of choices**. Suppose the input array is $[1, 2, 3]$. If we first choose $1$, then choose $3$, and finally choose $2$, we obtain the permutation $[1, 3, 2]$. Backtracking means undoing a choice and then trying other choices. -From a coding perspective, the candidate set `choices` consists of all elements in the input array, while `state` holds the elements selected so far. Since each element can only be chosen once, **all elements in `state` must be unique**. +From the perspective of backtracking code, the candidate set `choices` consists of all elements in the input array, and the state `state` is the elements that have been chosen so far. Note that each element can only be chosen once, **therefore all elements in `state` should be unique**. -As illustrated in Figure 13-5, we can expand the search process into a recursive tree, where each node represents the current `state`. Starting from the root node, after three rounds of selections, we reach the leaf nodes—each corresponding to a permutation. +As shown in Figure 13-5, we can unfold the search process into a recursion tree, where each node in the tree represents the current state `state`. Starting from the root node, after three rounds of choices, we reach a leaf node, and each leaf node corresponds to a permutation. -![Permutation recursive tree](permutations_problem.assets/permutations_i.png){ class="animation-figure" } +![Recursion tree of permutations](permutations_problem.assets/permutations_i.png){ class="animation-figure" } -

Figure 13-5   Permutation recursive tree

+

Figure 13-5   Recursion tree of permutations

-### 1.   Repeated-choice pruning +### 1.   Pruning Duplicate Choices -To ensure each element is selected only once, we introduce a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We then base our pruning steps on this array: +To ensure that each element is chosen only once, we consider introducing a boolean array `selected`, where `selected[i]` indicates whether `choices[i]` has been chosen. We implement the following pruning operation based on it. -- After choosing `choice[i]`, set `selected[i]` to $\text{True}$ to mark it as chosen. -- While iterating through `choices`, skip all elements marked as chosen (i.e., prune those branches). +- After making a choice `choice[i]`, we set `selected[i]` to $\text{True}$, indicating that it has been chosen. +- When traversing the candidate list `choices`, we skip all nodes that have been chosen, which is pruning. -As shown in Figure 13-6, suppose we choose 1 in the first round, then 3 in the second round, and finally 2 in the third round. We need to prune the branch for element 1 in the second round and the branches for elements 1 and 3 in the third round. +As shown in Figure 13-6, suppose we choose $1$ in the first round, $3$ in the second round, and $2$ in the third round. Then we need to prune the branch of element $1$ in the second round and prune the branches of elements $1$ and $3$ in the third round. -![Permutation pruning example](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" } +![Pruning example of permutations](permutations_problem.assets/permutations_i_pruning.png){ class="animation-figure" } -

Figure 13-6   Permutation pruning example

+

Figure 13-6   Pruning example of permutations

-From the figure, we can see that this pruning process reduces the search space from $O(n^n)$ to $O(n!)$. +Observing the above figure, we find that this pruning operation reduces the search space size from $O(n^n)$ to $O(n!)$. -### 2.   Code implementation +### 2.   Code Implementation -With this understanding, we can "fill in the blanks" of our framework code. To keep the overall code concise, we won’t implement each part of the framework separately but instead expand everything in the `backtrack()` function: +After understanding the above information, we can fill in the blanks in the template code. To shorten the overall code, we do not implement each function in the template separately, but instead unfold them in the `backtrack()` function: === "Python" @@ -61,7 +61,7 @@ With this understanding, we can "fill in the blanks" of our framework code. To k def backtrack( state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] ): - """Backtracking algorithm: Permutation I""" + """Backtracking algorithm: Permutations I""" # When the state length equals the number of elements, record the solution if len(state) == len(choices): res.append(list(state)) @@ -70,17 +70,17 @@ With this understanding, we can "fill in the blanks" of our framework code. To k for i, choice in enumerate(choices): # Pruning: do not allow repeated selection of elements if not selected[i]: - # Attempt: make a choice, update the state + # Attempt: make choice, update state selected[i] = True state.append(choice) # Proceed to the next round of selection backtrack(state, choices, selected, res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state selected[i] = False state.pop() def permutations_i(nums: list[int]) -> list[list[int]]: - """Permutation I""" + """Permutations I""" res = [] backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) return res @@ -89,7 +89,7 @@ With this understanding, we can "fill in the blanks" of our framework code. To k === "C++" ```cpp title="permutations_i.cpp" - /* Backtracking algorithm: Permutation I */ + /* Backtracking algorithm: Permutations I */ void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { // When the state length equals the number of elements, record the solution if (state.size() == choices.size()) { @@ -101,19 +101,19 @@ With this understanding, we can "fill in the blanks" of our framework code. To k int choice = choices[i]; // Pruning: do not allow repeated selection of elements if (!selected[i]) { - // Attempt: make a choice, update the state + // Attempt: make choice, update state selected[i] = true; state.push_back(choice); // Proceed to the next round of selection backtrack(state, choices, selected, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state selected[i] = false; state.pop_back(); } } } - /* Permutation I */ + /* Permutations I */ vector> permutationsI(vector nums) { vector state; vector selected(nums.size(), false); @@ -126,7 +126,7 @@ With this understanding, we can "fill in the blanks" of our framework code. To k === "Java" ```java title="permutations_i.java" - /* Backtracking algorithm: Permutation I */ + /* Backtracking algorithm: Permutations I */ void backtrack(List state, int[] choices, boolean[] selected, List> res) { // When the state length equals the number of elements, record the solution if (state.size() == choices.length) { @@ -138,19 +138,19 @@ With this understanding, we can "fill in the blanks" of our framework code. To k int choice = choices[i]; // Pruning: do not allow repeated selection of elements if (!selected[i]) { - // Attempt: make a choice, update the state + // Attempt: make choice, update state selected[i] = true; state.add(choice); // Proceed to the next round of selection backtrack(state, choices, selected, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state selected[i] = false; state.remove(state.size() - 1); } } } - /* Permutation I */ + /* Permutations I */ List> permutationsI(int[] nums) { List> res = new ArrayList>(); backtrack(new ArrayList(), nums, new boolean[nums.length], res); @@ -161,122 +161,414 @@ With this understanding, we can "fill in the blanks" of our framework code. To k === "C#" ```csharp title="permutations_i.cs" - [class]{permutations_i}-[func]{Backtrack} + /* Backtracking algorithm: Permutations I */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // When the state length equals the number of elements, record the solution + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // Traverse all choices + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true; + state.Add(choice); + // Proceed to the next round of selection + Backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } - [class]{permutations_i}-[func]{PermutationsI} + /* Permutations I */ + List> PermutationsI(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } ``` === "Go" ```go title="permutations_i.go" - [class]{}-[func]{backtrackI} + /* Backtracking algorithm: Permutations I */ + func backtrackI(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // When the state length equals the number of elements, record the solution + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // Traverse all choices + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // Pruning: do not allow repeated selection of elements + if !(*selected)[i] { + // Attempt: make choice, update state + (*selected)[i] = true + *state = append(*state, choice) + // Proceed to the next round of selection + backtrackI(state, choices, selected, res) + // Backtrack: undo choice, restore to previous state + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } - [class]{}-[func]{permutationsI} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // When the state length equals the number of elements, record the solution + if state.count == choices.count { + res.append(state) + return + } + // Traverse all choices + for (i, choice) in choices.enumerated() { + // Pruning: do not allow repeated selection of elements + if !selected[i] { + // Attempt: make choice, update state + selected[i] = true + state.append(choice) + // Proceed to the next round of selection + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // Backtrack: undo choice, restore to previous state + selected[i] = false + state.removeLast() + } + } + } - [class]{}-[func]{permutationsI} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + function backtrack(state, choices, selected, res) { + // When the state length equals the number of elements, record the solution + if (state.length === choices.length) { + res.push([...state]); + return; + } + // Traverse all choices + choices.forEach((choice, i) => { + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsI} + /* Permutations I */ + function permutationsI(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "TS" ```typescript title="permutations_i.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // When the state length equals the number of elements, record the solution + if (state.length === choices.length) { + res.push([...state]); + return; + } + // Traverse all choices + choices.forEach((choice, i) => { + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsI} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // When the state length equals the number of elements, record the solution + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // Traverse all choices + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true; + state.add(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsI} + /* Permutations I */ + List> permutationsI(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" ```rust title="permutations_i.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // When the state length equals the number of elements, record the solution + if state.len() == choices.len() { + res.push(state); + return; + } + // Traverse all choices + for i in 0..choices.len() { + let choice = choices[i]; + // Pruning: do not allow repeated selection of elements + if !selected[i] { + // Attempt: make choice, update state + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state.clone(), choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + } + } - [class]{}-[func]{permutations_i} + /* Permutations I */ + fn permutations_i(nums: &mut [i32]) -> Vec> { + let mut res = Vec::new(); // State (subset) + backtrack(Vec::new(), nums, &mut vec![false; nums.len()], &mut res); + res + } ``` === "C" ```c title="permutations_i.c" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // When the state length equals the number of elements, record the solution + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // Traverse all choices + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true; + state[stateSize] = choice; + // Proceed to the next round of selection + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + } + } + } - [class]{}-[func]{permutationsI} + /* Permutations I */ + int **permutationsI(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } ``` === "Kotlin" ```kotlin title="permutations_i.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations I */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // When the state length equals the number of elements, record the solution + if (state.size == choices.size) { + res.add(state.toMutableList()) + return + } + // Traverse all choices + for (i in choices.indices) { + val choice = choices[i] + // Pruning: do not allow repeated selection of elements + if (!selected[i]) { + // Attempt: make choice, update state + selected[i] = true + state.add(choice) + // Proceed to the next round of selection + backtrack(state, choices, selected, res) + // Backtrack: undo choice, restore to previous state + selected[i] = false + state.removeAt(state.size - 1) + } + } + } - [class]{}-[func]{permutationsI} + /* Permutations I */ + fun permutationsI(nums: IntArray): MutableList?> { + val res = mutableListOf?>() + backtrack(mutableListOf(), nums, BooleanArray(nums.size), res) + return res + } ``` === "Ruby" ```ruby title="permutations_i.rb" - [class]{}-[func]{backtrack} + ### Backtracking: permutations I ### + def backtrack(state, choices, selected, res) + # When the state length equals the number of elements, record the solution + if state.length == choices.length + res << state.dup + return + end - [class]{}-[func]{permutations_i} + # Traverse all choices + choices.each_with_index do |choice, i| + # Pruning: do not allow repeated selection of elements + unless selected[i] + # Attempt: make choice, update state + selected[i] = true + state << choice + # Proceed to the next round of selection + backtrack(state, choices, selected, res) + # Backtrack: undo choice, restore to previous state + selected[i] = false + state.pop + end + end + end + + ### Permutations I ### + def permutations_i(nums) + res = [] + backtrack([], nums, Array.new(nums.length, false), res) + res + end ``` -=== "Zig" - - ```zig title="permutations_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsI} - ``` - -## 13.2.2   Considering duplicate elements +## 13.2.2   Case with Duplicate Elements !!! question - Given an integer array**that may contain duplicate elements**, return all unique permutations. + Given an integer array that **may contain duplicate elements**, return all unique permutations. -Suppose the input array is $[1, 1, 2]$. To distinguish between the two identical elements $1$, we label the second one as $\hat{1}$. +Suppose the input array is $[1, 1, 2]$. To distinguish the two duplicate elements $1$, we denote the second $1$ as $\hat{1}$. -As shown in Figure 13-7, half of the permutations produced by this method are duplicates: +As shown in Figure 13-7, the method described above generates permutations where half are duplicates. ![Duplicate permutations](permutations_problem.assets/permutations_ii.png){ class="animation-figure" }

Figure 13-7   Duplicate permutations

-So how can we eliminate these duplicate permutations? One direct approach is to use a hash set to remove duplicates after generating all permutations. However, this is less elegant **because branches that produce duplicates are inherently unnecessary and should be pruned in advance,** thus improving the algorithm’s efficiency. +So how do we remove duplicate permutations? The most direct approach is to use a hash set to directly deduplicate the permutation results. However, this is not elegant because **the search branches that generate duplicate permutations are unnecessary and should be identified and pruned early**, which can further improve algorithm efficiency. -### 1.   Equal-element pruning +### 1.   Pruning Duplicate Elements -Looking at Figure 13-8, in the first round, choosing $1$ or $\hat{1}$ leads to the same permutations, so we prune $\hat{1}$. +Observe Figure 13-8. In the first round, choosing $1$ or choosing $\hat{1}$ is equivalent. All permutations generated under these two choices are duplicates. Therefore, we should prune $\hat{1}$. -Similarly, after choosing $2$ in the first round, choosing $1$ or $\hat{1}$ in the second round also leads to duplicate branches, so we prune $\hat{1}$ then as well. +Similarly, after choosing $2$ in the first round, the $1$ and $\hat{1}$ in the second round also produce duplicate branches, so the second round's $\hat{1}$ should also be pruned. -Essentially, **our goal is to ensure that multiple identical elements are only selected once per round of choices.** +Essentially, **our goal is to ensure that multiple equal elements are chosen only once in a certain round of choices**. -![Duplicate permutations pruning](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } +![Pruning duplicate permutations](permutations_problem.assets/permutations_ii_pruning.png){ class="animation-figure" } -

Figure 13-8   Duplicate permutations pruning

+

Figure 13-8   Pruning duplicate permutations

-### 2.   Code implementation +### 2.   Code Implementation -Based on the code from the previous problem, we introduce a hash set `duplicated` in each round. This set keeps track of elements we have already attempted, so we can prune duplicates: +Building on the code from the previous problem, we consider opening a hash set `duplicated` in each round of choices to record which elements have been tried in this round, and prune duplicate elements: === "Python" @@ -284,7 +576,7 @@ Based on the code from the previous problem, we introduce a hash set `duplicated def backtrack( state: list[int], choices: list[int], selected: list[bool], res: list[list[int]] ): - """Backtracking algorithm: Permutation II""" + """Backtracking algorithm: Permutations II""" # When the state length equals the number of elements, record the solution if len(state) == len(choices): res.append(list(state)) @@ -294,18 +586,18 @@ Based on the code from the previous problem, we introduce a hash set `duplicated for i, choice in enumerate(choices): # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements if not selected[i] and choice not in duplicated: - # Attempt: make a choice, update the state - duplicated.add(choice) # Record selected element values + # Attempt: make choice, update state + duplicated.add(choice) # Record the selected element value selected[i] = True state.append(choice) # Proceed to the next round of selection backtrack(state, choices, selected, res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state selected[i] = False state.pop() def permutations_ii(nums: list[int]) -> list[list[int]]: - """Permutation II""" + """Permutations II""" res = [] backtrack(state=[], choices=nums, selected=[False] * len(nums), res=res) return res @@ -314,7 +606,7 @@ Based on the code from the previous problem, we introduce a hash set `duplicated === "C++" ```cpp title="permutations_ii.cpp" - /* Backtracking algorithm: Permutation II */ + /* Backtracking algorithm: Permutations II */ void backtrack(vector &state, const vector &choices, vector &selected, vector> &res) { // When the state length equals the number of elements, record the solution if (state.size() == choices.size()) { @@ -327,20 +619,20 @@ Based on the code from the previous problem, we introduce a hash set `duplicated int choice = choices[i]; // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements if (!selected[i] && duplicated.find(choice) == duplicated.end()) { - // Attempt: make a choice, update the state - duplicated.emplace(choice); // Record selected element values + // Attempt: make choice, update state + duplicated.emplace(choice); // Record the selected element value selected[i] = true; state.push_back(choice); // Proceed to the next round of selection backtrack(state, choices, selected, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state selected[i] = false; state.pop_back(); } } } - /* Permutation II */ + /* Permutations II */ vector> permutationsII(vector nums) { vector state; vector selected(nums.size(), false); @@ -353,7 +645,7 @@ Based on the code from the previous problem, we introduce a hash set `duplicated === "Java" ```java title="permutations_ii.java" - /* Backtracking algorithm: Permutation II */ + /* Backtracking algorithm: Permutations II */ void backtrack(List state, int[] choices, boolean[] selected, List> res) { // When the state length equals the number of elements, record the solution if (state.size() == choices.length) { @@ -366,20 +658,20 @@ Based on the code from the previous problem, we introduce a hash set `duplicated int choice = choices[i]; // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements if (!selected[i] && !duplicated.contains(choice)) { - // Attempt: make a choice, update the state - duplicated.add(choice); // Record selected element values + // Attempt: make choice, update state + duplicated.add(choice); // Record the selected element value selected[i] = true; state.add(choice); // Proceed to the next round of selection backtrack(state, choices, selected, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state selected[i] = false; state.remove(state.size() - 1); } } } - /* Permutation II */ + /* Permutations II */ List> permutationsII(int[] nums) { List> res = new ArrayList>(); backtrack(new ArrayList(), nums, new boolean[nums.length], res); @@ -390,104 +682,417 @@ Based on the code from the previous problem, we introduce a hash set `duplicated === "C#" ```csharp title="permutations_ii.cs" - [class]{permutations_ii}-[func]{Backtrack} + /* Backtracking algorithm: Permutations II */ + void Backtrack(List state, int[] choices, bool[] selected, List> res) { + // When the state length equals the number of elements, record the solution + if (state.Count == choices.Length) { + res.Add(new List(state)); + return; + } + // Traverse all choices + HashSet duplicated = []; + for (int i = 0; i < choices.Length; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated.Contains(choice)) { + // Attempt: make choice, update state + duplicated.Add(choice); // Record the selected element value + selected[i] = true; + state.Add(choice); + // Proceed to the next round of selection + Backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.RemoveAt(state.Count - 1); + } + } + } - [class]{permutations_ii}-[func]{PermutationsII} + /* Permutations II */ + List> PermutationsII(int[] nums) { + List> res = []; + Backtrack([], nums, new bool[nums.Length], res); + return res; + } ``` === "Go" ```go title="permutations_ii.go" - [class]{}-[func]{backtrackII} + /* Backtracking algorithm: Permutations II */ + func backtrackII(state *[]int, choices *[]int, selected *[]bool, res *[][]int) { + // When the state length equals the number of elements, record the solution + if len(*state) == len(*choices) { + newState := append([]int{}, *state...) + *res = append(*res, newState) + } + // Traverse all choices + duplicated := make(map[int]struct{}, 0) + for i := 0; i < len(*choices); i++ { + choice := (*choices)[i] + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if _, ok := duplicated[choice]; !ok && !(*selected)[i] { + // Attempt: make choice, update state + // Record the selected element value + duplicated[choice] = struct{}{} + (*selected)[i] = true + *state = append(*state, choice) + // Proceed to the next round of selection + backtrackII(state, choices, selected, res) + // Backtrack: undo choice, restore to previous state + (*selected)[i] = false + *state = (*state)[:len(*state)-1] + } + } + } - [class]{}-[func]{permutationsII} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + func backtrack(state: inout [Int], choices: [Int], selected: inout [Bool], res: inout [[Int]]) { + // When the state length equals the number of elements, record the solution + if state.count == choices.count { + res.append(state) + return + } + // Traverse all choices + var duplicated: Set = [] + for (i, choice) in choices.enumerated() { + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if !selected[i], !duplicated.contains(choice) { + // Attempt: make choice, update state + duplicated.insert(choice) // Record the selected element value + selected[i] = true + state.append(choice) + // Proceed to the next round of selection + backtrack(state: &state, choices: choices, selected: &selected, res: &res) + // Backtrack: undo choice, restore to previous state + selected[i] = false + state.removeLast() + } + } + } - [class]{}-[func]{permutationsII} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + function backtrack(state, choices, selected, res) { + // When the state length equals the number of elements, record the solution + if (state.length === choices.length) { + res.push([...state]); + return; + } + // Traverse all choices + const duplicated = new Set(); + choices.forEach((choice, i) => { + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated.has(choice)) { + // Attempt: make choice, update state + duplicated.add(choice); // Record the selected element value + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsII} + /* Permutations II */ + function permutationsII(nums) { + const res = []; + backtrack([], nums, Array(nums.length).fill(false), res); + return res; + } ``` === "TS" ```typescript title="permutations_ii.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + function backtrack( + state: number[], + choices: number[], + selected: boolean[], + res: number[][] + ): void { + // When the state length equals the number of elements, record the solution + if (state.length === choices.length) { + res.push([...state]); + return; + } + // Traverse all choices + const duplicated = new Set(); + choices.forEach((choice, i) => { + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated.has(choice)) { + // Attempt: make choice, update state + duplicated.add(choice); // Record the selected element value + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + }); + } - [class]{}-[func]{permutationsII} + /* Permutations 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" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + void backtrack( + List state, + List choices, + List selected, + List> res, + ) { + // When the state length equals the number of elements, record the solution + if (state.length == choices.length) { + res.add(List.from(state)); + return; + } + // Traverse all choices + Set duplicated = {}; + for (int i = 0; i < choices.length; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated.contains(choice)) { + // Attempt: make choice, update state + duplicated.add(choice); // Record the selected element value + selected[i] = true; + state.add(choice); + // Proceed to the next round of selection + backtrack(state, choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.removeLast(); + } + } + } - [class]{}-[func]{permutationsII} + /* Permutations II */ + List> permutationsII(List nums) { + List> res = []; + backtrack([], nums, List.filled(nums.length, false), res); + return res; + } ``` === "Rust" ```rust title="permutations_ii.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + fn backtrack(mut state: Vec, choices: &[i32], selected: &mut [bool], res: &mut Vec>) { + // When the state length equals the number of elements, record the solution + if state.len() == choices.len() { + res.push(state); + return; + } + // Traverse all choices + let mut duplicated = HashSet::::new(); + for i in 0..choices.len() { + let choice = choices[i]; + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if !selected[i] && !duplicated.contains(&choice) { + // Attempt: make choice, update state + duplicated.insert(choice); // Record the selected element value + selected[i] = true; + state.push(choice); + // Proceed to the next round of selection + backtrack(state.clone(), choices, selected, res); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + state.pop(); + } + } + } - [class]{}-[func]{permutations_ii} + /* Permutations 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} + /* Backtracking algorithm: Permutations II */ + void backtrack(int *state, int stateSize, int *choices, int choicesSize, bool *selected, int **res, int *resSize) { + // When the state length equals the number of elements, record the solution + if (stateSize == choicesSize) { + res[*resSize] = (int *)malloc(choicesSize * sizeof(int)); + for (int i = 0; i < choicesSize; i++) { + res[*resSize][i] = state[i]; + } + (*resSize)++; + return; + } + // Traverse all choices + bool duplicated[MAX_SIZE] = {false}; + for (int i = 0; i < choicesSize; i++) { + int choice = choices[i]; + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated[choice]) { + // Attempt: make choice, update state + duplicated[choice] = true; // Record the selected element value + selected[i] = true; + state[stateSize] = choice; + // Proceed to the next round of selection + backtrack(state, stateSize + 1, choices, choicesSize, selected, res, resSize); + // Backtrack: undo choice, restore to previous state + selected[i] = false; + } + } + } - [class]{}-[func]{permutationsII} + /* Permutations II */ + int **permutationsII(int *nums, int numsSize, int *returnSize) { + int *state = (int *)malloc(numsSize * sizeof(int)); + bool *selected = (bool *)malloc(numsSize * sizeof(bool)); + for (int i = 0; i < numsSize; i++) { + selected[i] = false; + } + int **res = (int **)malloc(MAX_SIZE * sizeof(int *)); + *returnSize = 0; + + backtrack(state, 0, nums, numsSize, selected, res, returnSize); + + free(state); + free(selected); + + return res; + } ``` === "Kotlin" ```kotlin title="permutations_ii.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Permutations II */ + fun backtrack( + state: MutableList, + choices: IntArray, + selected: BooleanArray, + res: MutableList?> + ) { + // When the state length equals the number of elements, record the solution + if (state.size == choices.size) { + res.add(state.toMutableList()) + return + } + // Traverse all choices + val duplicated = HashSet() + for (i in choices.indices) { + val choice = choices[i] + // Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if (!selected[i] && !duplicated.contains(choice)) { + // Attempt: make choice, update state + duplicated.add(choice) // Record the selected element value + selected[i] = true + state.add(choice) + // Proceed to the next round of selection + backtrack(state, choices, selected, res) + // Backtrack: undo choice, restore to previous state + selected[i] = false + state.removeAt(state.size - 1) + } + } + } - [class]{}-[func]{permutationsII} + /* Permutations II */ + fun permutationsII(nums: IntArray): MutableList?> { + val res = mutableListOf?>() + backtrack(mutableListOf(), nums, BooleanArray(nums.size), res) + return res + } ``` === "Ruby" ```ruby title="permutations_ii.rb" - [class]{}-[func]{backtrack} + ### Backtracking: permutations II ### + def backtrack(state, choices, selected, res) + # When the state length equals the number of elements, record the solution + if state.length == choices.length + res << state.dup + return + end - [class]{}-[func]{permutations_ii} + # Traverse all choices + duplicated = Set.new + choices.each_with_index do |choice, i| + # Pruning: do not allow repeated selection of elements and do not allow repeated selection of equal elements + if !selected[i] && !duplicated.include?(choice) + # Attempt: make choice, update state + duplicated.add(choice) + selected[i] = true + state << choice + # Proceed to the next round of selection + backtrack(state, choices, selected, res) + # Backtrack: undo choice, restore to previous state + selected[i] = false + state.pop + end + end + end + + ### Permutations II ### + def permutations_ii(nums) + res = [] + backtrack([], nums, Array.new(nums.length, false), res) + res + end ``` -=== "Zig" +Assuming elements are pairwise distinct, there are $n!$ (factorial) permutations of $n$ elements. When recording results, we need to copy a list of length $n$, using $O(n)$ time. **Therefore, the time complexity is $O(n! \cdot n)$**. - ```zig title="permutations_ii.zig" - [class]{}-[func]{backtrack} +The maximum recursion depth is $n$, using $O(n)$ stack frame space. `selected` uses $O(n)$ space. At most $n$ `duplicated` sets exist simultaneously, using $O(n^2)$ space. **Therefore, the space complexity is $O(n^2)$**. - [class]{}-[func]{permutationsII} - ``` +### 3.   Comparison of Two Pruning Methods -Assuming all elements are distinct, there are $n!$ (factorial) permutations of $n$ elements. Recording each result requires copying a list of length $n$, which takes $O(n)$ time. **Hence, the total time complexity is $O(n!n)$.** +Note that although both `selected` and `duplicated` are used for pruning, they have different objectives. -The maximum recursion depth is $n$, using $O(n)$ stack space. The `selected` array also requires $O(n)$ space. Because there can be up to $n$ separate `duplicated` sets at any one time, they collectively occupy $O(n^2)$ space. **Therefore, the space complexity is $O(n^2)$.** +- **Pruning duplicate choices**: There is only one `selected` throughout the entire search process. It records which elements are included in the current state, and its purpose is to prevent an element from appearing repeatedly in `state`. +- **Pruning duplicate elements**: Each round of choices (each `backtrack` function call) contains a `duplicated` set. It records which elements have been chosen in this round's iteration (the `for` loop), and its purpose is to ensure that equal elements are chosen only once. -### 3.   Comparing the two pruning methods +Figure 13-9 shows the effective scope of the two pruning conditions. Note that each node in the tree represents a choice, and the nodes on the path from the root to a leaf node form a permutation. -Although both `selected` and `duplicated` serve as pruning mechanisms, they target different issues: +![Effective scope of two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" } -- **Repeated-choice pruning**(via `selected`): There is a single `selected` array for the entire search, indicating which elements are already in the current state. This prevents the same element from appearing more than once in `state`. -- **Equal-element pruning**(via `duplicated`): Each call to the `backtrack` function uses its own `duplicated` set, recording which elements have already been chosen in that specific iteration (`for` loop). This ensures that equal elements are selected only once per round of choices. - -Figure 13-9 shows the scope of these two pruning strategies. Each node in the tree represents a choice; the path from the root to any leaf corresponds to one complete permutation. - -![Scope of the two pruning conditions](permutations_problem.assets/permutations_ii_pruning_summary.png){ class="animation-figure" } - -

Figure 13-9   Scope of the two pruning conditions

+

Figure 13-9   Effective scope of two pruning conditions

diff --git a/en/docs/chapter_backtracking/subset_sum_problem.md b/en/docs/chapter_backtracking/subset_sum_problem.md index 05bacefbe..4fe4bf7ac 100644 --- a/en/docs/chapter_backtracking/subset_sum_problem.md +++ b/en/docs/chapter_backtracking/subset_sum_problem.md @@ -2,24 +2,24 @@ comments: true --- -# 13.3   Subset sum problem +# 13.3   Subset-Sum Problem -## 13.3.1   Case without duplicate elements +## 13.3.1   Without Duplicate Elements !!! question - Given an array of positive integers `nums` and a target positive integer `target`, find all possible combinations such that the sum of the elements in the combination equals `target`. The given array has no duplicate elements, and each element can be chosen multiple times. Please return these combinations as a list, which should not contain duplicate combinations. + Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. The given array has no duplicate elements, and each element can be selected multiple times. Return these combinations in list form, where the list should not contain duplicate combinations. -For example, for the input set $\{3, 4, 5\}$ and target integer $9$, the solutions are $\{3, 3, 3\}, \{4, 5\}$. Note the following two points. +For example, given the set $\{3, 4, 5\}$ and target integer $9$, the solutions are $\{3, 3, 3\}, \{4, 5\}$. Note the following two points: -- Elements in the input set can be chosen an unlimited number of times. -- Subsets do not distinguish the order of elements, for example $\{4, 5\}$ and $\{5, 4\}$ are the same subset. +- Elements in the input set can be selected repeatedly without limit. +- Subsets do not distinguish element order; for example, $\{4, 5\}$ and $\{5, 4\}$ are the same subset. -### 1.   Reference permutation solution +### 1.   Reference to Full Permutation Solution -Similar to the permutation problem, we can imagine the generation of subsets as a series of choices, updating the "element sum" in real-time during the choice process. When the element sum equals `target`, the subset is recorded in the result list. +Similar to the full permutation problem, we can imagine the process of generating subsets as a series of choices, and update the "sum of elements" in real-time during the selection process. When the sum equals `target`, we record the subset to the result list. -Unlike the permutation problem, **elements in this problem can be chosen an unlimited number of times**, thus there is no need to use a `selected` boolean list to record whether an element has been chosen. We can make minor modifications to the permutation code to initially solve the problem: +Unlike the full permutation problem, **elements in this problem's set can be selected unlimited times**, so we do not need to use a `selected` boolean list to track whether an element has been selected. We can make minor modifications to the full permutation code and initially obtain the solution: === "Python" @@ -31,25 +31,25 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli choices: list[int], res: list[list[int]], ): - """Backtracking algorithm: Subset Sum I""" + """Backtracking algorithm: Subset sum I""" # When the subset sum equals target, record the solution if total == target: res.append(list(state)) return # Traverse all choices for i in range(len(choices)): - # Pruning: if the subset sum exceeds target, skip that choice + # Pruning: if the subset sum exceeds target, skip this choice if total + choices[i] > target: continue - # Attempt: make a choice, update elements and total + # Attempt: make choice, update element sum total state.append(choices[i]) # Proceed to the next round of selection backtrack(state, target, total + choices[i], choices, res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state state.pop() def subset_sum_i_naive(nums: list[int], target: int) -> list[list[int]]: - """Solve Subset Sum I (including duplicate subsets)""" + """Solve subset sum I (including duplicate subsets)""" state = [] # State (subset) total = 0 # Subset sum res = [] # Result list (subset list) @@ -60,7 +60,7 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli === "C++" ```cpp title="subset_sum_i_naive.cpp" - /* Backtracking algorithm: Subset Sum I */ + /* Backtracking algorithm: Subset sum I */ void backtrack(vector &state, int target, int total, vector &choices, vector> &res) { // When the subset sum equals target, record the solution if (total == target) { @@ -69,20 +69,20 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli } // Traverse all choices for (size_t i = 0; i < choices.size(); i++) { - // Pruning: if the subset sum exceeds target, skip that choice + // Pruning: if the subset sum exceeds target, skip this choice if (total + choices[i] > target) { continue; } - // Attempt: make a choice, update elements and total + // Attempt: make choice, update element sum total state.push_back(choices[i]); // Proceed to the next round of selection backtrack(state, target, total + choices[i], choices, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.pop_back(); } } - /* Solve Subset Sum I (including duplicate subsets) */ + /* Solve subset sum I (including duplicate subsets) */ vector> subsetSumINaive(vector &nums, int target) { vector state; // State (subset) int total = 0; // Subset sum @@ -95,7 +95,7 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli === "Java" ```java title="subset_sum_i_naive.java" - /* Backtracking algorithm: Subset Sum I */ + /* Backtracking algorithm: Subset sum I */ void backtrack(List state, int target, int total, int[] choices, List> res) { // When the subset sum equals target, record the solution if (total == target) { @@ -104,20 +104,20 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli } // Traverse all choices for (int i = 0; i < choices.length; i++) { - // Pruning: if the subset sum exceeds target, skip that choice + // Pruning: if the subset sum exceeds target, skip this choice if (total + choices[i] > target) { continue; } - // Attempt: make a choice, update elements and total + // Attempt: make choice, update element sum total state.add(choices[i]); // Proceed to the next round of selection backtrack(state, target, total + choices[i], choices, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.remove(state.size() - 1); } } - /* Solve Subset Sum I (including duplicate subsets) */ + /* Solve subset sum I (including duplicate subsets) */ List> subsetSumINaive(int[] nums, int target) { List state = new ArrayList<>(); // State (subset) int total = 0; // Subset sum @@ -130,131 +130,417 @@ Unlike the permutation problem, **elements in this problem can be chosen an unli === "C#" ```csharp title="subset_sum_i_naive.cs" - [class]{subset_sum_i_naive}-[func]{Backtrack} + /* Backtracking algorithm: Subset sum I */ + void Backtrack(List state, int target, int total, int[] choices, List> res) { + // When the subset sum equals target, record the solution + if (total == target) { + res.Add(new List(state)); + return; + } + // Traverse all choices + for (int i = 0; i < choices.Length; i++) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue; + } + // Attempt: make choice, update element sum total + state.Add(choices[i]); + // Proceed to the next round of selection + Backtrack(state, target, total + choices[i], choices, res); + // Backtrack: undo choice, restore to previous state + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_i_naive}-[func]{SubsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + List> SubsetSumINaive(int[] nums, int target) { + List state = []; // State (subset) + int total = 0; // Subset sum + List> res = []; // Result list (subset list) + Backtrack(state, target, total, nums, res); + return res; + } ``` === "Go" ```go title="subset_sum_i_naive.go" - [class]{}-[func]{backtrackSubsetSumINaive} + /* Backtracking algorithm: Subset sum I */ + func backtrackSubsetSumINaive(total, target int, state, choices *[]int, res *[][]int) { + // When the subset sum equals target, record the solution + if target == total { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // Traverse all choices + for i := 0; i < len(*choices); i++ { + // Pruning: if the subset sum exceeds target, skip this choice + if total+(*choices)[i] > target { + continue + } + // Attempt: make choice, update element sum total + *state = append(*state, (*choices)[i]) + // Proceed to the next round of selection + backtrackSubsetSumINaive(total+(*choices)[i], target, state, choices, res) + // Backtrack: undo choice, restore to previous state + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + func subsetSumINaive(nums []int, target int) [][]int { + state := make([]int, 0) // State (subset) + total := 0 // Subset sum + res := make([][]int, 0) // Result list (subset list) + backtrackSubsetSumINaive(total, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_i_naive.swift" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + func backtrack(state: inout [Int], target: Int, total: Int, choices: [Int], res: inout [[Int]]) { + // When the subset sum equals target, record the solution + if total == target { + res.append(state) + return + } + // Traverse all choices + for i in choices.indices { + // Pruning: if the subset sum exceeds target, skip this choice + if total + choices[i] > target { + continue + } + // Attempt: make choice, update element sum total + state.append(choices[i]) + // Proceed to the next round of selection + backtrack(state: &state, target: target, total: total + choices[i], choices: choices, res: &res) + // Backtrack: undo choice, restore to previous state + state.removeLast() + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + func subsetSumINaive(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // State (subset) + let total = 0 // Subset sum + var res: [[Int]] = [] // Result list (subset list) + backtrack(state: &state, target: target, total: total, choices: nums, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_i_naive.js" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + function backtrack(state, target, total, choices, res) { + // When the subset sum equals target, record the solution + if (total === target) { + res.push([...state]); + return; + } + // Traverse all choices + for (let i = 0; i < choices.length; i++) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue; + } + // Attempt: make choice, update element sum total + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + function subsetSumINaive(nums, target) { + const state = []; // State (subset) + const total = 0; // Subset sum + const res = []; // Result list (subset list) + backtrack(state, target, total, nums, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_i_naive.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + function backtrack( + state: number[], + target: number, + total: number, + choices: number[], + res: number[][] + ): void { + // When the subset sum equals target, record the solution + if (total === target) { + res.push([...state]); + return; + } + // Traverse all choices + for (let i = 0; i < choices.length; i++) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue; + } + // Attempt: make choice, update element sum total + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + function subsetSumINaive(nums: number[], target: number): number[][] { + const state = []; // State (subset) + const total = 0; // Subset sum + const res = []; // Result list (subset list) + backtrack(state, target, total, nums, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_i_naive.dart" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + void backtrack( + List state, + int target, + int total, + List choices, + List> res, + ) { + // When the subset sum equals target, record the solution + if (total == target) { + res.add(List.from(state)); + return; + } + // Traverse all choices + for (int i = 0; i < choices.length; i++) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue; + } + // Attempt: make choice, update element sum total + state.add(choices[i]); + // Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res); + // Backtrack: undo choice, restore to previous state + state.removeLast(); + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + List> subsetSumINaive(List nums, int target) { + List state = []; // State (subset) + int total = 0; // Sum of elements + List> res = []; // Result list (subset list) + backtrack(state, target, total, nums, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_i_naive.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + fn backtrack( + state: &mut Vec, + target: i32, + total: i32, + choices: &[i32], + res: &mut Vec>, + ) { + // When the subset sum equals target, record the solution + if total == target { + res.push(state.clone()); + return; + } + // Traverse all choices + for i in 0..choices.len() { + // Pruning: if the subset sum exceeds target, skip this choice + if total + choices[i] > target { + continue; + } + // Attempt: make choice, update element sum total + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subset_sum_i_naive} + /* Solve subset sum I (including duplicate subsets) */ + fn subset_sum_i_naive(nums: &[i32], target: i32) -> Vec> { + let mut state = Vec::new(); // State (subset) + let total = 0; // Subset sum + let mut res = Vec::new(); // Result list (subset list) + backtrack(&mut state, target, total, nums, &mut res); + res + } ``` === "C" ```c title="subset_sum_i_naive.c" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + void backtrack(int target, int total, int *choices, int choicesSize) { + // When the subset sum equals target, record the solution + if (total == target) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // Traverse all choices + for (int i = 0; i < choicesSize; i++) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue; + } + // Attempt: make choice, update element sum total + state[stateSize++] = choices[i]; + // Proceed to the next round of selection + backtrack(target, total + choices[i], choices, choicesSize); + // Backtrack: undo choice, restore to previous state + stateSize--; + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + void subsetSumINaive(int *nums, int numsSize, int target) { + resSize = 0; // Initialize solution count to 0 + backtrack(target, 0, nums, numsSize); + } ``` === "Kotlin" ```kotlin title="subset_sum_i_naive.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + fun backtrack( + state: MutableList, + target: Int, + total: Int, + choices: IntArray, + res: MutableList?> + ) { + // When the subset sum equals target, record the solution + if (total == target) { + res.add(state.toMutableList()) + return + } + // Traverse all choices + for (i in choices.indices) { + // Pruning: if the subset sum exceeds target, skip this choice + if (total + choices[i] > target) { + continue + } + // Attempt: make choice, update element sum total + state.add(choices[i]) + // Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res) + // Backtrack: undo choice, restore to previous state + state.removeAt(state.size - 1) + } + } - [class]{}-[func]{subsetSumINaive} + /* Solve subset sum I (including duplicate subsets) */ + fun subsetSumINaive(nums: IntArray, target: Int): MutableList?> { + val state = mutableListOf() // State (subset) + val total = 0 // Subset sum + val res = mutableListOf?>() // Result list (subset list) + backtrack(state, target, total, nums, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_i_naive.rb" - [class]{}-[func]{backtrack} + ### Backtracking: subset sum I ### + def backtrack(state, target, total, choices, res) + # When the subset sum equals target, record the solution + if total == target + res << state.dup + return + end - [class]{}-[func]{subset_sum_i_naive} + # Traverse all choices + for i in 0...choices.length + # Pruning: if the subset sum exceeds target, skip this choice + next if total + choices[i] > target + # Attempt: make choice, update element sum total + state << choices[i] + # Proceed to the next round of selection + backtrack(state, target, total + choices[i], choices, res) + # Backtrack: undo choice, restore to previous state + state.pop + end + end + + ### Solve subset sum I (with duplicate subsets) ### + def subset_sum_i_naive(nums, target) + state = [] # State (subset) + total = 0 # Subset sum + res = [] # Result list (subset list) + backtrack(state, target, total, nums, res) + res + end ``` -=== "Zig" +When we input array $[3, 4, 5]$ and target element $9$ to the above code, the output is $[3, 3, 3], [4, 5], [5, 4]$. **Although we successfully find all subsets that sum to $9$, there are duplicate subsets $[4, 5]$ and $[5, 4]$**. - ```zig title="subset_sum_i_naive.zig" - [class]{}-[func]{backtrack} +This is because the search process distinguishes the order of selections, but subsets do not distinguish selection order. As shown in Figure 13-10, selecting 4 first and then 5 versus selecting 5 first and then 4 are different branches, but they correspond to the same subset. - [class]{}-[func]{subsetSumINaive} - ``` +![Subset search and boundary pruning](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" } -Inputting the array $[3, 4, 5]$ and target element $9$ into the above code yields the results $[3, 3, 3], [4, 5], [5, 4]$. **Although it successfully finds all subsets with a sum of $9$, it includes the duplicate subset $[4, 5]$ and $[5, 4]$**. +

Figure 13-10   Subset search and boundary pruning

-This is because the search process distinguishes the order of choices, however, subsets do not distinguish the choice order. As shown in Figure 13-10, choosing $4$ before $5$ and choosing $5$ before $4$ are different branches, but correspond to the same subset. +To eliminate duplicate subsets, **one straightforward idea is to deduplicate the result list**. However, this approach is very inefficient for two reasons: -![Subset search and pruning out of bounds](subset_sum_problem.assets/subset_sum_i_naive.png){ class="animation-figure" } +- When there are many array elements, especially when `target` is large, the search process generates many duplicate subsets. +- Comparing subsets (arrays) is very time-consuming, requiring sorting the arrays first, then comparing each element in them. -

Figure 13-10   Subset search and pruning out of bounds

+### 2.   Pruning Duplicate Subsets -To eliminate duplicate subsets, **a straightforward idea is to deduplicate the result list**. However, this method is very inefficient for two reasons. +**We consider deduplication through pruning during the search process**. Observing Figure 13-11, duplicate subsets occur when array elements are selected in different orders, as in the following cases: -- When there are many array elements, especially when `target` is large, the search process produces a large number of duplicate subsets. -- Comparing subsets (arrays) for differences is very time-consuming, requiring arrays to be sorted first, then comparing the differences of each element in the arrays. +1. When the first and second rounds select $3$ and $4$ respectively, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$. +2. Afterward, when the first round selects $4$, **the second round should skip $3$**, because the subset $[4, 3, \dots]$ generated by this choice is completely duplicate with the subset generated in step `1.` -### 2.   Duplicate subset pruning +In the search process, each level's choices are tried from left to right, so the rightmost branches are pruned more. -**We consider deduplication during the search process through pruning**. Observing Figure 13-11, duplicate subsets are generated when choosing array elements in different orders, for example in the following situations. +1. The first two rounds select $3$ and $5$, generating subset $[3, 5, \dots]$. +2. The first two rounds select $4$ and $5$, generating subset $[4, 5, \dots]$. +3. If the first round selects $5$, **the second round should skip $3$ and $4$**, because subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ are completely duplicate with the subsets described in steps `1.` and `2.` -1. When choosing $3$ in the first round and $4$ in the second round, all subsets containing these two elements are generated, denoted as $[3, 4, \dots]$. -2. Later, when $4$ is chosen in the first round, **the second round should skip $3$** because the subset $[4, 3, \dots]$ generated by this choice completely duplicates the subset from step `1.`. +![Different selection orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } -In the search process, each layer's choices are tried one by one from left to right, so the more to the right a branch is, the more it is pruned. +

Figure 13-11   Different selection orders leading to duplicate subsets

-1. First two rounds choose $3$ and $5$, generating subset $[3, 5, \dots]$. -2. First two rounds choose $4$ and $5$, generating subset $[4, 5, \dots]$. -3. If $5$ is chosen in the first round, **then the second round should skip $3$ and $4$** as the subsets $[5, 3, \dots]$ and $[5, 4, \dots]$ completely duplicate the subsets described in steps `1.` and `2.`. +In summary, given an input array $[x_1, x_2, \dots, x_n]$, let the selection sequence in the search process be $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$. This selection sequence must satisfy $i_1 \leq i_2 \leq \dots \leq i_m$; **any selection sequence that does not satisfy this condition will cause duplicates and should be pruned**. -![Different choice orders leading to duplicate subsets](subset_sum_problem.assets/subset_sum_i_pruning.png){ class="animation-figure" } +### 3.   Code Implementation -

Figure 13-11   Different choice orders leading to duplicate subsets

+To implement this pruning, we initialize a variable `start` to indicate the starting point of traversal. **After making choice $x_{i}$, set the next round to start traversal from index $i$**. This ensures that the selection sequence satisfies $i_1 \leq i_2 \leq \dots \leq i_m$, guaranteeing subset uniqueness. -In summary, given the input array $[x_1, x_2, \dots, x_n]$, the choice sequence in the search process should be $[x_{i_1}, x_{i_2}, \dots, x_{i_m}]$, which needs to satisfy $i_1 \leq i_2 \leq \dots \leq i_m$. **Any choice sequence that does not meet this condition will cause duplicates and should be pruned**. +In addition, we have made the following two optimizations to the code: -### 3.   Code implementation - -To implement this pruning, we initialize the variable `start`, which indicates the starting point for traversal. **After making the choice $x_{i}$, set the next round to start from index $i$**. This will ensure the choice sequence satisfies $i_1 \leq i_2 \leq \dots \leq i_m$, thereby ensuring the uniqueness of the subsets. - -Besides, we have made the following two optimizations to the code. - -- Before starting the search, sort the array `nums`. In the traversal of all choices, **end the loop directly when the subset sum exceeds `target`** as subsequent elements are larger and their subset sum will definitely exceed `target`. -- Eliminate the element sum variable `total`, **by performing subtraction on `target` to count the element sum**. When `target` equals $0$, record the solution. +- Before starting the search, first sort the array `nums`. When traversing all choices, **end the loop immediately when the subset sum exceeds `target`**, because subsequent elements are larger, and their subset sums must exceed `target`. +- Omit the element sum variable `total` and **use subtraction on `target` to track the sum of elements**. Record the solution when `target` equals $0$. === "Python" @@ -262,27 +548,27 @@ Besides, we have made the following two optimizations to the code. def backtrack( state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] ): - """Backtracking algorithm: Subset Sum I""" + """Backtracking algorithm: Subset sum I""" # When the subset sum equals target, record the solution if target == 0: res.append(list(state)) return # Traverse all choices - # Pruning two: start traversing from start to avoid generating duplicate subsets + # Pruning 2: start traversing from start to avoid generating duplicate subsets for i in range(start, len(choices)): - # Pruning one: if the subset sum exceeds target, end the loop immediately + # Pruning 1: if the subset sum exceeds target, end the loop directly # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if target - choices[i] < 0: break - # Attempt: make a choice, update target, start + # Attempt: make choice, update target, start state.append(choices[i]) # Proceed to the next round of selection backtrack(state, target - choices[i], choices, i, res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state state.pop() def subset_sum_i(nums: list[int], target: int) -> list[list[int]]: - """Solve Subset Sum I""" + """Solve subset sum I""" state = [] # State (subset) nums.sort() # Sort nums start = 0 # Start point for traversal @@ -294,7 +580,7 @@ Besides, we have made the following two optimizations to the code. === "C++" ```cpp title="subset_sum_i.cpp" - /* Backtracking algorithm: Subset Sum I */ + /* Backtracking algorithm: Subset sum I */ void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { // When the subset sum equals target, record the solution if (target == 0) { @@ -302,23 +588,23 @@ Besides, we have made the following two optimizations to the code. return; } // Traverse all choices - // Pruning two: start traversing from start to avoid generating duplicate subsets + // Pruning 2: start traversing from start to avoid generating duplicate subsets for (int i = start; i < choices.size(); i++) { - // Pruning one: if the subset sum exceeds target, end the loop immediately + // Pruning 1: if the subset sum exceeds target, end the loop directly // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if (target - choices[i] < 0) { break; } - // Attempt: make a choice, update target, start + // Attempt: make choice, update target, start state.push_back(choices[i]); // Proceed to the next round of selection backtrack(state, target - choices[i], choices, i, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.pop_back(); } } - /* Solve Subset Sum I */ + /* Solve subset sum I */ vector> subsetSumI(vector &nums, int target) { vector state; // State (subset) sort(nums.begin(), nums.end()); // Sort nums @@ -332,7 +618,7 @@ Besides, we have made the following two optimizations to the code. === "Java" ```java title="subset_sum_i.java" - /* Backtracking algorithm: Subset Sum I */ + /* Backtracking algorithm: Subset sum I */ void backtrack(List state, int target, int[] choices, int start, List> res) { // When the subset sum equals target, record the solution if (target == 0) { @@ -340,23 +626,23 @@ Besides, we have made the following two optimizations to the code. return; } // Traverse all choices - // Pruning two: start traversing from start to avoid generating duplicate subsets + // Pruning 2: start traversing from start to avoid generating duplicate subsets for (int i = start; i < choices.length; i++) { - // Pruning one: if the subset sum exceeds target, end the loop immediately + // Pruning 1: if the subset sum exceeds target, end the loop directly // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if (target - choices[i] < 0) { break; } - // Attempt: make a choice, update target, start + // Attempt: make choice, update target, start state.add(choices[i]); // Proceed to the next round of selection backtrack(state, target - choices[i], choices, i, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.remove(state.size() - 1); } } - /* Solve Subset Sum I */ + /* Solve subset sum I */ List> subsetSumI(int[] nums, int target) { List state = new ArrayList<>(); // State (subset) Arrays.sort(nums); // Sort nums @@ -370,118 +656,434 @@ Besides, we have made the following two optimizations to the code. === "C#" ```csharp title="subset_sum_i.cs" - [class]{subset_sum_i}-[func]{Backtrack} + /* Backtracking algorithm: Subset sum I */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.Add(new List(state)); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (int i = start; i < choices.Length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Attempt: make choice, update target, start + state.Add(choices[i]); + // Proceed to the next round of selection + Backtrack(state, target - choices[i], choices, i, res); + // Backtrack: undo choice, restore to previous state + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_i}-[func]{SubsetSumI} + /* Solve subset sum I */ + List> SubsetSumI(int[] nums, int target) { + List state = []; // State (subset) + Array.Sort(nums); // Sort nums + int start = 0; // Start point for traversal + List> res = []; // Result list (subset list) + Backtrack(state, target, nums, start, res); + return res; + } ``` === "Go" ```go title="subset_sum_i.go" - [class]{}-[func]{backtrackSubsetSumI} + /* Backtracking algorithm: Subset sum I */ + func backtrackSubsetSumI(start, target int, state, choices *[]int, res *[][]int) { + // When the subset sum equals target, record the solution + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for i := start; i < len(*choices); i++ { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target-(*choices)[i] < 0 { + break + } + // Attempt: make choice, update target, start + *state = append(*state, (*choices)[i]) + // Proceed to the next round of selection + backtrackSubsetSumI(i, target-(*choices)[i], state, choices, res) + // Backtrack: undo choice, restore to previous state + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + func subsetSumI(nums []int, target int) [][]int { + state := make([]int, 0) // State (subset) + sort.Ints(nums) // Sort nums + start := 0 // Start point for traversal + res := make([][]int, 0) // Result list (subset list) + backtrackSubsetSumI(start, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_i.swift" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // When the subset sum equals target, record the solution + if target == 0 { + res.append(state) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for i in choices.indices.dropFirst(start) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target - choices[i] < 0 { + break + } + // Attempt: make choice, update target, start + state.append(choices[i]) + // Proceed to the next round of selection + backtrack(state: &state, target: target - choices[i], choices: choices, start: i, res: &res) + // Backtrack: undo choice, restore to previous state + state.removeLast() + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + func subsetSumI(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // State (subset) + let nums = nums.sorted() // Sort nums + let start = 0 // Start point for traversal + var res: [[Int]] = [] // Result list (subset list) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_i.js" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + function backtrack(state, target, choices, start, res) { + // When the subset sum equals target, record the solution + if (target === 0) { + res.push([...state]); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (let i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + function subsetSumI(nums, target) { + const state = []; // State (subset) + nums.sort((a, b) => a - b); // Sort nums + const start = 0; // Start point for traversal + const res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_i.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // When the subset sum equals target, record the solution + if (target === 0) { + res.push([...state]); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (let i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + function subsetSumI(nums: number[], target: number): number[][] { + const state = []; // State (subset) + nums.sort((a, b) => a - b); // Sort nums + const start = 0; // Start point for traversal + const res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_i.dart" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.add(List.from(state)); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (int i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Attempt: make choice, update target, start + state.add(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i, res); + // Backtrack: undo choice, restore to previous state + state.removeLast(); + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + List> subsetSumI(List nums, int target) { + List state = []; // State (subset) + nums.sort(); // Sort nums + int start = 0; // Start point for traversal + List> res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_i.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + fn backtrack( + state: &mut Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // When the subset sum equals target, record the solution + if target == 0 { + res.push(state.clone()); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for i in start..choices.len() { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target - choices[i] < 0 { + break; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subset_sum_i} + /* Solve subset sum I */ + fn subset_sum_i(nums: &mut [i32], target: i32) -> Vec> { + let mut state = Vec::new(); // State (subset) + nums.sort(); // Sort nums + let start = 0; // Start point for traversal + let mut res = Vec::new(); // Result list (subset list) + backtrack(&mut state, target, nums, start, &mut res); + res + } ``` === "C" ```c title="subset_sum_i.c" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // When the subset sum equals target, record the solution + if (target == 0) { + for (int i = 0; i < stateSize; ++i) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (int i = start; i < choicesSize; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Attempt: make choice, update target, start + state[stateSize] = choices[i]; + stateSize++; + // Proceed to the next round of selection + backtrack(target - choices[i], choices, choicesSize, i); + // Backtrack: undo choice, restore to previous state + stateSize--; + } + } - [class]{}-[func]{subsetSumI} + /* Solve subset sum I */ + void subsetSumI(int *nums, int numsSize, int target) { + qsort(nums, numsSize, sizeof(int), cmp); // Sort nums + int start = 0; // Start point for traversal + backtrack(target, nums, numsSize, start); + } ``` === "Kotlin" ```kotlin title="subset_sum_i.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum I */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.add(state.toMutableList()) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + for (i in start..?> { + val state = mutableListOf() // State (subset) + nums.sort() // Sort nums + val start = 0 // Start point for traversal + val res = mutableListOf?>() // Result list (subset list) + backtrack(state, target, nums, start, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_i.rb" - [class]{}-[func]{backtrack} + ### Backtracking: subset sum I ### + def backtrack(state, target, choices, start, res) + # When the subset sum equals target, record the solution + if target.zero? + res << state.dup + return + end + # Traverse all choices + # Pruning 2: start traversing from start to avoid generating duplicate subsets + for i in start...choices.length + # Pruning 1: if the subset sum exceeds target, end the loop directly + # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + break if target - choices[i] < 0 + # Attempt: make choice, update target, start + state << choices[i] + # Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i, res) + # Backtrack: undo choice, restore to previous state + state.pop + end + end - [class]{}-[func]{subset_sum_i} + ### Solve subset sum I ### + def subset_sum_i(nums, target) + state = [] # State (subset) + nums.sort! # Sort nums + start = 0 # Start point for traversal + res = [] # Result list (subset list) + backtrack(state, target, nums, start, res) + res + end ``` -=== "Zig" +Figure 13-12 shows the complete backtracking process when array $[3, 4, 5]$ and target element $9$ are input to the above code. - ```zig title="subset_sum_i.zig" - [class]{}-[func]{backtrack} +![Subset-sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } - [class]{}-[func]{subsetSumI} - ``` +

Figure 13-12   Subset-sum I backtracking process

-Figure 13-12 shows the overall backtracking process after inputting the array $[3, 4, 5]$ and target element $9$ into the above code. - -![Subset sum I backtracking process](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } - -

Figure 13-12   Subset sum I backtracking process

- -## 13.3.2   Considering cases with duplicate elements +## 13.3.2   With Duplicate Elements in Array !!! question - Given an array of positive integers `nums` and a target positive integer `target`, find all possible combinations such that the sum of the elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can only be chosen once**. Please return these combinations as a list, which should not contain duplicate combinations. + Given a positive integer array `nums` and a target positive integer `target`, find all possible combinations where the sum of elements in the combination equals `target`. **The given array may contain duplicate elements, and each element can be selected at most once**. Return these combinations in list form, where the list should not contain duplicate combinations. -Compared to the previous question, **this question's input array may contain duplicate elements**, introducing new problems. For example, given the array $[4, \hat{4}, 5]$ and target element $9$, the existing code's output results in $[4, 5], [\hat{4}, 5]$, resulting in duplicate subsets. +Compared to the previous problem, **the input array in this problem may contain duplicate elements**, which introduces new challenges. For example, given array $[4, \hat{4}, 5]$ and target element $9$, the output of the existing code is $[4, 5], [\hat{4}, 5]$, which contains duplicate subsets. -**The reason for this duplication is that equal elements are chosen multiple times in a certain round**. In Figure 13-13, the first round has three choices, two of which are $4$, generating two duplicate search branches, thus outputting duplicate subsets; similarly, the two $4$s in the second round also produce duplicate subsets. +**The reason for this duplication is that equal elements are selected multiple times in a certain round**. In Figure 13-13, the first round has three choices, two of which are $4$, creating two duplicate search branches that output duplicate subsets. Similarly, the two $4$'s in the second round also produce duplicate subsets. ![Duplicate subsets caused by equal elements](subset_sum_problem.assets/subset_sum_ii_repeat.png){ class="animation-figure" }

Figure 13-13   Duplicate subsets caused by equal elements

-### 1.   Equal element pruning +### 1.   Pruning Equal Elements -To solve this issue, **we need to limit equal elements to being chosen only once per round**. The implementation is quite clever: since the array is sorted, equal elements are adjacent. This means that in a certain round of choices, if the current element is equal to its left-hand element, it means it has already been chosen, so skip the current element directly. +To solve this problem, **we need to limit equal elements to be selected only once in each round**. The implementation is quite clever: since the array is already sorted, equal elements are adjacent. This means that in a certain round of selection, if the current element equals the element to its left, it means this element has already been selected, so we skip the current element directly. -At the same time, **this question stipulates that each array element can only be chosen once**. Fortunately, we can also use the variable `start` to meet this constraint: after making the choice $x_{i}$, set the next round to start from index $i + 1$ going forward. This not only eliminates duplicate subsets but also avoids repeated selection of elements. +At the same time, **this problem specifies that each array element can only be selected once**. Fortunately, we can also use the variable `start` to satisfy this constraint: after making choice $x_{i}$, set the next round to start traversal from index $i + 1$ onwards. This both eliminates duplicate subsets and avoids selecting elements multiple times. -### 2.   Code implementation +### 2.   Code Implementation === "Python" @@ -489,31 +1091,31 @@ At the same time, **this question stipulates that each array element can only be def backtrack( state: list[int], target: int, choices: list[int], start: int, res: list[list[int]] ): - """Backtracking algorithm: Subset Sum II""" + """Backtracking algorithm: Subset sum II""" # When the subset sum equals target, record the solution if target == 0: res.append(list(state)) return # Traverse all choices - # Pruning two: start traversing from start to avoid generating duplicate subsets - # Pruning three: start traversing from start to avoid repeatedly selecting the same element + # Pruning 2: start traversing from start to avoid generating duplicate subsets + # Pruning 3: start traversing from start to avoid repeatedly selecting the same element for i in range(start, len(choices)): - # Pruning one: if the subset sum exceeds target, end the loop immediately + # Pruning 1: if the subset sum exceeds target, end the loop directly # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if target - choices[i] < 0: break - # Pruning four: if the element equals the left element, it indicates that the search branch is repeated, skip it + # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly if i > start and choices[i] == choices[i - 1]: continue - # Attempt: make a choice, update target, start + # Attempt: make choice, update target, start state.append(choices[i]) # Proceed to the next round of selection backtrack(state, target - choices[i], choices, i + 1, res) - # Retract: undo the choice, restore to the previous state + # Backtrack: undo choice, restore to previous state state.pop() def subset_sum_ii(nums: list[int], target: int) -> list[list[int]]: - """Solve Subset Sum II""" + """Solve subset sum II""" state = [] # State (subset) nums.sort() # Sort nums start = 0 # Start point for traversal @@ -525,7 +1127,7 @@ At the same time, **this question stipulates that each array element can only be === "C++" ```cpp title="subset_sum_ii.cpp" - /* Backtracking algorithm: Subset Sum II */ + /* Backtracking algorithm: Subset sum II */ void backtrack(vector &state, int target, vector &choices, int start, vector> &res) { // When the subset sum equals target, record the solution if (target == 0) { @@ -533,28 +1135,28 @@ At the same time, **this question stipulates that each array element can only be return; } // Traverse all choices - // Pruning two: start traversing from start to avoid generating duplicate subsets - // Pruning three: start traversing from start to avoid repeatedly selecting the same element + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element for (int i = start; i < choices.size(); i++) { - // Pruning one: if the subset sum exceeds target, end the loop immediately + // Pruning 1: if the subset sum exceeds target, end the loop directly // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if (target - choices[i] < 0) { break; } - // Pruning four: if the element equals the left element, it indicates that the search branch is repeated, skip it + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly if (i > start && choices[i] == choices[i - 1]) { continue; } - // Attempt: make a choice, update target, start + // Attempt: make choice, update target, start state.push_back(choices[i]); // Proceed to the next round of selection backtrack(state, target - choices[i], choices, i + 1, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.pop_back(); } } - /* Solve Subset Sum II */ + /* Solve subset sum II */ vector> subsetSumII(vector &nums, int target) { vector state; // State (subset) sort(nums.begin(), nums.end()); // Sort nums @@ -568,7 +1170,7 @@ At the same time, **this question stipulates that each array element can only be === "Java" ```java title="subset_sum_ii.java" - /* Backtracking algorithm: Subset Sum II */ + /* Backtracking algorithm: Subset sum II */ void backtrack(List state, int target, int[] choices, int start, List> res) { // When the subset sum equals target, record the solution if (target == 0) { @@ -576,28 +1178,28 @@ At the same time, **this question stipulates that each array element can only be return; } // Traverse all choices - // Pruning two: start traversing from start to avoid generating duplicate subsets - // Pruning three: start traversing from start to avoid repeatedly selecting the same element + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element for (int i = start; i < choices.length; i++) { - // Pruning one: if the subset sum exceeds target, end the loop immediately + // Pruning 1: if the subset sum exceeds target, end the loop directly // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target if (target - choices[i] < 0) { break; } - // Pruning four: if the element equals the left element, it indicates that the search branch is repeated, skip it + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly if (i > start && choices[i] == choices[i - 1]) { continue; } - // Attempt: make a choice, update target, start + // Attempt: make choice, update target, start state.add(choices[i]); // Proceed to the next round of selection backtrack(state, target - choices[i], choices, i + 1, res); - // Retract: undo the choice, restore to the previous state + // Backtrack: undo choice, restore to previous state state.remove(state.size() - 1); } } - /* Solve Subset Sum II */ + /* Solve subset sum II */ List> subsetSumII(int[] nums, int target) { List state = new ArrayList<>(); // State (subset) Arrays.sort(nums); // Sort nums @@ -611,93 +1213,458 @@ At the same time, **this question stipulates that each array element can only be === "C#" ```csharp title="subset_sum_ii.cs" - [class]{subset_sum_ii}-[func]{Backtrack} + /* Backtracking algorithm: Subset sum II */ + void Backtrack(List state, int target, int[] choices, int start, List> res) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.Add(new List(state)); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (int i = start; i < choices.Length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // Attempt: make choice, update target, start + state.Add(choices[i]); + // Proceed to the next round of selection + Backtrack(state, target - choices[i], choices, i + 1, res); + // Backtrack: undo choice, restore to previous state + state.RemoveAt(state.Count - 1); + } + } - [class]{subset_sum_ii}-[func]{SubsetSumII} + /* Solve subset sum II */ + List> SubsetSumII(int[] nums, int target) { + List state = []; // State (subset) + Array.Sort(nums); // Sort nums + int start = 0; // Start point for traversal + List> res = []; // Result list (subset list) + Backtrack(state, target, nums, start, res); + return res; + } ``` === "Go" ```go title="subset_sum_ii.go" - [class]{}-[func]{backtrackSubsetSumII} + /* Backtracking algorithm: Subset sum II */ + func backtrackSubsetSumII(start, target int, state, choices *[]int, res *[][]int) { + // When the subset sum equals target, record the solution + if target == 0 { + newState := append([]int{}, *state...) + *res = append(*res, newState) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for i := start; i < len(*choices); i++ { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target-(*choices)[i] < 0 { + break + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if i > start && (*choices)[i] == (*choices)[i-1] { + continue + } + // Attempt: make choice, update target, start + *state = append(*state, (*choices)[i]) + // Proceed to the next round of selection + backtrackSubsetSumII(i+1, target-(*choices)[i], state, choices, res) + // Backtrack: undo choice, restore to previous state + *state = (*state)[:len(*state)-1] + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + func subsetSumII(nums []int, target int) [][]int { + state := make([]int, 0) // State (subset) + sort.Ints(nums) // Sort nums + start := 0 // Start point for traversal + res := make([][]int, 0) // Result list (subset list) + backtrackSubsetSumII(start, target, &state, &nums, &res) + return res + } ``` === "Swift" ```swift title="subset_sum_ii.swift" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + func backtrack(state: inout [Int], target: Int, choices: [Int], start: Int, res: inout [[Int]]) { + // When the subset sum equals target, record the solution + if target == 0 { + res.append(state) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for i in choices.indices.dropFirst(start) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target - choices[i] < 0 { + break + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if i > start, choices[i] == choices[i - 1] { + continue + } + // Attempt: make choice, update target, start + state.append(choices[i]) + // Proceed to the next round of selection + backtrack(state: &state, target: target - choices[i], choices: choices, start: i + 1, res: &res) + // Backtrack: undo choice, restore to previous state + state.removeLast() + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + func subsetSumII(nums: [Int], target: Int) -> [[Int]] { + var state: [Int] = [] // State (subset) + let nums = nums.sorted() // Sort nums + let start = 0 // Start point for traversal + var res: [[Int]] = [] // Result list (subset list) + backtrack(state: &state, target: target, choices: nums, start: start, res: &res) + return res + } ``` === "JS" ```javascript title="subset_sum_ii.js" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + function backtrack(state, target, choices, start, res) { + // When the subset sum equals target, record the solution + if (target === 0) { + res.push([...state]); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (let i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + function subsetSumII(nums, target) { + const state = []; // State (subset) + nums.sort((a, b) => a - b); // Sort nums + const start = 0; // Start point for traversal + const res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "TS" ```typescript title="subset_sum_ii.ts" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + function backtrack( + state: number[], + target: number, + choices: number[], + start: number, + res: number[][] + ): void { + // When the subset sum equals target, record the solution + if (target === 0) { + res.push([...state]); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (let i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if (i > start && choices[i] === choices[i - 1]) { + continue; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + function subsetSumII(nums: number[], target: number): number[][] { + const state = []; // State (subset) + nums.sort((a, b) => a - b); // Sort nums + const start = 0; // Start point for traversal + const res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Dart" ```dart title="subset_sum_ii.dart" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + void backtrack( + List state, + int target, + List choices, + int start, + List> res, + ) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.add(List.from(state)); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (int i = start; i < choices.length; i++) { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if (target - choices[i] < 0) { + break; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // Attempt: make choice, update target, start + state.add(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res); + // Backtrack: undo choice, restore to previous state + state.removeLast(); + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + List> subsetSumII(List nums, int target) { + List state = []; // State (subset) + nums.sort(); // Sort nums + int start = 0; // Start point for traversal + List> res = []; // Result list (subset list) + backtrack(state, target, nums, start, res); + return res; + } ``` === "Rust" ```rust title="subset_sum_ii.rs" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + fn backtrack( + state: &mut Vec, + target: i32, + choices: &[i32], + start: usize, + res: &mut Vec>, + ) { + // When the subset sum equals target, record the solution + if target == 0 { + res.push(state.clone()); + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for i in start..choices.len() { + // Pruning 1: if the subset sum exceeds target, end the loop directly + // This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + if target - choices[i] < 0 { + break; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if i > start && choices[i] == choices[i - 1] { + continue; + } + // Attempt: make choice, update target, start + state.push(choices[i]); + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res); + // Backtrack: undo choice, restore to previous state + state.pop(); + } + } - [class]{}-[func]{subset_sum_ii} + /* Solve subset sum II */ + fn subset_sum_ii(nums: &mut [i32], target: i32) -> Vec> { + let mut state = Vec::new(); // State (subset) + nums.sort(); // Sort nums + let start = 0; // Start point for traversal + let mut res = Vec::new(); // Result list (subset list) + backtrack(&mut state, target, nums, start, &mut res); + res + } ``` === "C" ```c title="subset_sum_ii.c" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + void backtrack(int target, int *choices, int choicesSize, int start) { + // When the subset sum equals target, record the solution + if (target == 0) { + for (int i = 0; i < stateSize; i++) { + res[resSize][i] = state[i]; + } + resColSizes[resSize++] = stateSize; + return; + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (int i = start; i < choicesSize; i++) { + // Pruning 1: Skip if subset sum exceeds target + if (target - choices[i] < 0) { + continue; + } + // Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + if (i > start && choices[i] == choices[i - 1]) { + continue; + } + // Attempt: make choice, update target, start + state[stateSize] = choices[i]; + stateSize++; + // Proceed to the next round of selection + backtrack(target - choices[i], choices, choicesSize, i + 1); + // Backtrack: undo choice, restore to previous state + stateSize--; + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + void subsetSumII(int *nums, int numsSize, int target) { + // Sort nums + qsort(nums, numsSize, sizeof(int), cmp); + // Start backtracking + backtrack(target, nums, numsSize, 0); + } ``` === "Kotlin" ```kotlin title="subset_sum_ii.kt" - [class]{}-[func]{backtrack} + /* Backtracking algorithm: Subset sum II */ + fun backtrack( + state: MutableList, + target: Int, + choices: IntArray, + start: Int, + res: MutableList?> + ) { + // When the subset sum equals target, record the solution + if (target == 0) { + res.add(state.toMutableList()) + return + } + // Traverse all choices + // Pruning 2: start traversing from start to avoid generating duplicate subsets + // Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for (i in start.. start && choices[i] == choices[i - 1]) { + continue + } + // Attempt: make choice, update target, start + state.add(choices[i]) + // Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res) + // Backtrack: undo choice, restore to previous state + state.removeAt(state.size - 1) + } + } - [class]{}-[func]{subsetSumII} + /* Solve subset sum II */ + fun subsetSumII(nums: IntArray, target: Int): MutableList?> { + val state = mutableListOf() // State (subset) + nums.sort() // Sort nums + val start = 0 // Start point for traversal + val res = mutableListOf?>() // Result list (subset list) + backtrack(state, target, nums, start, res) + return res + } ``` === "Ruby" ```ruby title="subset_sum_ii.rb" - [class]{}-[func]{backtrack} + ### Backtracking: subset sum II ### + def backtrack(state, target, choices, start, res) + # When the subset sum equals target, record the solution + if target.zero? + res << state.dup + return + end - [class]{}-[func]{subset_sum_ii} + # Traverse all choices + # Pruning 2: start traversing from start to avoid generating duplicate subsets + # Pruning 3: start traversing from start to avoid repeatedly selecting the same element + for i in start...choices.length + # Pruning 1: if the subset sum exceeds target, end the loop directly + # This is because the array is sorted, and later elements are larger, so the subset sum will definitely exceed target + break if target - choices[i] < 0 + # Pruning 4: if this element equals the left element, it means this search branch is duplicate, skip it directly + next if i > start && choices[i] == choices[i - 1] + # Attempt: make choice, update target, start + state << choices[i] + # Proceed to the next round of selection + backtrack(state, target - choices[i], choices, i + 1, res) + # Backtrack: undo choice, restore to previous state + state.pop + end + end + + ### Solve subset sum II ### + def subset_sum_ii(nums, target) + state = [] # State (subset) + nums.sort! # Sort nums + start = 0 # Start point for traversal + res = [] # Result list (subset list) + backtrack(state, target, nums, start, res) + res + end ``` -=== "Zig" +Figure 13-14 shows the backtracking process for array $[4, 4, 5]$ and target element $9$, which includes four types of pruning operations. Combine the illustration with the code comments to understand the entire search process and how each pruning operation works. - ```zig title="subset_sum_ii.zig" - [class]{}-[func]{backtrack} +![Subset-sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } - [class]{}-[func]{subsetSumII} - ``` - -Figure 13-14 shows the backtracking process for the array $[4, 4, 5]$ and target element $9$, including four types of pruning operations. Please combine the illustration with the code comments to understand the entire search process and how each type of pruning operation works. - -![Subset sum II backtracking process](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } - -

Figure 13-14   Subset sum II backtracking process

+

Figure 13-14   Subset-sum II backtracking process

diff --git a/en/docs/chapter_backtracking/summary.md b/en/docs/chapter_backtracking/summary.md index 2e38706be..738d765ff 100644 --- a/en/docs/chapter_backtracking/summary.md +++ b/en/docs/chapter_backtracking/summary.md @@ -4,24 +4,24 @@ comments: true # 13.5   Summary -### 1.   Key review +### 1.   Key Review -- The essence of the backtracking algorithm is exhaustive search. It seeks solutions that meet the conditions by performing a depth-first traversal of the solution space. During the search, if a satisfying solution is found, it is recorded, until all solutions are found or the traversal is completed. -- The search process of the backtracking algorithm includes trying and backtracking. It uses depth-first search to explore various choices, and when a choice does not meet the constraints, the previous choice is undone. Then it reverts to the previous state and continues to try other options. Trying and backtracking are operations in opposite directions. -- Backtracking problems usually contain multiple constraints. These constraints can be used to perform pruning operations. Pruning can terminate unnecessary search branches in advance, greatly enhancing search efficiency. -- The backtracking algorithm is mainly used to solve search problems and constraint satisfaction problems. Although combinatorial optimization problems can be solved using backtracking, there are often more efficient or effective solutions available. -- The permutation problem aims to search for all possible permutations of the elements in a given set. We use an array to record whether each element has been chosen, avoiding repeated selection of the same element. This ensures that each element is chosen only once. -- In permutation problems, if the set contains duplicate elements, the final result will include duplicate permutations. We need to restrict that identical elements can only be selected once in each round, which is usually implemented using a hash set. -- The subset-sum problem aims to find all subsets in a given set that sum to a target value. The set does not distinguish the order of elements, but the search process may generate duplicate subsets. This occurs because the algorithm explores different element orders as unique paths. Before backtracking, we sort the data and set a variable to indicate the starting point of the traversal for each round. This allows us to prune the search branches that generate duplicate subsets. -- For the subset-sum problem, equal elements in the array can produce duplicate sets. Using the precondition that the array is already sorted, we prune by determining if adjacent elements are equal. This ensures that equal elements are only selected once per round. -- The $n$ queens problem aims to find schemes to place $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of the problem include row constraints, column constraints, and constraints on the main and secondary diagonals. To meet the row constraint, we adopt a strategy of placing one queen per row, ensuring each row has one queen placed. -- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether there is a queen in each column, thereby indicating whether the selected cell is legal. For diagonal constraints, we use two arrays to respectively record the presence of queens on the main and secondary diagonals. The challenge is to determine the relationship between row and column indices for cells on the same main or secondary diagonal. +- The backtracking algorithm is fundamentally an exhaustive search method. It finds solutions that meet specified conditions by performing a depth-first traversal of the solution space. During the search process, when a solution satisfying the conditions is found, it is recorded. The search ends either after finding all solutions or when the traversal is complete. +- The backtracking algorithm search process consists of two parts: attempting and backtracking. It tries various choices through depth-first search. When encountering situations that violate constraints, it reverts the previous choice, returns to the previous state, and continues exploring other options. Attempting and backtracking are operations in opposite directions. +- Backtracking problems typically contain multiple constraints, which can be utilized to implement pruning operations. Pruning can terminate unnecessary search branches early, significantly improving search efficiency. +- The backtracking algorithm is primarily used to solve search problems and constraint satisfaction problems. While combinatorial optimization problems can be solved with backtracking, there are often more efficient or better-performing solutions available. +- The permutation problem aims to find all possible permutations of elements in a given set. We use an array to record whether each element has been selected, thereby pruning search branches that attempt to select the same element repeatedly, ensuring each element is selected exactly once. +- In the permutation problem, if the set contains duplicate elements, the final result will contain duplicate permutations. We need to impose a constraint so that equal elements can only be selected once per round, which is typically achieved using a hash set. +- The subset-sum problem aims to find all subsets of a given set that sum to a target value. Since the set is unordered but the search process outputs results in all orders, duplicate subsets are generated. We sort the data before backtracking and use a variable to indicate the starting point of each round's traversal, thereby pruning search branches that generate duplicate subsets. +- For the subset-sum problem, equal elements in the array produce duplicate sets. We leverage the precondition that the array is sorted by checking whether adjacent elements are equal to implement pruning, ensuring that equal elements can only be selected once per round. +- The $n$ queens problem aims to find placements of $n$ queens on an $n \times n$ chessboard such that no two queens can attack each other. The constraints of this problem include row constraints, column constraints, and main and anti-diagonal constraints. To satisfy row constraints, we adopt a row-by-row placement strategy, ensuring exactly one queen is placed in each row. +- The handling of column constraints and diagonal constraints is similar. For column constraints, we use an array to record whether each column has a queen, thereby indicating whether a selected cell is valid. For diagonal constraints, we use two arrays to separately record whether queens exist on each main or anti-diagonal. The challenge lies in finding the row-column index pattern that characterizes cells on the same main (anti-)diagonal. ### 2.   Q & A -**Q**: How can we understand the relationship between backtracking and recursion? +**Q**: How should we understand the relationship between backtracking and recursion? -Overall, backtracking is an "algorithmic strategy," while recursion is more of a "tool." +Overall, backtracking is an "algorithm strategy", while recursion is more like a "tool". -- Backtracking algorithms are typically based on recursion. However, backtracking is one of the application scenarios of recursion, specifically in search problems. -- The structure of recursion reflects the problem-solving paradigm of "sub-problem decomposition." It is commonly used in solving problems involving divide and conquer, backtracking, and dynamic programming (memoized recursion). +- The backtracking algorithm is typically implemented based on recursion. However, backtracking is one application scenario of recursion and represents the application of recursion in search problems. +- The structure of recursion embodies the "subproblem decomposition" problem-solving paradigm, commonly used to solve problems involving divide-and-conquer, backtracking, and dynamic programming (memoized recursion). diff --git a/en/docs/chapter_computational_complexity/index.md b/en/docs/chapter_computational_complexity/index.md index 6188ae671..39d6bfe90 100644 --- a/en/docs/chapter_computational_complexity/index.md +++ b/en/docs/chapter_computational_complexity/index.md @@ -3,20 +3,20 @@ comments: true icon: material/timer-sand --- -# Chapter 2.   Complexity analysis +# Chapter 2.   Complexity Analysis ![Complexity analysis](../assets/covers/chapter_complexity_analysis.jpg){ class="cover-image" } !!! abstract - Complexity analysis is like a space-time navigator in the vast universe of algorithms. + Complexity analysis is like a space-time guide in the vast universe of algorithms. - It guides us in exploring deeper within the dimensions of time and space, seeking more elegant solutions. + It leads us to explore deeply within the two dimensions of time and space, seeking more elegant solutions. ## Chapter contents -- [2.1   Algorithm efficiency assessment](performance_evaluation.md) -- [2.2   Iteration and recursion](iteration_and_recursion.md) -- [2.3   Time complexity](time_complexity.md) -- [2.4   Space complexity](space_complexity.md) +- [2.1   Algorithm Efficiency Evaluation](performance_evaluation.md) +- [2.2   Iteration and Recursion](iteration_and_recursion.md) +- [2.3   Time Complexity](time_complexity.md) +- [2.4   Space Complexity](space_complexity.md) - [2.5   Summary](summary.md) diff --git a/en/docs/chapter_computational_complexity/iteration_and_recursion.md b/en/docs/chapter_computational_complexity/iteration_and_recursion.md index 58795f7da..4424aa8ca 100644 --- a/en/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/en/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -2,19 +2,19 @@ comments: true --- -# 2.2   Iteration and recursion +# 2.2   Iteration and Recursion -In algorithms, the repeated execution of a task is quite common and is closely related to the analysis of complexity. Therefore, before delving into the concepts of time complexity and space complexity, let's first explore how to implement repetitive tasks in programming. This involves understanding two fundamental programming control structures: iteration and recursion. +In algorithms, repeatedly executing a task is very common and closely related to complexity analysis. Therefore, before introducing time complexity and space complexity, let's first understand how to implement repeated task execution in programs, namely the two basic program control structures: iteration and recursion. ## 2.2.1   Iteration -Iteration is a control structure for repeatedly performing a task. In iteration, a program repeats a block of code as long as a certain condition is met until this condition is no longer satisfied. +Iteration is a control structure for repeatedly executing a task. In iteration, a program repeatedly executes a segment of code under certain conditions until those conditions are no longer satisfied. -### 1.   For loops +### 1.   For Loop -The `for` loop is one of the most common forms of iteration, and **it's particularly suitable when the number of iterations is known in advance**. +The `for` loop is one of the most common forms of iteration, **suitable for use when the number of iterations is known in advance**. -The following function uses a `for` loop to perform a summation of $1 + 2 + \dots + n$, with the sum being stored in the variable `res`. It's important to note that in Python, `range(a, b)` creates an interval that is inclusive of `a` but exclusive of `b`, meaning it iterates over the range from $a$ up to $b−1$. +The following function implements the summation $1 + 2 + \dots + n$ based on a `for` loop, with the sum result recorded using the variable `res`. Note that in Python, `range(a, b)` corresponds to a "left-closed, right-open" interval, with the traversal range being $a, a + 1, \dots, b-1$: === "Python" @@ -22,7 +22,7 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot def for_loop(n: int) -> int: """for loop""" res = 0 - # Loop sum 1, 2, ..., n-1, n + # Sum 1, 2, ..., n-1, n for i in range(1, n + 1): res += i return res @@ -34,7 +34,7 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot /* for loop */ int forLoop(int n) { int res = 0; - // Loop sum 1, 2, ..., n-1, n + // Sum 1, 2, ..., n-1, n for (int i = 1; i <= n; ++i) { res += i; } @@ -48,7 +48,7 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot /* for loop */ int forLoop(int n) { int res = 0; - // Loop sum 1, 2, ..., n-1, n + // Sum 1, 2, ..., n-1, n for (int i = 1; i <= n; i++) { res += i; } @@ -59,82 +59,158 @@ The following function uses a `for` loop to perform a summation of $1 + 2 + \dot === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{ForLoop} + /* for loop */ + int ForLoop(int n) { + int res = 0; + // Sum 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{forLoop} + /* for loop */ + func forLoop(n int) int { + res := 0 + // Sum 1, 2, ..., n-1, n + for i := 1; i <= n; i++ { + res += i + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{forLoop} + /* for loop */ + func forLoop(n: Int) -> Int { + var res = 0 + // Sum 1, 2, ..., n-1, n + for i in 1 ... n { + res += i + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{forLoop} + /* for loop */ + function forLoop(n) { + let res = 0; + // Sum 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{forLoop} + /* for loop */ + function forLoop(n: number): number { + let res = 0; + // Sum 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{forLoop} + /* for loop */ + int forLoop(int n) { + int res = 0; + // Sum 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{for_loop} + /* for loop */ + fn for_loop(n: i32) -> i32 { + let mut res = 0; + // Sum 1, 2, ..., n-1, n + for i in 1..=n { + res += i; + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{forLoop} + /* for loop */ + int forLoop(int n) { + int res = 0; + // Sum 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + res += i; + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{forLoop} + /* for loop */ + fun forLoop(n: Int): Int { + var res = 0 + // Sum 1, 2, ..., n-1, n + for (i in 1..n) { + res += i + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{for_loop} + ### for loop ### + def for_loop(n) + res = 0 + + # Sum 1, 2, ..., n-1, n + for i in 1..n + res += i + end + + res + end ``` -=== "Zig" +Figure 2-1 shows the flowchart of this summation function. - ```zig title="iteration.zig" - [class]{}-[func]{forLoop} - ``` +![Flowchart of the summation function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } -Figure 2-1 represents this sum function. +

Figure 2-1   Flowchart of the summation function

-![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } +The number of operations in this summation function is proportional to the input data size $n$, or has a "linear relationship". In fact, **time complexity describes precisely this "linear relationship"**. Related content will be introduced in detail in the next section. -

Figure 2-1   Flowchart of the sum function

+### 2.   While Loop -The number of operations in this summation function is proportional to the size of the input data $n$, or in other words, it has a linear relationship. **This "linear relationship" is what time complexity describes**. This topic will be discussed in more detail in the next section. +Similar to the `for` loop, the `while` loop is also a method for implementing iteration. In a `while` loop, the program first checks the condition in each round; if the condition is true, it continues execution, otherwise it ends the loop. -### 2.   While loops - -Similar to `for` loops, `while` loops are another approach for implementing iteration. In a `while` loop, the program checks a condition at the beginning of each iteration; if the condition is true, the execution continues, otherwise, the loop ends. - -Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$. +Below we use a `while` loop to implement the summation $1 + 2 + \dots + n$: === "Python" @@ -143,7 +219,7 @@ Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$. """while loop""" res = 0 i = 1 # Initialize condition variable - # Loop sum 1, 2, ..., n-1, n + # Sum 1, 2, ..., n-1, n while i <= n: res += i i += 1 # Update condition variable @@ -157,7 +233,7 @@ Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$. int whileLoop(int n) { int res = 0; int i = 1; // Initialize condition variable - // Loop sum 1, 2, ..., n-1, n + // Sum 1, 2, ..., n-1, n while (i <= n) { res += i; i++; // Update condition variable @@ -173,7 +249,7 @@ Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$. int whileLoop(int n) { int res = 0; int i = 1; // Initialize condition variable - // Loop sum 1, 2, ..., n-1, n + // Sum 1, 2, ..., n-1, n while (i <= n) { res += i; i++; // Update condition variable @@ -185,72 +261,171 @@ Below we use a `while` loop to implement the sum $1 + 2 + \dots + n$. === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{WhileLoop} + /* while loop */ + int WhileLoop(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i += 1; // Update condition variable + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{whileLoop} + /* while loop */ + func whileLoop(n int) int { + res := 0 + // Initialize condition variable + i := 1 + // Sum 1, 2, ..., n-1, n + for i <= n { + res += i + // Update condition variable + i++ + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{whileLoop} + /* while loop */ + func whileLoop(n: Int) -> Int { + var res = 0 + var i = 1 // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while i <= n { + res += i + i += 1 // Update condition variable + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{whileLoop} + /* while loop */ + function whileLoop(n) { + let res = 0; + let i = 1; // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // Update condition variable + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{whileLoop} + /* while loop */ + function whileLoop(n: number): number { + let res = 0; + let i = 1; // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // Update condition variable + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoop} + /* while loop */ + int whileLoop(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // Update condition variable + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{while_loop} + /* while loop */ + fn while_loop(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // Initialize condition variable + + // Sum 1, 2, ..., n-1, n + while i <= n { + res += i; + i += 1; // Update condition variable + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{whileLoop} + /* while loop */ + int whileLoop(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i; + i++; // Update condition variable + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{whileLoop} + /* while loop */ + fun whileLoop(n: Int): Int { + var res = 0 + var i = 1 // Initialize condition variable + // Sum 1, 2, ..., n-1, n + while (i <= n) { + res += i + i++ // Update condition variable + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{while_loop} + ### while loop ### + def while_loop(n) + res = 0 + i = 1 # Initialize condition variable + + # Sum 1, 2, ..., n-1, n + while i <= n + res += i + i += 1 # Update condition variable + end + + res + end ``` -=== "Zig" +**The `while` loop has greater flexibility than the `for` loop**. In a `while` loop, we can freely design the initialization and update steps of the condition variable. - ```zig title="iteration.zig" - [class]{}-[func]{whileLoop} - ``` - -**`while` loops provide more flexibility than `for` loops**, especially since they allow for custom initialization and modification of the condition variable at each step. - -For example, in the following code, the condition variable $i$ is updated twice each round, which would be inconvenient to implement with a `for` loop. +For example, in the following code, the condition variable $i$ is updated twice per round, which is not convenient to implement using a `for` loop: === "Python" @@ -259,7 +434,7 @@ For example, in the following code, the condition variable $i$ is updated twice """while loop (two updates)""" res = 0 i = 1 # Initialize condition variable - # Loop sum 1, 4, 10, ... + # Sum 1, 4, 10, ... while i <= n: res += i # Update condition variable @@ -275,7 +450,7 @@ For example, in the following code, the condition variable $i$ is updated twice int whileLoopII(int n) { int res = 0; int i = 1; // Initialize condition variable - // Loop sum 1, 4, 10, ... + // Sum 1, 4, 10, ... while (i <= n) { res += i; // Update condition variable @@ -293,7 +468,7 @@ For example, in the following code, the condition variable $i$ is updated twice int whileLoopII(int n) { int res = 0; int i = 1; // Initialize condition variable - // Loop sum 1, 4, 10, ... + // Sum 1, 4, 10, ... while (i <= n) { res += i; // Update condition variable @@ -307,80 +482,198 @@ For example, in the following code, the condition variable $i$ is updated twice === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{WhileLoopII} + /* while loop (two updates) */ + int WhileLoopII(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i; + // Update condition variable + i += 1; + i *= 2; + } + return res; + } ``` === "Go" ```go title="iteration.go" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + func whileLoopII(n int) int { + res := 0 + // Initialize condition variable + i := 1 + // Sum 1, 4, 10, ... + for i <= n { + res += i + // Update condition variable + i++ + i *= 2 + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + func whileLoopII(n: Int) -> Int { + var res = 0 + var i = 1 // Initialize condition variable + // Sum 1, 4, 10, ... + while i <= n { + res += i + // Update condition variable + i += 1 + i *= 2 + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + function whileLoopII(n) { + let res = 0; + let i = 1; // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i; + // Update condition variable + i++; + i *= 2; + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + function whileLoopII(n: number): number { + let res = 0; + let i = 1; // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i; + // Update condition variable + i++; + i *= 2; + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i; + // Update condition variable + i++; + i *= 2; + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{while_loop_ii} + /* while loop (two updates) */ + fn while_loop_ii(n: i32) -> i32 { + let mut res = 0; + let mut i = 1; // Initialize condition variable + + // Sum 1, 4, 10, ... + while i <= n { + res += i; + // Update condition variable + i += 1; + i *= 2; + } + res + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + int whileLoopII(int n) { + int res = 0; + int i = 1; // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i; + // Update condition variable + i++; + i *= 2; + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{whileLoopII} + /* while loop (two updates) */ + fun whileLoopII(n: Int): Int { + var res = 0 + var i = 1 // Initialize condition variable + // Sum 1, 4, 10, ... + while (i <= n) { + res += i + // Update condition variable + i++ + i *= 2 + } + return res + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{while_loop_ii} + ### while loop (two updates) ### + def while_loop_ii(n) + res = 0 + i = 1 # Initialize condition variable + + # Sum 1, 4, 10, ... + while i <= n + res += i + # Update condition variable + i += 1 + i *= 2 + end + + res + end ``` -=== "Zig" +Overall, **`for` loops have more compact code, while `while` loops are more flexible**; both can implement iterative structures. The choice of which to use should be determined based on the requirements of the specific problem. - ```zig title="iteration.zig" - [class]{}-[func]{whileLoopII} - ``` +### 3.   Nested Loops -Overall, **`for` loops are more concise, while `while` loops are more flexible**. Both can implement iterative structures. Which one to use should be determined based on the specific requirements of the problem. - -### 3.   Nested loops - -We can nest one loop structure within another. Below is an example using `for` loops: +We can nest one loop structure inside another. Below is an example using `for` loops: === "Python" ```python title="iteration.py" def nested_for_loop(n: int) -> str: - """Double for loop""" + """Nested for loop""" res = "" # Loop i = 1, 2, ..., n-1, n for i in range(1, n + 1): @@ -393,7 +686,7 @@ We can nest one loop structure within another. Below is an example using `for` l === "C++" ```cpp title="iteration.cpp" - /* Double for loop */ + /* Nested for loop */ string nestedForLoop(int n) { ostringstream res; // Loop i = 1, 2, ..., n-1, n @@ -410,7 +703,7 @@ We can nest one loop structure within another. Below is an example using `for` l === "Java" ```java title="iteration.java" - /* Double for loop */ + /* Nested for loop */ String nestedForLoop(int n) { StringBuilder res = new StringBuilder(); // Loop i = 1, 2, ..., n-1, n @@ -427,93 +720,203 @@ We can nest one loop structure within another. Below is an example using `for` l === "C#" ```csharp title="iteration.cs" - [class]{iteration}-[func]{NestedForLoop} + /* Nested for loop */ + string NestedForLoop(int n) { + StringBuilder res = new(); + // Loop i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // Loop 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" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + func nestedForLoop(n int) string { + res := "" + // Loop i = 1, 2, ..., n-1, n + for i := 1; i <= n; i++ { + for j := 1; j <= n; j++ { + // Loop j = 1, 2, ..., n-1, n + res += fmt.Sprintf("(%d, %d), ", i, j) + } + } + return res + } ``` === "Swift" ```swift title="iteration.swift" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + func nestedForLoop(n: Int) -> String { + var res = "" + // Loop i = 1, 2, ..., n-1, n + for i in 1 ... n { + // Loop j = 1, 2, ..., n-1, n + for j in 1 ... n { + res.append("(\(i), \(j)), ") + } + } + return res + } ``` === "JS" ```javascript title="iteration.js" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + function nestedForLoop(n) { + let res = ''; + // Loop i = 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + // Loop j = 1, 2, ..., n-1, n + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } ``` === "TS" ```typescript title="iteration.ts" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + function nestedForLoop(n: number): string { + let res = ''; + // Loop i = 1, 2, ..., n-1, n + for (let i = 1; i <= n; i++) { + // Loop j = 1, 2, ..., n-1, n + for (let j = 1; j <= n; j++) { + res += `(${i}, ${j}), `; + } + } + return res; + } ``` === "Dart" ```dart title="iteration.dart" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + String nestedForLoop(int n) { + String res = ""; + // Loop i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // Loop j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + res += "($i, $j), "; + } + } + return res; + } ``` === "Rust" ```rust title="iteration.rs" - [class]{}-[func]{nested_for_loop} + /* Nested for loop */ + fn nested_for_loop(n: i32) -> String { + let mut res = vec![]; + // Loop i = 1, 2, ..., n-1, n + for i in 1..=n { + // Loop j = 1, 2, ..., n-1, n + for j in 1..=n { + res.push(format!("({}, {}), ", i, j)); + } + } + res.join("") + } ``` === "C" ```c title="iteration.c" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + char *nestedForLoop(int n) { + // n * n is the number of points, "(i, j), " string max length is 6+10*2, plus extra space for null character \0 + int size = n * n * 26 + 1; + char *res = malloc(size * sizeof(char)); + // Loop i = 1, 2, ..., n-1, n + for (int i = 1; i <= n; i++) { + // Loop j = 1, 2, ..., n-1, n + for (int j = 1; j <= n; j++) { + char tmp[26]; + snprintf(tmp, sizeof(tmp), "(%d, %d), ", i, j); + strncat(res, tmp, size - strlen(res) - 1); + } + } + return res; + } ``` === "Kotlin" ```kotlin title="iteration.kt" - [class]{}-[func]{nestedForLoop} + /* Nested for loop */ + fun nestedForLoop(n: Int): String { + val res = StringBuilder() + // Loop i = 1, 2, ..., n-1, n + for (i in 1..n) { + // Loop j = 1, 2, ..., n-1, n + for (j in 1..n) { + res.append(" ($i, $j), ") + } + } + return res.toString() + } ``` === "Ruby" ```ruby title="iteration.rb" - [class]{}-[func]{nested_for_loop} + ### Nested for loop ### + def nested_for_loop(n) + res = "" + + # Loop i = 1, 2, ..., n-1, n + for i in 1..n + # Loop j = 1, 2, ..., n-1, n + for j in 1..n + res += "(#{i}, #{j}), " + end + end + + res + end ``` -=== "Zig" +Figure 2-2 shows the flowchart of this nested loop. - ```zig title="iteration.zig" - [class]{}-[func]{nestedForLoop} - ``` +![Flowchart of nested loops](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } -Figure 2-2 represents this nested loop. +

Figure 2-2   Flowchart of nested loops

-![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } +In this case, the number of operations of the function is proportional to $n^2$, or the algorithm's running time has a "quadratic relationship" with the input data size $n$. -

Figure 2-2   Flowchart of the nested loop

- -In such cases, the number of operations of the function is proportional to $n^2$, meaning the algorithm's runtime and the size of the input data $n$ has a 'quadratic relationship.' - -We can further increase the complexity by adding more nested loops, each level of nesting effectively "increasing the dimension," which raises the time complexity to "cubic," "quartic," and so on. +We can continue adding nested loops, where each nesting is a "dimension increase", raising the time complexity to "cubic relationship", "quartic relationship", and so on. ## 2.2.2   Recursion -Recursion is an algorithmic strategy where a function solves a problem by calling itself. It primarily involves two phases: +Recursion is an algorithmic strategy that solves problems by having a function call itself. It mainly consists of two phases. -1. **Calling**: This is where the program repeatedly calls itself, often with progressively smaller or simpler arguments, moving towards the "termination condition." -2. **Returning**: Upon triggering the "termination condition," the program begins to return from the deepest recursive function, aggregating the results of each layer. +1. **Descend**: The program continuously calls itself deeper, usually passing in smaller or more simplified parameters, until reaching a "termination condition". +2. **Ascend**: After triggering the "termination condition", the program returns layer by layer from the deepest recursive function, aggregating the result of each layer. -From an implementation perspective, recursive code mainly includes three elements. +From an implementation perspective, recursive code mainly consists of three elements. -1. **Termination Condition**: Determines when to switch from "calling" to "returning." -2. **Recursive Call**: Corresponds to "calling," where the function calls itself, usually with smaller or more simplified parameters. -3. **Return Result**: Corresponds to "returning," where the result of the current recursion level is returned to the previous layer. +1. **Termination condition**: Used to determine when to switch from "descending" to "ascending". +2. **Recursive call**: Corresponds to "descending", where the function calls itself, usually with smaller or more simplified parameters. +3. **Return result**: Corresponds to "ascending", returning the result of the current recursion level to the previous layer. -Observe the following code, where simply calling the function `recur(n)` can compute the sum of $1 + 2 + \dots + n$: +Observe the following code. We only need to call the function `recur(n)` to complete the calculation of $1 + 2 + \dots + n$: === "Python" @@ -523,7 +926,7 @@ Observe the following code, where simply calling the function `recur(n)` can com # Termination condition if n == 1: return 1 - # Recursive: recursive call + # Recurse: recursive call res = recur(n - 1) # Return: return result return n + res @@ -537,7 +940,7 @@ Observe the following code, where simply calling the function `recur(n)` can com // Termination condition if (n == 1) return 1; - // Recursive: recursive call + // Recurse: recursive call int res = recur(n - 1); // Return: return result return n + res; @@ -552,7 +955,7 @@ Observe the following code, where simply calling the function `recur(n)` can com // Termination condition if (n == 1) return 1; - // Recursive: recursive call + // Recurse: recursive call int res = recur(n - 1); // Return: return result return n + res; @@ -562,108 +965,191 @@ Observe the following code, where simply calling the function `recur(n)` can com === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{Recur} + /* Recursion */ + int Recur(int n) { + // Termination condition + if (n == 1) + return 1; + // Recurse: recursive call + int res = Recur(n - 1); + // Return: return result + return n + res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{recur} + /* Recursion */ + func recur(n int) int { + // Termination condition + if n == 1 { + return 1 + } + // Recurse: recursive call + res := recur(n - 1) + // Return: return result + return n + res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{recur} + /* Recursion */ + func recur(n: Int) -> Int { + // Termination condition + if n == 1 { + return 1 + } + // Recurse: recursive call + let res = recur(n: n - 1) + // Return: return result + return n + res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{recur} + /* Recursion */ + function recur(n) { + // Termination condition + if (n === 1) return 1; + // Recurse: recursive call + const res = recur(n - 1); + // Return: return result + return n + res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{recur} + /* Recursion */ + function recur(n: number): number { + // Termination condition + if (n === 1) return 1; + // Recurse: recursive call + const res = recur(n - 1); + // Return: return result + return n + res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{recur} + /* Recursion */ + int recur(int n) { + // Termination condition + if (n == 1) return 1; + // Recurse: recursive call + int res = recur(n - 1); + // Return: return result + return n + res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{recur} + /* Recursion */ + fn recur(n: i32) -> i32 { + // Termination condition + if n == 1 { + return 1; + } + // Recurse: recursive call + let res = recur(n - 1); + // Return: return result + n + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{recur} + /* Recursion */ + int recur(int n) { + // Termination condition + if (n == 1) + return 1; + // Recurse: recursive call + int res = recur(n - 1); + // Return: return result + return n + res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{recur} + /* Recursion */ + fun recur(n: Int): Int { + // Termination condition + if (n == 1) + return 1 + // Descend: recursive call + val res = recur(n - 1) + // Return: return result + return n + res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{recur} - ``` - -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{recur} + ### Recursion ### + def recur(n) + # Termination condition + return 1 if n == 1 + # Recurse: recursive call + res = recur(n - 1) + # Return: return result + n + res + end ``` Figure 2-3 shows the recursive process of this function. -![Recursive process of the sum function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } +![Recursive process of the summation function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } -

Figure 2-3   Recursive process of the sum function

+

Figure 2-3   Recursive process of the summation function

-Although iteration and recursion can achieve the same results from a computational standpoint, **they represent two entirely different paradigms of thinking and problem-solving**. +Although from a computational perspective, iteration and recursion can achieve the same results, **they represent two completely different paradigms for thinking about and solving problems**. -- **Iteration**: Solves problems "from the bottom up." It starts with the most basic steps, and then repeatedly adds or accumulates these steps until the task is complete. -- **Recursion**: Solves problems "from the top down." It breaks down the original problem into smaller sub-problems, each of which has the same form as the original problem. These sub-problems are then further decomposed into even smaller sub-problems, stopping at the base case whose solution is known. +- **Iteration**: Solves problems "bottom-up". Starting from the most basic steps, these steps are then repeatedly executed or accumulated until the task is complete. +- **Recursion**: Solves problems "top-down". The original problem is decomposed into smaller subproblems that have the same form as the original problem. These subproblems continue to be decomposed into even smaller subproblems until reaching the base case (where the solution is known). -Let's take the earlier example of the summation function, defined as $f(n) = 1 + 2 + \dots + n$. +Taking the above summation function as an example, let the problem be $f(n) = 1 + 2 + \dots + n$. -- **Iteration**: In this approach, we simulate the summation process within a loop. Starting from $1$ and traversing to $n$, we perform the summation operation in each iteration to eventually compute $f(n)$. -- **Recursion**: Here, the problem is broken down into a sub-problem: $f(n) = n + f(n-1)$. This decomposition continues recursively until reaching the base case, $f(1) = 1$, at which point the recursion terminates. +- **Iteration**: Simulates the summation process in a loop, traversing from $1$ to $n$, performing the summation operation in each round to obtain $f(n)$. +- **Recursion**: Decomposes the problem into the subproblem $f(n) = n + f(n-1)$, continuously decomposing (recursively) until terminating at the base case $f(1) = 1$. -### 1.   Call stack +### 1.   Call Stack -Every time a recursive function calls itself, the system allocates memory for the newly initiated function to store local variables, the return address, and other relevant information. This leads to two primary outcomes. +Each time a recursive function calls itself, the system allocates memory for the newly opened function to store local variables, call addresses, and other information. This leads to two consequences. -- The function's context data is stored in a memory area called "stack frame space" and is only released after the function returns. Therefore, **recursion generally consumes more memory space than iteration**. -- Recursive calls introduce additional overhead. **Hence, recursion is usually less time-efficient than loops.** +- The function's context data is stored in a memory area called "stack frame space", which is not released until the function returns. Therefore, **recursion usually consumes more memory space than iteration**. +- Recursive function calls incur additional overhead. **Therefore, recursion is usually less time-efficient than loops**. -As shown in Figure 2-4, there are $n$ unreturned recursive functions before triggering the termination condition, indicating a **recursion depth of $n$**. +As shown in Figure 2-4, before the termination condition is triggered, there are $n$ unreturned recursive functions existing simultaneously, with a **recursion depth of $n$**. ![Recursion call depth](iteration_and_recursion.assets/recursion_sum_depth.png){ class="animation-figure" }

Figure 2-4   Recursion call depth

-In practice, the depth of recursion allowed by programming languages is usually limited, and excessively deep recursion can lead to stack overflow errors. +In practice, the recursion depth allowed by programming languages is usually limited, and excessively deep recursion may lead to stack overflow errors. -### 2.   Tail recursion +### 2.   Tail Recursion -Interestingly, **if a function performs its recursive call as the very last step before returning,** it can be optimized by the compiler or interpreter to be as space-efficient as iteration. This scenario is known as tail recursion. +Interestingly, **if a function makes the recursive call as the very last step before returning**, the function can be optimized by the compiler or interpreter to have space efficiency comparable to iteration. This case is called tail recursion. -- **Regular recursion**: In standard recursion, when the function returns to the previous level, it continues to execute more code, requiring the system to save the context of the previous call. -- **Tail recursion**: Here, the recursive call is the final operation before the function returns. This means that upon returning to the previous level, no further actions are needed, so the system does not need to save the context of the previous level. +- **Regular recursion**: When a function returns to the previous level, it needs to continue executing code, so the system needs to save the context of the previous layer's call. +- **Tail recursion**: The recursive call is the last operation before the function returns, meaning that after returning to the previous level, there is no need to continue executing other operations, so the system does not need to save the context of the previous layer's function. -For example, in calculating $1 + 2 + \dots + n$, we can make the result variable `res` a parameter of the function, thereby achieving tail recursion: +Taking the calculation of $1 + 2 + \dots + n$ as an example, we can set the result variable `res` as a function parameter to implement tail recursion: === "Python" @@ -706,73 +1192,137 @@ For example, in calculating $1 + 2 + \dots + n$, we can make the result variable === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{TailRecur} + /* Tail recursion */ + int TailRecur(int n, int res) { + // Termination condition + if (n == 0) + return res; + // Tail recursive call + return TailRecur(n - 1, res + n); + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + func tailRecur(n int, res int) int { + // Termination condition + if n == 0 { + return res + } + // Tail recursive call + return tailRecur(n-1, res+n) + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + func tailRecur(n: Int, res: Int) -> Int { + // Termination condition + if n == 0 { + return res + } + // Tail recursive call + return tailRecur(n: n - 1, res: res + n) + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + function tailRecur(n, res) { + // Termination condition + if (n === 0) return res; + // Tail recursive call + return tailRecur(n - 1, res + n); + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + function tailRecur(n: number, res: number): number { + // Termination condition + if (n === 0) return res; + // Tail recursive call + return tailRecur(n - 1, res + n); + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + int tailRecur(int n, int res) { + // Termination condition + if (n == 0) return res; + // Tail recursive call + return tailRecur(n - 1, res + n); + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{tail_recur} + /* Tail recursion */ + fn tail_recur(n: i32, res: i32) -> i32 { + // Termination condition + if n == 0 { + return res; + } + // Tail recursive call + tail_recur(n - 1, res + n) + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + int tailRecur(int n, int res) { + // Termination condition + if (n == 0) + return res; + // Tail recursive call + return tailRecur(n - 1, res + n); + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{tailRecur} + /* Tail recursion */ + tailrec fun tailRecur(n: Int, res: Int): Int { + // Add tailrec keyword to enable tail recursion optimization + // Termination condition + if (n == 0) + return res + // Tail recursive call + return tailRecur(n - 1, res + n) + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{tail_recur} + ### Tail recursion ### + def tail_recur(n, res) + # Termination condition + return res if n == 0 + # Tail recursive call + tail_recur(n - 1, res + n) + end ``` -=== "Zig" +The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the execution point of the summation operation is different. - ```zig title="recursion.zig" - [class]{}-[func]{tailRecur} - ``` - -The execution process of tail recursion is shown in Figure 2-5. Comparing regular recursion and tail recursion, the point of the summation operation is different. - -- **Regular recursion**: The summation operation occurs during the "returning" phase, requiring another summation after each layer returns. -- **Tail recursion**: The summation operation occurs during the "calling" phase, and the "returning" phase only involves returning through each layer. +- **Regular recursion**: The summation operation is performed during the "ascending" process, requiring an additional summation operation after each layer returns. +- **Tail recursion**: The summation operation is performed during the "descending" process; the "ascending" process only needs to return layer by layer. ![Tail recursion process](iteration_and_recursion.assets/tail_recursion_sum.png){ class="animation-figure" } @@ -780,28 +1330,28 @@ The execution process of tail recursion is shown in Figure 2-5. Comparing regula !!! tip - Note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if the function is in the form of tail recursion, it may still encounter stack overflow issues. + Please note that many compilers or interpreters do not support tail recursion optimization. For example, Python does not support tail recursion optimization by default, so even if a function is in tail recursive form, it may still encounter stack overflow issues. -### 3.   Recursion tree +### 3.   Recursion Tree -When dealing with algorithms related to "divide and conquer", recursion often offers a more intuitive approach and more readable code than iteration. Take the "Fibonacci sequence" as an example. +When dealing with algorithmic problems related to "divide and conquer", recursion often provides a more intuitive approach and more readable code than iteration. Taking the "Fibonacci sequence" as an example. !!! question - Given a Fibonacci sequence $0, 1, 1, 2, 3, 5, 8, 13, \dots$, find the $n$th number in the sequence. + Given a Fibonacci sequence $0, 1, 1, 2, 3, 5, 8, 13, \dots$, find the $n$-th number in the sequence. -Let the $n$th number of the Fibonacci sequence be $f(n)$, it's easy to deduce two conclusions: +Let the $n$-th number of the Fibonacci sequence be $f(n)$. Two conclusions can be easily obtained. - The first two numbers of the sequence are $f(1) = 0$ and $f(2) = 1$. -- Each number in the sequence is the sum of the two preceding ones, that is, $f(n) = f(n - 1) + f(n - 2)$. +- Each number in the sequence is the sum of the previous two numbers, i.e., $f(n) = f(n - 1) + f(n - 2)$. -Using the recursive relation, and considering the first two numbers as termination conditions, we can write the recursive code. Calling `fib(n)` will yield the $n$th number of the Fibonacci sequence: +Following the recurrence relation to make recursive calls, with the first two numbers as termination conditions, we can write the recursive code. Calling `fib(n)` will give us the $n$-th number of the Fibonacci sequence: === "Python" ```python title="recursion.py" def fib(n: int) -> int: - """Fibonacci sequence: Recursion""" + """Fibonacci sequence: recursion""" # Termination condition f(1) = 0, f(2) = 1 if n == 1 or n == 2: return n - 1 @@ -814,7 +1364,7 @@ Using the recursive relation, and considering the first two numbers as terminati === "C++" ```cpp title="recursion.cpp" - /* Fibonacci sequence: Recursion */ + /* Fibonacci sequence: recursion */ int fib(int n) { // Termination condition f(1) = 0, f(2) = 1 if (n == 1 || n == 2) @@ -829,7 +1379,7 @@ Using the recursive relation, and considering the first two numbers as terminati === "Java" ```java title="recursion.java" - /* Fibonacci sequence: Recursion */ + /* Fibonacci sequence: recursion */ int fib(int n) { // Termination condition f(1) = 0, f(2) = 1 if (n == 1 || n == 2) @@ -844,125 +1394,208 @@ Using the recursive relation, and considering the first two numbers as terminati === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{Fib} + /* Fibonacci sequence: recursion */ + int Fib(int n) { + // Termination condition f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // Recursive call f(n) = f(n-1) + f(n-2) + int res = Fib(n - 1) + Fib(n - 2); + // Return result f(n) + return res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + func fib(n int) int { + // Termination condition f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // Recursive call f(n) = f(n-1) + f(n-2) + res := fib(n-1) + fib(n-2) + // Return result f(n) + return res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + func fib(n: Int) -> Int { + // Termination condition f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1 + } + // Recursive call f(n) = f(n-1) + f(n-2) + let res = fib(n: n - 1) + fib(n: n - 2) + // Return result f(n) + return res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + function fib(n) { + // Termination condition f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // Recursive call f(n) = f(n-1) + f(n-2) + const res = fib(n - 1) + fib(n - 2); + // Return result f(n) + return res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + function fib(n: number): number { + // Termination condition f(1) = 0, f(2) = 1 + if (n === 1 || n === 2) return n - 1; + // Recursive call f(n) = f(n-1) + f(n-2) + const res = fib(n - 1) + fib(n - 2); + // Return result f(n) + return res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + int fib(int n) { + // Termination condition f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) return n - 1; + // Recursive call f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // Return result f(n) + return res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + fn fib(n: i32) -> i32 { + // Termination condition f(1) = 0, f(2) = 1 + if n == 1 || n == 2 { + return n - 1; + } + // Recursive call f(n) = f(n-1) + f(n-2) + let res = fib(n - 1) + fib(n - 2); + // Return result + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + int fib(int n) { + // Termination condition f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1; + // Recursive call f(n) = f(n-1) + f(n-2) + int res = fib(n - 1) + fib(n - 2); + // Return result f(n) + return res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{fib} + /* Fibonacci sequence: recursion */ + fun fib(n: Int): Int { + // Termination condition f(1) = 0, f(2) = 1 + if (n == 1 || n == 2) + return n - 1 + // Recursive call f(n) = f(n-1) + f(n-2) + val res = fib(n - 1) + fib(n - 2) + // Return result f(n) + return res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{fib} + ### Fibonacci sequence: recursion ### + def fib(n) + # Termination condition f(1) = 0, f(2) = 1 + return n - 1 if n == 1 || n == 2 + # Recursive call f(n) = f(n-1) + f(n-2) + res = fib(n - 1) + fib(n - 2) + # Return result f(n) + res + end ``` -=== "Zig" +Observing the above code, we recursively call two functions within the function, **meaning that one call produces two call branches**. As shown in Figure 2-6, such continuous recursive calling will eventually produce a recursion tree with $n$ levels. - ```zig title="recursion.zig" - [class]{}-[func]{fib} - ``` +![Recursion tree of the Fibonacci sequence](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } -Observing the above code, we see that it recursively calls two functions within itself, **meaning that one call generates two branching calls**. As illustrated in Figure 2-6, this continuous recursive calling eventually creates a recursion tree with a depth of $n$. +

Figure 2-6   Recursion tree of the Fibonacci sequence

-![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } +Fundamentally, recursion embodies the paradigm of "decomposing a problem into smaller subproblems", and this divide-and-conquer strategy is crucial. -

Figure 2-6   Fibonacci sequence recursion tree

+- From an algorithmic perspective, many important algorithmic strategies such as searching, sorting, backtracking, divide and conquer, and dynamic programming directly or indirectly apply this way of thinking. +- From a data structure perspective, recursion is naturally suited for handling problems related to linked lists, trees, and graphs, because they are well-suited for analysis using divide-and-conquer thinking. -Fundamentally, recursion embodies the paradigm of "breaking down a problem into smaller sub-problems." This divide-and-conquer strategy is crucial. +## 2.2.3   Comparison of the Two -- From an algorithmic perspective, many important strategies like searching, sorting, backtracking, divide-and-conquer, and dynamic programming directly or indirectly use this way of thinking. -- From a data structure perspective, recursion is naturally suited for dealing with linked lists, trees, and graphs, as they are well suited for analysis using the divide-and-conquer approach. +Summarizing the above content, as shown in Table 2-1, iteration and recursion differ in implementation, performance, and applicability. -## 2.2.3   Comparison - -Summarizing the above content, the following table shows the differences between iteration and recursion in terms of implementation, performance, and applicability. - -

Table: Comparison of iteration and recursion characteristics

+

Table 2-1   Comparison of iteration and recursion characteristics

-| | Iteration | Recursion | -| ----------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -| Approach | Loop structure | Function calls itself | -| Time Efficiency | Generally higher efficiency, no function call overhead | Each function call generates overhead | -| Memory Usage | Typically uses a fixed size of memory space | Accumulative function calls can use a substantial amount of stack frame space | -| Suitable Problems | Suitable for simple loop tasks, intuitive and readable code | Suitable for problem decomposition, like trees, graphs, divide-and-conquer, backtracking, etc., concise and clear code structure | +| | Iteration | Recursion | +| -------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| Implementation | Loop structure | Function calls itself | +| Time efficiency | Generally more efficient, no function call overhead | Each function call incurs overhead | +| Memory usage | Usually uses a fixed amount of memory space | Accumulated function calls may use a large amount of stack frame space | +| Suitable problems | Suitable for simple loop tasks, with intuitive and readable code | Suitable for subproblem decomposition, such as trees, graphs, divide and conquer, backtracking, etc., with concise and clear code structure |
!!! tip - If you find the following content difficult to understand, consider revisiting it after reading the "Stack" chapter. + If you find the following content difficult to understand, you can review it after reading the "Stack" chapter. -So, what is the intrinsic connection between iteration and recursion? Taking the above recursive function as an example, the summation operation occurs during the recursion's "return" phase. This means that the initially called function is the last to complete its summation operation, **mirroring the "last in, first out" principle of a stack**. +What is the intrinsic relationship between iteration and recursion? Taking the above recursive function as an example, the summation operation is performed during the "ascending" phase of recursion. This means that the function called first actually completes its summation operation last, **and this working mechanism is similar to the "last-in, first-out" principle of stacks**. -Recursive terms like "call stack" and "stack frame space" hint at the close relationship between recursion and stacks. +In fact, recursive terminology such as "call stack" and "stack frame space" already hints at the close relationship between recursion and stacks. -1. **Calling**: When a function is called, the system allocates a new stack frame on the "call stack" for that function, storing local variables, parameters, return addresses, and other data. -2. **Returning**: When a function completes execution and returns, the corresponding stack frame is removed from the "call stack," restoring the execution environment of the previous function. +1. **Descend**: When a function is called, the system allocates a new stack frame on the "call stack" for that function to store the function's local variables, parameters, return address, and other data. +2. **Ascend**: When the function completes execution and returns, the corresponding stack frame is removed from the "call stack", restoring the execution environment of the previous function. -Therefore, **we can use an explicit stack to simulate the behavior of the call stack**, thus transforming recursion into an iterative form: +Therefore, **we can use an explicit stack to simulate the behavior of the call stack**, thus transforming recursion into iterative form: === "Python" ```python title="recursion.py" def for_loop_recur(n: int) -> int: - """Simulate recursion with iteration""" + """Simulate recursion using iteration""" # Use an explicit stack to simulate the system call stack stack = [] res = 0 - # Recursive: recursive call + # Recurse: recursive call for i in range(n, 0, -1): - # Simulate "recursive" by "pushing onto the stack" + # Simulate "recurse" with "push" stack.append(i) # Return: return result while stack: - # Simulate "return" by "popping from the stack" + # Simulate "return" with "pop" res += stack.pop() # res = 1+2+3+...+n return res @@ -971,19 +1604,19 @@ Therefore, **we can use an explicit stack to simulate the behavior of the call s === "C++" ```cpp title="recursion.cpp" - /* Simulate recursion with iteration */ + /* Simulate recursion using iteration */ int forLoopRecur(int n) { // Use an explicit stack to simulate the system call stack stack stack; int res = 0; - // Recursive: recursive call + // Recurse: recursive call for (int i = n; i > 0; i--) { - // Simulate "recursive" by "pushing onto the stack" + // Simulate "recurse" with "push" stack.push(i); } // Return: return result while (!stack.empty()) { - // Simulate "return" by "popping from the stack" + // Simulate "return" with "pop" res += stack.top(); stack.pop(); } @@ -995,19 +1628,19 @@ Therefore, **we can use an explicit stack to simulate the behavior of the call s === "Java" ```java title="recursion.java" - /* Simulate recursion with iteration */ + /* Simulate recursion using iteration */ int forLoopRecur(int n) { // Use an explicit stack to simulate the system call stack Stack stack = new Stack<>(); int res = 0; - // Recursive: recursive call + // Recurse: recursive call for (int i = n; i > 0; i--) { - // Simulate "recursive" by "pushing onto the stack" + // Simulate "recurse" with "push" stack.push(i); } // Return: return result while (!stack.isEmpty()) { - // Simulate "return" by "popping from the stack" + // Simulate "return" with "pop" res += stack.pop(); } // res = 1+2+3+...+n @@ -1018,72 +1651,238 @@ Therefore, **we can use an explicit stack to simulate the behavior of the call s === "C#" ```csharp title="recursion.cs" - [class]{recursion}-[func]{ForLoopRecur} + /* Simulate recursion using iteration */ + int ForLoopRecur(int n) { + // Use an explicit stack to simulate the system call stack + Stack stack = new(); + int res = 0; + // Recurse: recursive call + for (int i = n; i > 0; i--) { + // Simulate "recurse" with "push" + stack.Push(i); + } + // Return: return result + while (stack.Count > 0) { + // Simulate "return" with "pop" + res += stack.Pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Go" ```go title="recursion.go" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + func forLoopRecur(n int) int { + // Use an explicit stack to simulate the system call stack + stack := list.New() + res := 0 + // Recurse: recursive call + for i := n; i > 0; i-- { + // Simulate "recurse" with "push" + stack.PushBack(i) + } + // Return: return result + for stack.Len() != 0 { + // Simulate "return" with "pop" + res += stack.Back().Value.(int) + stack.Remove(stack.Back()) + } + // res = 1+2+3+...+n + return res + } ``` === "Swift" ```swift title="recursion.swift" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + func forLoopRecur(n: Int) -> Int { + // Use an explicit stack to simulate the system call stack + var stack: [Int] = [] + var res = 0 + // Recurse: recursive call + for i in (1 ... n).reversed() { + // Simulate "recurse" with "push" + stack.append(i) + } + // Return: return result + while !stack.isEmpty { + // Simulate "return" with "pop" + res += stack.removeLast() + } + // res = 1+2+3+...+n + return res + } ``` === "JS" ```javascript title="recursion.js" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + function forLoopRecur(n) { + // Use an explicit stack to simulate the system call stack + const stack = []; + let res = 0; + // Recurse: recursive call + for (let i = n; i > 0; i--) { + // Simulate "recurse" with "push" + stack.push(i); + } + // Return: return result + while (stack.length) { + // Simulate "return" with "pop" + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "TS" ```typescript title="recursion.ts" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + function forLoopRecur(n: number): number { + // Use an explicit stack to simulate the system call stack + const stack: number[] = []; + let res: number = 0; + // Recurse: recursive call + for (let i = n; i > 0; i--) { + // Simulate "recurse" with "push" + stack.push(i); + } + // Return: return result + while (stack.length) { + // Simulate "return" with "pop" + res += stack.pop(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Dart" ```dart title="recursion.dart" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + int forLoopRecur(int n) { + // Use an explicit stack to simulate the system call stack + List stack = []; + int res = 0; + // Recurse: recursive call + for (int i = n; i > 0; i--) { + // Simulate "recurse" with "push" + stack.add(i); + } + // Return: return result + while (!stack.isEmpty) { + // Simulate "return" with "pop" + res += stack.removeLast(); + } + // res = 1+2+3+...+n + return res; + } ``` === "Rust" ```rust title="recursion.rs" - [class]{}-[func]{for_loop_recur} + /* Simulate recursion using iteration */ + fn for_loop_recur(n: i32) -> i32 { + // Use an explicit stack to simulate the system call stack + let mut stack = Vec::new(); + let mut res = 0; + // Recurse: recursive call + for i in (1..=n).rev() { + // Simulate "recurse" with "push" + stack.push(i); + } + // Return: return result + while !stack.is_empty() { + // Simulate "return" with "pop" + res += stack.pop().unwrap(); + } + // res = 1+2+3+...+n + res + } ``` === "C" ```c title="recursion.c" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + int forLoopRecur(int n) { + int stack[1000]; // Use a large array to simulate stack + int top = -1; // Stack top index + int res = 0; + // Recurse: recursive call + for (int i = n; i > 0; i--) { + // Simulate "recurse" with "push" + stack[1 + top++] = i; + } + // Return: return result + while (top >= 0) { + // Simulate "return" with "pop" + res += stack[top--]; + } + // res = 1+2+3+...+n + return res; + } ``` === "Kotlin" ```kotlin title="recursion.kt" - [class]{}-[func]{forLoopRecur} + /* Simulate recursion using iteration */ + fun forLoopRecur(n: Int): Int { + // Use an explicit stack to simulate the system call stack + val stack = Stack() + var res = 0 + // Descend: recursive call + for (i in n downTo 0) { + // Simulate "recurse" with "push" + stack.push(i) + } + // Return: return result + while (stack.isNotEmpty()) { + // Simulate "return" with "pop" + res += stack.pop() + } + // res = 1+2+3+...+n + return res + } ``` === "Ruby" ```ruby title="recursion.rb" - [class]{}-[func]{for_loop_recur} + ### Use iteration to simulate recursion ### + def for_loop_recur(n) + # Use an explicit stack to simulate the system call stack + stack = [] + res = 0 + + # Recurse: recursive call + for i in n.downto(0) + # Simulate "recurse" with "push" + stack << i + end + # Return: return result + while !stack.empty? + res += stack.pop + end + + # res = 1+2+3+...+n + res + end ``` -=== "Zig" +Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can be converted into each other in many cases, it may not be worthwhile to do so for the following two reasons. - ```zig title="recursion.zig" - [class]{}-[func]{forLoopRecur} - ``` +- The transformed code may be more difficult to understand and less readable. +- For some complex problems, simulating the behavior of the system call stack can be very difficult. -Observing the above code, when recursion is transformed into iteration, the code becomes more complex. Although iteration and recursion can often be transformed into each other, it's not always advisable to do so for two reasons: - -- The transformed code may become more challenging to understand and less readable. -- For some complex problems, simulating the behavior of the system's call stack can be quite challenging. - -In conclusion, **whether to choose iteration or recursion depends on the specific nature of the problem**. In programming practice, it's crucial to weigh the pros and cons of both and choose the most suitable approach for the situation at hand. +In summary, **choosing between iteration and recursion depends on the nature of the specific problem**. In programming practice, it is crucial to weigh the pros and cons of both and choose the appropriate method based on the context. diff --git a/en/docs/chapter_computational_complexity/performance_evaluation.md b/en/docs/chapter_computational_complexity/performance_evaluation.md index bb090c137..9f612b366 100644 --- a/en/docs/chapter_computational_complexity/performance_evaluation.md +++ b/en/docs/chapter_computational_complexity/performance_evaluation.md @@ -2,52 +2,52 @@ comments: true --- -# 2.1   Algorithm efficiency assessment +# 2.1   Algorithm Efficiency Evaluation -In algorithm design, we pursue the following two objectives in sequence. +In algorithm design, we pursue the following two levels of objectives sequentially. -1. **Finding a Solution to the Problem**: The algorithm should reliably find the correct solution within the specified range of inputs. -2. **Seeking the Optimal Solution**: For the same problem, multiple solutions might exist, and we aim to find the most efficient algorithm possible. +1. **Finding a solution to the problem**: The algorithm must reliably obtain the correct solution within the specified input range. +2. **Seeking the optimal solution**: Multiple solutions may exist for the same problem, and we hope to find an algorithm that is as efficient as possible. -In other words, under the premise of being able to solve the problem, algorithm efficiency has become the main criterion for evaluating an algorithm, which includes the following two dimensions. +In other words, under the premise of being able to solve the problem, algorithm efficiency has become the primary evaluation criterion for measuring the quality of algorithms. It includes the following two dimensions. -- **Time efficiency**: The speed at which an algorithm runs. -- **Space efficiency**: The size of the memory space occupied by an algorithm. +- **Time efficiency**: The length of time the algorithm runs. +- **Space efficiency**: The size of memory space the algorithm occupies. -In short, **our goal is to design data structures and algorithms that are both fast and memory-efficient**. Effectively assessing algorithm efficiency is crucial because only then can we compare various algorithms and guide the process of algorithm design and optimization. +In short, **our goal is to design data structures and algorithms that are "both fast and memory-efficient"**. Effectively evaluating algorithm efficiency is crucial, because only in this way can we compare various algorithms and guide the algorithm design and optimization process. -There are mainly two methods of efficiency assessment: actual testing and theoretical estimation. +Efficiency evaluation methods are mainly divided into two types: actual testing and theoretical estimation. -## 2.1.1   Actual testing +## 2.1.1   Actual Testing -Suppose we have algorithms `A` and `B`, both capable of solving the same problem, and we need to compare their efficiencies. The most direct method is to use a computer to run these two algorithms, monitor and record their runtime and memory usage. This assessment method reflects the actual situation, but it has significant limitations. +Suppose we now have algorithm `A` and algorithm `B`, both of which can solve the same problem, and we need to compare the efficiency of these two algorithms. The most direct method is to find a computer, run these two algorithms, and monitor and record their running time and memory usage. This evaluation approach can reflect the real situation, but it also has considerable limitations. -On one hand, **it's difficult to eliminate interference from the testing environment**. Hardware configurations can affect algorithm performance. For example, an algorithm with a high degree of parallelism is better suited for running on multi-core CPUs, while an algorithm that involves intensive memory operations performs better with high-performance memory. The test results of an algorithm may vary across different machines. This means testing across multiple machines to calculate average efficiency becomes impractical. +On one hand, **it is difficult to eliminate interference factors from the testing environment**. Hardware configuration affects the performance of algorithms. For example, if an algorithm has a high degree of parallelism, it is more suitable for running on multi-core CPUs; if an algorithm has intensive memory operations, it will perform better on high-performance memory. In other words, the test results of an algorithm on different machines may be inconsistent. This means we need to test on various machines and calculate average efficiency, which is impractical. -On the other hand, **conducting a full test is very resource-intensive**. Algorithm efficiency varies with input data size. For example, with smaller data volumes, algorithm `A` might run faster than `B`, but with larger data volumes, the test results may be the opposite. Therefore, to draw convincing conclusions, we need to test a wide range of input data sizes, which requires excessive computational resources. +On the other hand, **conducting complete testing is very resource-intensive**. As the input data volume changes, the algorithm will exhibit different efficiencies. For example, when the input data volume is small, the running time of algorithm `A` is shorter than algorithm `B`; but when the input data volume is large, the test results may be exactly the opposite. Therefore, to obtain convincing conclusions, we need to test input data of various scales, which requires a large amount of computational resources. -## 2.1.2   Theoretical estimation +## 2.1.2   Theoretical Estimation -Due to the significant limitations of actual testing, we can consider evaluating algorithm efficiency solely through calculations. This estimation method is known as asymptotic complexity analysis, or simply complexity analysis. +Since actual testing has considerable limitations, we can consider evaluating algorithm efficiency through calculations alone. This estimation method is called asymptotic complexity analysis, or complexity analysis for short. -Complexity analysis reflects the relationship between the time and space resources required for algorithm execution and the size of the input data. **It describes the trend of growth in the time and space required by the algorithm as the size of the input data increases**. This definition might sound complex, but we can break it down into three key points to understand it better. +Complexity analysis can reflect the relationship between the time and space resources required for algorithm execution and the input data scale. **It describes the growth trend of the time and space required for algorithm execution as the input data scale increases**. This definition is somewhat convoluted, so we can break it down into three key points to understand. - "Time and space resources" correspond to time complexity and space complexity, respectively. -- "As the size of input data increases" means that complexity reflects the relationship between algorithm efficiency and the volume of input data. -- "The trend of growth in time and space" indicates that complexity analysis focuses not on the specific values of runtime or space occupied, but on the "rate" at which time or space increases. +- "As the input data scale increases" means that complexity reflects the relationship between algorithm running efficiency and input data scale. +- "Growth trend of time and space" indicates that complexity analysis focuses not on the specific values of running time or occupied space, but on how "fast" time or space grows. -**Complexity analysis overcomes the disadvantages of actual testing methods**, reflected in the following aspects: +**Complexity analysis overcomes the drawbacks of the actual testing method**, reflected in the following aspects. -- It does not require actually running the code, making it more environmentally friendly and energy efficient. -- It is independent of the testing environment and applicable to all operating platforms. -- It can reflect algorithm efficiency under different data volumes, especially in the performance of algorithms with large data volumes. +- It does not need to actually run the code, making it more environmentally friendly and energy-efficient. +- It is independent of the testing environment, and the analysis results are applicable to all running platforms. +- It can reflect algorithm efficiency at different data volumes, especially algorithm performance at large data volumes. !!! tip - If you're still confused about the concept of complexity, don't worry. We will cover it in detail in subsequent chapters. + If you are still confused about the concept of complexity, don't worry—we will introduce it in detail in subsequent chapters. -Complexity analysis provides us with a "ruler" to evaluate the efficiency of an algorithm, enabling us to measure the time and space resources required to execute it and compare the efficiency of different algorithms. +Complexity analysis provides us with a "ruler" for evaluating algorithm efficiency, allowing us to measure the time and space resources required to execute a certain algorithm and compare the efficiency between different algorithms. -Complexity is a mathematical concept that might be abstract and challenging for beginners. From this perspective, complexity analysis might not be the most suitable topic to introduce first. However, when discussing the characteristics of a particular data structure or algorithm, it's hard to avoid analyzing its speed and space usage. +Complexity is a mathematical concept that may be relatively abstract for beginners, with a relatively high learning difficulty. From this perspective, complexity analysis may not be very suitable as the first content to be introduced. However, when we discuss the characteristics of a certain data structure or algorithm, it is difficult to avoid analyzing its running speed and space usage. -In summary, it is recommended to develop a basic understanding of complexity analysis before diving deep into data structures and algorithms, **so that you can perform complexity analysis on simple algorithms**. \ No newline at end of file +In summary, it is recommended that before diving deep into data structures and algorithms, **you first establish a preliminary understanding of complexity analysis so that you can complete complexity analysis of simple algorithms**. diff --git a/en/docs/chapter_computational_complexity/space_complexity.md b/en/docs/chapter_computational_complexity/space_complexity.md index 20c5571b8..7a79c445e 100644 --- a/en/docs/chapter_computational_complexity/space_complexity.md +++ b/en/docs/chapter_computational_complexity/space_complexity.md @@ -2,135 +2,134 @@ comments: true --- -# 2.4   Space complexity +# 2.4   Space Complexity -Space complexity is used to measure the growth trend of the memory space occupied by an algorithm as the amount of data increases. This concept is very similar to time complexity, except that "running time" is replaced with "occupied memory space". +Space complexity measures the growth trend of memory space occupied by an algorithm as the data size increases. This concept is very similar to time complexity, except that "running time" is replaced with "occupied memory space". -## 2.4.1   Space related to algorithms +## 2.4.1   Algorithm-Related Space -The memory space used by an algorithm during its execution mainly includes the following types. +The memory space used by an algorithm during execution mainly includes the following types. - **Input space**: Used to store the input data of the algorithm. - **Temporary space**: Used to store variables, objects, function contexts, and other data during the algorithm's execution. - **Output space**: Used to store the output data of the algorithm. -Generally, the scope of space complexity statistics includes both "Temporary Space" and "Output Space". +In general, the scope of space complexity statistics is "temporary space" plus "output space". Temporary space can be further divided into three parts. - **Temporary data**: Used to save various constants, variables, objects, etc., during the algorithm's execution. -- **Stack frame space**: Used to save the context data of the called function. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns. -- **Instruction space**: Used to store compiled program instructions, which are usually negligible in actual statistics. +- **Stack frame space**: Used to save the context data of called functions. The system creates a stack frame at the top of the stack each time a function is called, and the stack frame space is released after the function returns. +- **Instruction space**: Used to save compiled program instructions, which are usually ignored in actual statistics. -When analyzing the space complexity of a program, **we typically count the Temporary Data, Stack Frame Space, and Output Data**, as shown in Figure 2-15. +When analyzing the space complexity of a program, **we usually count three parts: temporary data, stack frame space, and output data**, as shown in the following figure. -![Space types used in algorithms](space_complexity.assets/space_types.png){ class="animation-figure" } +![Algorithm-related space](space_complexity.assets/space_types.png){ class="animation-figure" } -

Figure 2-15   Space types used in algorithms

+

Figure 2-15   Algorithm-related space

-The relevant code is as follows: +The related code is as follows: === "Python" ```python title="" class Node: - """Classes""" + """Class""" def __init__(self, x: int): - self.val: int = x # node value - self.next: Node | None = None # reference to the next node + self.val: int = x # Node value + self.next: Node | None = None # Reference to the next node def function() -> int: - """Functions""" - # Perform certain operations... + """Function""" + # Perform some operations... return 0 - def algorithm(n) -> int: # input data - A = 0 # temporary data (constant, usually in uppercase) - b = 0 # temporary data (variable) - node = Node(0) # temporary data (object) - c = function() # Stack frame space (call function) - return A + b + c # output data + def algorithm(n) -> int: # Input data + A = 0 # Temporary data (constant, usually represented by uppercase letters) + b = 0 # Temporary data (variable) + node = Node(0) # Temporary data (object) + c = function() # Stack frame space (function call) + return A + b + c # Output data ``` === "C++" ```cpp title="" - /* Structures */ + /* Structure */ struct Node { int val; Node *next; Node(int x) : val(x), next(nullptr) {} }; - /* Functions */ + /* Function */ int func() { - // Perform certain operations... + // Perform some operations... return 0; } - int algorithm(int n) { // input data - const int a = 0; // temporary data (constant) - int b = 0; // temporary data (variable) - Node* node = new Node(0); // temporary data (object) - int c = func(); // stack frame space (call function) - return a + b + c; // output data + int algorithm(int n) { // Input data + const int a = 0; // Temporary data (constant) + int b = 0; // Temporary data (variable) + Node* node = new Node(0); // Temporary data (object) + int c = func(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "Java" ```java title="" - /* Classes */ + /* Class */ class Node { int val; Node next; Node(int x) { val = x; } } - - /* Functions */ + + /* Function */ int function() { - // Perform certain operations... + // Perform some operations... return 0; } - - int algorithm(int n) { // input data - final int a = 0; // temporary data (constant) - int b = 0; // temporary data (variable) - Node node = new Node(0); // temporary data (object) - int c = function(); // stack frame space (call function) - return a + b + c; // output data + + int algorithm(int n) { // Input data + final int a = 0; // Temporary data (constant) + int b = 0; // Temporary data (variable) + Node node = new Node(0); // Temporary data (object) + int c = function(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "C#" ```csharp title="" - /* Classes */ - class Node { - int val; + /* Class */ + class Node(int x) { + int val = x; Node next; - Node(int x) { val = x; } } - /* Functions */ + /* Function */ int Function() { - // Perform certain operations... + // Perform some operations... return 0; } - int Algorithm(int n) { // input data - const int a = 0; // temporary data (constant) - int b = 0; // temporary data (variable) - Node node = new(0); // temporary data (object) - int c = Function(); // stack frame space (call function) - return a + b + c; // output data + int Algorithm(int n) { // Input data + const int a = 0; // Temporary data (constant) + int b = 0; // Temporary data (variable) + Node node = new(0); // Temporary data (object) + int c = Function(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "Go" ```go title="" - /* Structures */ + /* Structure */ type node struct { val int next *node @@ -140,26 +139,26 @@ The relevant code is as follows: func newNode(val int) *node { return &node{val: val} } - - /* Functions */ + + /* Function */ func function() int { - // Perform certain operations... + // Perform some operations... return 0 } - func algorithm(n int) int { // input data - const a = 0 // temporary data (constant) - b := 0 // temporary storage of data (variable) - newNode(0) // temporary data (object) - c := function() // stack frame space (call function) - return a + b + c // output data + func algorithm(n int) int { // Input data + const a = 0 // Temporary data (constant) + b := 0 // Temporary data (variable) + newNode(0) // Temporary data (object) + c := function() // Stack frame space (function call) + return a + b + c // Output data } ``` === "Swift" ```swift title="" - /* Classes */ + /* Class */ class Node { var val: Int var next: Node? @@ -169,99 +168,99 @@ The relevant code is as follows: } } - /* Functions */ + /* Function */ func function() -> Int { - // Perform certain operations... + // Perform some operations... return 0 } - func algorithm(n: Int) -> Int { // input data - let a = 0 // temporary data (constant) - var b = 0 // temporary data (variable) - let node = Node(x: 0) // temporary data (object) - let c = function() // stack frame space (call function) - return a + b + c // output data + func algorithm(n: Int) -> Int { // Input data + let a = 0 // Temporary data (constant) + var b = 0 // Temporary data (variable) + let node = Node(x: 0) // Temporary data (object) + let c = function() // Stack frame space (function call) + return a + b + c // Output data } ``` === "JS" ```javascript title="" - /* Classes */ + /* Class */ class Node { val; next; constructor(val) { - this.val = val === undefined ? 0 : val; // node value - this.next = null; // reference to the next node + this.val = val === undefined ? 0 : val; // Node value + this.next = null; // Reference to the next node } } - /* Functions */ + /* Function */ function constFunc() { - // Perform certain operations + // Perform some operations return 0; } - function algorithm(n) { // input data - const a = 0; // temporary data (constant) - let b = 0; // temporary data (variable) - const node = new Node(0); // temporary data (object) - const c = constFunc(); // Stack frame space (calling function) - return a + b + c; // output data + function algorithm(n) { // Input data + const a = 0; // Temporary data (constant) + let b = 0; // Temporary data (variable) + const node = new Node(0); // Temporary data (object) + const c = constFunc(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "TS" ```typescript title="" - /* Classes */ + /* Class */ class Node { val: number; next: Node | null; constructor(val?: number) { - this.val = val === undefined ? 0 : val; // node value - this.next = null; // reference to the next node + this.val = val === undefined ? 0 : val; // Node value + this.next = null; // Reference to the next node } } - /* Functions */ + /* Function */ function constFunc(): number { - // Perform certain operations + // Perform some operations return 0; } - function algorithm(n: number): number { // input data - const a = 0; // temporary data (constant) - let b = 0; // temporary data (variable) - const node = new Node(0); // temporary data (object) - const c = constFunc(); // Stack frame space (calling function) - return a + b + c; // output data + function algorithm(n: number): number { // Input data + const a = 0; // Temporary data (constant) + let b = 0; // Temporary data (variable) + const node = new Node(0); // Temporary data (object) + const c = constFunc(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "Dart" ```dart title="" - /* Classes */ + /* Class */ class Node { int val; Node next; Node(this.val, [this.next]); } - /* Functions */ + /* Function */ int function() { - // Perform certain operations... + // Perform some operations... return 0; } - int algorithm(int n) { // input data - const int a = 0; // temporary data (constant) - int b = 0; // temporary data (variable) - Node node = Node(0); // temporary data (object) - int c = function(); // stack frame space (call function) - return a + b + c; // output data + int algorithm(int n) { // Input data + const int a = 0; // Temporary data (constant) + int b = 0; // Temporary data (variable) + Node node = Node(0); // Temporary data (object) + int c = function(); // Stack frame space (function call) + return a + b + c; // Output data } ``` @@ -270,74 +269,114 @@ The relevant code is as follows: ```rust title="" use std::rc::Rc; use std::cell::RefCell; - - /* Structures */ + + /* Structure */ struct Node { val: i32, next: Option>>, } - /* Constructor */ + /* Create Node structure */ impl Node { fn new(val: i32) -> Self { Self { val: val, next: None } } } - /* Functions */ - fn function() -> i32 { - // Perform certain operations... + /* Function */ + fn function() -> i32 { + // Perform some operations... return 0; } - fn algorithm(n: i32) -> i32 { // input data - const a: i32 = 0; // temporary data (constant) - let mut b = 0; // temporary data (variable) - let node = Node::new(0); // temporary data (object) - let c = function(); // stack frame space (call function) - return a + b + c; // output data + fn algorithm(n: i32) -> i32 { // Input data + const a: i32 = 0; // Temporary data (constant) + let mut b = 0; // Temporary data (variable) + let node = Node::new(0); // Temporary data (object) + let c = function(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "C" ```c title="" - /* Functions */ + /* Function */ int func() { - // Perform certain operations... + // Perform some operations... return 0; } - int algorithm(int n) { // input data - const int a = 0; // temporary data (constant) - int b = 0; // temporary data (variable) - int c = func(); // stack frame space (call function) - return a + b + c; // output data + int algorithm(int n) { // Input data + const int a = 0; // Temporary data (constant) + int b = 0; // Temporary data (variable) + int c = func(); // Stack frame space (function call) + return a + b + c; // Output data } ``` === "Kotlin" ```kotlin title="" + /* Class */ + class Node(var _val: Int) { + var next: Node? = null + } + /* Function */ + fun function(): Int { + // Perform some operations... + return 0 + } + + fun algorithm(n: Int): Int { // Input data + val a = 0 // Temporary data (constant) + var b = 0 // Temporary data (variable) + val node = Node(0) // Temporary data (object) + val c = function() // Stack frame space (function call) + return a + b + c // Output data + } ``` -=== "Zig" +=== "Ruby" - ```zig title="" + ```ruby title="" + ### Class ### + class Node + attr_accessor :val # Node value + attr_accessor :next # Reference to the next node + def initialize(x) + @val = x + end + end + + ### Function ### + def function + # Perform some operations... + 0 + end + + ### Algorithm ### + def algorithm(n) # Input data + a = 0 # Temporary data (constant) + b = 0 # Temporary data (variable) + node = Node.new(0) # Temporary data (object) + c = function # Stack frame space (function call) + a + b + c # Output data + end ``` -## 2.4.2   Calculation method +## 2.4.2   Calculation Method -The method for calculating space complexity is roughly similar to that of time complexity, with the only change being the shift of the statistical object from "number of operations" to "size of used space". +The calculation method for space complexity is roughly the same as for time complexity, except that the statistical object is changed from "number of operations" to "size of space used". -However, unlike time complexity, **we usually only focus on the worst-case space complexity**. This is because memory space is a hard requirement, and we must ensure that there is enough memory space reserved under all input data. +Unlike time complexity, **we usually only focus on the worst-case space complexity**. This is because memory space is a hard requirement, and we must ensure that sufficient memory space is reserved for all input data. -Consider the following code, the term "worst-case" in worst-case space complexity has two meanings. +Observe the following code. The "worst case" in worst-case space complexity has two meanings. -1. **Based on the worst input data**: When $n < 10$, the space complexity is $O(1)$; but when $n > 10$, the initialized array `nums` occupies $O(n)$ space, thus the worst-case space complexity is $O(n)$. -2. **Based on the peak memory used during the algorithm's execution**: For example, before executing the last line, the program occupies $O(1)$ space; when initializing the array `nums`, the program occupies $O(n)$ space, hence the worst-case space complexity is $O(n)$. +1. **Based on the worst input data**: When $n < 10$, the space complexity is $O(1)$; but when $n > 10$, the initialized array `nums` occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. +2. **Based on the peak memory during algorithm execution**: For example, before executing the last line, the program occupies $O(1)$ space; when initializing the array `nums`, the program occupies $O(n)$ space, so the worst-case space complexity is $O(n)$. === "Python" @@ -449,10 +488,10 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```rust title="" fn algorithm(n: i32) { - let a = 0; // O(1) - let b = [0; 10000]; // O(1) + let a = 0; // O(1) + let b = [0; 10000]; // O(1) if n > 10 { - let nums = vec![0; n as usize]; // O(n) + let nums = vec![0; n as usize]; // O(n) } } ``` @@ -471,31 +510,41 @@ Consider the following code, the term "worst-case" in worst-case space complexit === "Kotlin" ```kotlin title="" - + fun algorithm(n: Int) { + val a = 0 // O(1) + val b = IntArray(10000) // O(1) + if (n > 10) { + val nums = IntArray(n) // O(n) + } + } ``` -=== "Zig" - - ```zig title="" +=== "Ruby" + ```ruby title="" + def algorithm(n) + a = 0 # O(1) + b = Array.new(10000) # O(1) + nums = Array.new(n) if n > 10 # O(n) + end ``` -**In recursive functions, stack frame space must be taken into count**. Consider the following code: +**In recursive functions, it is necessary to count the stack frame space**. Observe the following code: === "Python" ```python title="" def function() -> int: - # Perform certain operations + # Perform some operations return 0 def loop(n: int): - """Loop O(1)""" + """Loop has space complexity of O(1)""" for _ in range(n): function() def recur(n: int): - """Recursion O(n)""" + """Recursion has space complexity of O(n)""" if n == 1: return return recur(n - 1) @@ -505,16 +554,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```cpp title="" int func() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { func(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -525,16 +574,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```java title="" int function() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { function(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -545,16 +594,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```csharp title="" int Function() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ void Loop(int n) { for (int i = 0; i < n; i++) { Function(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ int Recur(int n) { if (n == 1) return 1; return Recur(n - 1); @@ -565,18 +614,18 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```go title="" func function() int { - // Perform certain operations + // Perform some operations return 0 } - - /* Cycle O(1) */ + + /* Loop has space complexity of O(1) */ func loop(n int) { for i := 0; i < n; i++ { function() } } - - /* Recursion O(n) */ + + /* Recursion has space complexity of O(n) */ func recur(n int) { if n == 1 { return @@ -590,18 +639,18 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```swift title="" @discardableResult func function() -> Int { - // Perform certain operations + // Perform some operations return 0 } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ func loop(n: Int) { for _ in 0 ..< n { function() } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ func recur(n: Int) { if n == 1 { return @@ -614,16 +663,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```javascript title="" function constFunc() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ function loop(n) { for (let i = 0; i < n; i++) { constFunc(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ function recur(n) { if (n === 1) return; return recur(n - 1); @@ -634,16 +683,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```typescript title="" function constFunc(): number { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ function loop(n: number): void { for (let i = 0; i < n; i++) { constFunc(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ function recur(n: number): void { if (n === 1) return; return recur(n - 1); @@ -654,16 +703,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```dart title="" int function() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { function(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -674,17 +723,17 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```rust title="" fn function() -> i32 { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ fn loop(n: i32) { for i in 0..n { function(); } } - /* Recursion O(n) */ - void recur(n: i32) { + /* Recursion has space complexity of O(n) */ + fn recur(n: i32) { if n == 1 { return; } @@ -696,16 +745,16 @@ Consider the following code, the term "worst-case" in worst-case space complexit ```c title="" int func() { - // Perform certain operations + // Perform some operations return 0; } - /* Cycle O(1) */ + /* Loop has space complexity of O(1) */ void loop(int n) { for (int i = 0; i < n; i++) { func(); } } - /* Recursion O(n) */ + /* Recursion has space complexity of O(n) */ void recur(int n) { if (n == 1) return; recur(n - 1); @@ -715,28 +764,56 @@ Consider the following code, the term "worst-case" in worst-case space complexit === "Kotlin" ```kotlin title="" - + fun function(): Int { + // Perform some operations + return 0 + } + /* Loop has space complexity of O(1) */ + fun loop(n: Int) { + for (i in 0.. Figure 2-16   Common types of space complexity

-### 1.   Constant order $O(1)$ {data-toc-label="1.   Constant order"} +### 1.   Constant Order $O(1)$ {data-toc-label="1.   Constant Order"} -Constant order is common in constants, variables, objects that are independent of the size of input data $n$. +Constant order is common in constants, variables, and objects whose quantity is independent of the input data size $n$. -Note that memory occupied by initializing variables or calling functions in a loop, which is released upon entering the next cycle, does not accumulate over space, thus the space complexity remains $O(1)$: +It should be noted that memory occupied by initializing variables or calling functions in a loop is released when entering the next iteration, so it does not accumulate space, and the space complexity remains $O(1)$: === "Python" @@ -759,15 +836,15 @@ Note that memory occupied by initializing variables or calling functions in a lo return 0 def constant(n: int): - """Constant complexity""" + """Constant order""" # Constants, variables, objects occupy O(1) space a = 0 nums = [0] * 10000 node = ListNode(0) - # Variables in a loop occupy O(1) space + # Variables in the loop occupy O(1) space for _ in range(n): c = 0 - # Functions in a loop occupy O(1) space + # Functions in the loop occupy O(1) space for _ in range(n): function() ``` @@ -781,18 +858,18 @@ Note that memory occupied by initializing variables or calling functions in a lo return 0; } - /* Constant complexity */ + /* Constant order */ void constant(int n) { // Constants, variables, objects occupy O(1) space const int a = 0; int b = 0; vector nums(10000); ListNode node(0); - // Variables in a loop occupy O(1) space + // Variables in the loop occupy O(1) space for (int i = 0; i < n; i++) { int c = 0; } - // Functions in a loop occupy O(1) space + // Functions in the loop occupy O(1) space for (int i = 0; i < n; i++) { func(); } @@ -808,18 +885,18 @@ Note that memory occupied by initializing variables or calling functions in a lo return 0; } - /* Constant complexity */ + /* Constant order */ void constant(int n) { // Constants, variables, objects occupy O(1) space final int a = 0; int b = 0; int[] nums = new int[10000]; ListNode node = new ListNode(0); - // Variables in a loop occupy O(1) space + // Variables in the loop occupy O(1) space for (int i = 0; i < n; i++) { int c = 0; } - // Functions in a loop occupy O(1) space + // Functions in the loop occupy O(1) space for (int i = 0; i < n; i++) { function(); } @@ -829,92 +906,278 @@ Note that memory occupied by initializing variables or calling functions in a lo === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Function} + /* Function */ + int Function() { + // Perform some operations + return 0; + } - [class]{space_complexity}-[func]{Constant} + /* Constant order */ + void Constant(int n) { + // Constants, variables, objects occupy O(1) space + int a = 0; + int b = 0; + int[] nums = new int[10000]; + ListNode node = new(0); + // Variables in the loop occupy O(1) space + for (int i = 0; i < n; i++) { + int c = 0; + } + // Functions in the loop occupy O(1) space + for (int i = 0; i < n; i++) { + Function(); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{function} + /* Function */ + func function() int { + // Perform some operations... + return 0 + } - [class]{}-[func]{spaceConstant} + /* Constant order */ + func spaceConstant(n int) { + // Constants, variables, objects occupy O(1) space + const a = 0 + b := 0 + nums := make([]int, 10000) + node := newNode(0) + // Variables in the loop occupy O(1) space + var c int + for i := 0; i < n; i++ { + c = 0 + } + // Functions in the loop occupy O(1) space + for i := 0; i < n; i++ { + function() + } + b += 0 + c += 0 + nums[0] = 0 + node.val = 0 + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{function} + /* Function */ + @discardableResult + func function() -> Int { + // Perform some operations + return 0 + } - [class]{}-[func]{constant} + /* Constant order */ + func constant(n: Int) { + // Constants, variables, objects occupy O(1) space + let a = 0 + var b = 0 + let nums = Array(repeating: 0, count: 10000) + let node = ListNode(x: 0) + // Variables in the loop occupy O(1) space + for _ in 0 ..< n { + let c = 0 + } + // Functions in the loop occupy O(1) space + for _ in 0 ..< n { + function() + } + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{constFunc} + /* Function */ + function constFunc() { + // Perform some operations + return 0; + } - [class]{}-[func]{constant} + /* Constant order */ + function constant(n) { + // Constants, variables, objects occupy O(1) space + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // Variables in the loop occupy O(1) space + for (let i = 0; i < n; i++) { + const c = 0; + } + // Functions in the loop occupy O(1) space + for (let i = 0; i < n; i++) { + constFunc(); + } + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{constFunc} + /* Function */ + function constFunc(): number { + // Perform some operations + return 0; + } - [class]{}-[func]{constant} + /* Constant order */ + function constant(n: number): void { + // Constants, variables, objects occupy O(1) space + const a = 0; + const b = 0; + const nums = new Array(10000); + const node = new ListNode(0); + // Variables in the loop occupy O(1) space + for (let i = 0; i < n; i++) { + const c = 0; + } + // Functions in the loop occupy O(1) space + for (let i = 0; i < n; i++) { + constFunc(); + } + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{function} + /* Function */ + int function() { + // Perform some operations + return 0; + } - [class]{}-[func]{constant} + /* Constant order */ + void constant(int n) { + // Constants, variables, objects occupy O(1) space + final int a = 0; + int b = 0; + List nums = List.filled(10000, 0); + ListNode node = ListNode(0); + // Variables in the loop occupy O(1) space + for (var i = 0; i < n; i++) { + int c = 0; + } + // Functions in the loop occupy O(1) space + for (var i = 0; i < n; i++) { + function(); + } + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{function} + /* Function */ + fn function() -> i32 { + // Perform some operations + return 0; + } - [class]{}-[func]{constant} + /* Constant order */ + #[allow(unused)] + fn constant(n: i32) { + // Constants, variables, objects occupy O(1) space + const A: i32 = 0; + let b = 0; + let nums = vec![0; 10000]; + let node = ListNode::new(0); + // Variables in the loop occupy O(1) space + for i in 0..n { + let c = 0; + } + // Functions in the loop occupy O(1) space + for i in 0..n { + function(); + } + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{func} + /* Function */ + int func() { + // Perform some operations + return 0; + } - [class]{}-[func]{constant} + /* Constant order */ + void constant(int n) { + // Constants, variables, objects occupy O(1) space + const int a = 0; + int b = 0; + int nums[1000]; + ListNode *node = newListNode(0); + free(node); + // Variables in the loop occupy O(1) space + for (int i = 0; i < n; i++) { + int c = 0; + } + // Functions in the loop occupy O(1) space + for (int i = 0; i < n; i++) { + func(); + } + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{function} + /* Function */ + fun function(): Int { + // Perform some operations + return 0 + } - [class]{}-[func]{constant} + /* Constant order */ + fun constant(n: Int) { + // Constants, variables, objects occupy O(1) space + val a = 0 + var b = 0 + val nums = Array(10000) { 0 } + val node = ListNode(0) + // Variables in the loop occupy O(1) space + for (i in 0.. nums(n); // A list of length n occupies O(n) space vector nodes; @@ -954,9 +1217,9 @@ Linear order is common in arrays, linked lists, stacks, queues, etc., where the === "Java" ```java title="space_complexity.java" - /* Linear complexity */ + /* Linear order */ void linear(int n) { - // Array of length n occupies O(n) space + // Array of length n uses O(n) space int[] nums = new int[n]; // A list of length n occupies O(n) space List nodes = new ArrayList<>(); @@ -974,79 +1237,227 @@ Linear order is common in arrays, linked lists, stacks, queues, etc., where the === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Linear} + /* Linear order */ + void Linear(int n) { + // Array of length n uses O(n) space + int[] nums = new int[n]; + // A list of length n occupies O(n) space + List nodes = []; + for (int i = 0; i < n; i++) { + nodes.Add(new ListNode(i)); + } + // A hash table of length n occupies O(n) space + Dictionary map = []; + for (int i = 0; i < n; i++) { + map.Add(i, i.ToString()); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceLinear} + /* Linear order */ + func spaceLinear(n int) { + // Array of length n uses O(n) space + _ = make([]int, n) + // A list of length n occupies O(n) space + var nodes []*node + for i := 0; i < n; i++ { + nodes = append(nodes, newNode(i)) + } + // A hash table of length n occupies O(n) space + m := make(map[int]string, n) + for i := 0; i < n; i++ { + m[i] = strconv.Itoa(i) + } + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{linear} + /* Linear order */ + func linear(n: Int) { + // Array of length n uses O(n) space + let nums = Array(repeating: 0, count: n) + // A list of length n occupies O(n) space + let nodes = (0 ..< n).map { ListNode(x: $0) } + // A hash table of length n occupies O(n) space + let map = Dictionary(uniqueKeysWithValues: (0 ..< n).map { ($0, "\($0)") }) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{linear} + /* Linear order */ + function linear(n) { + // Array of length n uses O(n) space + const nums = new Array(n); + // A list of length n occupies O(n) space + const nodes = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // A hash table of length n occupies O(n) space + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{linear} + /* Linear order */ + function linear(n: number): void { + // Array of length n uses O(n) space + const nums = new Array(n); + // A list of length n occupies O(n) space + const nodes: ListNode[] = []; + for (let i = 0; i < n; i++) { + nodes.push(new ListNode(i)); + } + // A hash table of length n occupies O(n) space + const map = new Map(); + for (let i = 0; i < n; i++) { + map.set(i, i.toString()); + } + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{linear} + /* Linear order */ + void linear(int n) { + // Array of length n uses O(n) space + List nums = List.filled(n, 0); + // A list of length n occupies O(n) space + List nodes = []; + for (var i = 0; i < n; i++) { + nodes.add(ListNode(i)); + } + // A hash table of length n occupies O(n) space + Map map = HashMap(); + for (var i = 0; i < n; i++) { + map.putIfAbsent(i, () => i.toString()); + } + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{linear} + /* Linear order */ + #[allow(unused)] + fn linear(n: i32) { + // Array of length n uses O(n) space + let mut nums = vec![0; n as usize]; + // A list of length n occupies O(n) space + let mut nodes = Vec::new(); + for i in 0..n { + nodes.push(ListNode::new(i)) + } + // A hash table of length n occupies O(n) space + let mut map = HashMap::new(); + for i in 0..n { + map.insert(i, i.to_string()); + } + } ``` === "C" ```c title="space_complexity.c" - [class]{HashTable}-[func]{} + /* Hash table */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // Implemented using uthash.h + } HashTable; - [class]{}-[func]{linear} + /* Linear order */ + void linear(int n) { + // Array of length n uses O(n) space + int *nums = malloc(sizeof(int) * n); + free(nums); + + // A list of length n occupies O(n) space + ListNode **nodes = malloc(sizeof(ListNode *) * n); + for (int i = 0; i < n; i++) { + nodes[i] = newListNode(i); + } + // Memory release + for (int i = 0; i < n; i++) { + free(nodes[i]); + } + free(nodes); + + // A hash table of length n occupies O(n) space + 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); + } + + // Memory release + HashTable *curr, *tmp; + HASH_ITER(hh, h, curr, tmp) { + HASH_DEL(h, curr); + free(curr); + } + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{linear} + /* Linear order */ + fun linear(n: Int) { + // Array of length n uses O(n) space + val nums = Array(n) { 0 } + // A list of length n occupies O(n) space + val nodes = mutableListOf() + for (i in 0..() + for (i in 0.. Figure 2-17   Linear order space complexity generated by recursive function

-![Recursive function generating linear order space complexity](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" } +### 3.   Quadratic Order $O(n^2)$ {data-toc-label="3.   Quadratic Order"} -

Figure 2-17   Recursive function generating linear order space complexity

- -### 3.   Quadratic order $O(n^2)$ {data-toc-label="3.   Quadratic order"} - -Quadratic order is common in matrices and graphs, where the number of elements is quadratic to $n$: +Quadratic order is common in matrices and graphs, where the number of elements is quadratically related to $n$: === "Python" ```python title="space_complexity.py" def quadratic(n: int): - """Quadratic complexity""" - # A two-dimensional list occupies O(n^2) space + """Quadratic order""" + # A 2D list occupies O(n^2) space num_matrix = [[0] * n for _ in range(n)] ``` === "C++" ```cpp title="space_complexity.cpp" - /* Quadratic complexity */ + /* Exponential order */ void quadratic(int n) { - // A two-dimensional list occupies O(n^2) space + // 2D list uses O(n^2) space vector> numMatrix; for (int i = 0; i < n; i++) { vector tmp; @@ -1179,11 +1642,11 @@ Quadratic order is common in matrices and graphs, where the number of elements i === "Java" ```java title="space_complexity.java" - /* Quadratic complexity */ + /* Exponential order */ void quadratic(int n) { - // Matrix occupies O(n^2) space + // Matrix uses O(n^2) space int[][] numMatrix = new int[n][n]; - // A two-dimensional list occupies O(n^2) space + // 2D list uses O(n^2) space List> numList = new ArrayList<>(); for (int i = 0; i < n; i++) { List tmp = new ArrayList<>(); @@ -1198,79 +1661,188 @@ Quadratic order is common in matrices and graphs, where the number of elements i === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{Quadratic} + /* Exponential order */ + void Quadratic(int n) { + // Matrix uses O(n^2) space + int[,] numMatrix = new int[n, n]; + // 2D list uses O(n^2) space + List> numList = []; + for (int i = 0; i < n; i++) { + List tmp = []; + for (int j = 0; j < n; j++) { + tmp.Add(0); + } + numList.Add(tmp); + } + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceQuadratic} + /* Exponential order */ + func spaceQuadratic(n int) { + // Matrix uses O(n^2) space + numMatrix := make([][]int, n) + for i := 0; i < n; i++ { + numMatrix[i] = make([]int, n) + } + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{quadratic} + /* Exponential order */ + func quadratic(n: Int) { + // 2D list uses O(n^2) space + let numList = Array(repeating: Array(repeating: 0, count: n), count: n) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{quadratic} + /* Exponential order */ + function quadratic(n) { + // Matrix uses O(n^2) space + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 2D list uses O(n^2) space + 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" - [class]{}-[func]{quadratic} + /* Exponential order */ + function quadratic(n: number): void { + // Matrix uses O(n^2) space + const numMatrix = Array(n) + .fill(null) + .map(() => Array(n).fill(null)); + // 2D list uses O(n^2) space + 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" - [class]{}-[func]{quadratic} + /* Exponential order */ + void quadratic(int n) { + // Matrix uses O(n^2) space + List> numMatrix = List.generate(n, (_) => List.filled(n, 0)); + // 2D list uses O(n^2) space + 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" - [class]{}-[func]{quadratic} + /* Exponential order */ + #[allow(unused)] + fn quadratic(n: i32) { + // Matrix uses O(n^2) space + let num_matrix = vec![vec![0; n as usize]; n as usize]; + // 2D list uses O(n^2) space + 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" - [class]{}-[func]{quadratic} + /* Exponential order */ + void quadratic(int n) { + // 2D list uses O(n^2) space + 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; + } + + // Memory release + for (int i = 0; i < n; i++) { + free(numMatrix[i]); + } + free(numMatrix); + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{quadratic} + /* Exponential order */ + fun quadratic(n: Int) { + // Matrix uses O(n^2) space + val numMatrix = arrayOfNulls?>(n) + // 2D list uses O(n^2) space + val numList = mutableListOf>() + for (i in 0..() + for (j in 0.. int: - """Quadratic complexity (recursive implementation)""" + """Quadratic order (recursive implementation)""" if n <= 0: return 0 - # Array nums length = n, n-1, ..., 2, 1 + # Array nums length is n, n-1, ..., 2, 1 nums = [0] * n return quadratic_recur(n - 1) ``` @@ -1278,12 +1850,12 @@ As shown in Figure 2-18, the recursive depth of this function is $n$, and in eac === "C++" ```cpp title="space_complexity.cpp" - /* Quadratic complexity (recursive implementation) */ + /* Quadratic order (recursive implementation) */ int quadraticRecur(int n) { if (n <= 0) return 0; vector nums(n); - cout << "Recursive n = " << n << ", length of nums = " << nums.size() << endl; + cout << "In recursion n = " << n << ", nums length = " << nums.size() << endl; return quadraticRecur(n - 1); } ``` @@ -1291,13 +1863,13 @@ As shown in Figure 2-18, the recursive depth of this function is $n$, and in eac === "Java" ```java title="space_complexity.java" - /* Quadratic complexity (recursive implementation) */ + /* Quadratic order (recursive implementation) */ int quadraticRecur(int n) { if (n <= 0) return 0; - // Array nums length = n, n-1, ..., 2, 1 + // Array nums has length n, n-1, ..., 2, 1 int[] nums = new int[n]; - System.out.println("Recursion n = " + n + " in the length of nums = " + nums.length); + System.out.println("In recursion n = " + n + ", nums length = " + nums.length); return quadraticRecur(n - 1); } ``` @@ -1305,82 +1877,151 @@ As shown in Figure 2-18, the recursive depth of this function is $n$, and in eac === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{QuadraticRecur} + /* Quadratic order (recursive implementation) */ + int QuadraticRecur(int n) { + if (n <= 0) return 0; + int[] nums = new int[n]; + Console.WriteLine("Recursion n = " + n + ", nums length = " + nums.Length); + return QuadraticRecur(n - 1); + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{spaceQuadraticRecur} + /* Quadratic order (recursive implementation) */ + func spaceQuadraticRecur(n int) int { + if n <= 0 { + return 0 + } + nums := make([]int, n) + fmt.Printf("In recursion n = %d, nums length = %d \n", n, len(nums)) + return spaceQuadraticRecur(n - 1) + } ``` === "Swift" ```swift title="space_complexity.swift" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + @discardableResult + func quadraticRecur(n: Int) -> Int { + if n <= 0 { + return 0 + } + // Array nums has length n, n-1, ..., 2, 1 + let nums = Array(repeating: 0, count: n) + print("In recursion n = \(n), nums length = \(nums.count)") + return quadraticRecur(n: n - 1) + } ``` === "JS" ```javascript title="space_complexity.js" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + function quadraticRecur(n) { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`In recursion n = ${n}, nums length = ${nums.length}`); + return quadraticRecur(n - 1); + } ``` === "TS" ```typescript title="space_complexity.ts" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + function quadraticRecur(n: number): number { + if (n <= 0) return 0; + const nums = new Array(n); + console.log(`In recursion n = ${n}, nums length = ${nums.length}`); + return quadraticRecur(n - 1); + } ``` === "Dart" ```dart title="space_complexity.dart" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + int quadraticRecur(int n) { + if (n <= 0) return 0; + List nums = List.filled(n, 0); + print('In recursion n = $n, nums length = ${nums.length}'); + return quadraticRecur(n - 1); + } ``` === "Rust" ```rust title="space_complexity.rs" - [class]{}-[func]{quadratic_recur} + /* Quadratic order (recursive implementation) */ + fn quadratic_recur(n: i32) -> i32 { + if n <= 0 { + return 0; + }; + // Array nums has length n, n-1, ..., 2, 1 + let nums = vec![0; n as usize]; + println!("In recursion n = {}, nums length = {}", n, nums.len()); + return quadratic_recur(n - 1); + } ``` === "C" ```c title="space_complexity.c" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + int quadraticRecur(int n) { + if (n <= 0) + return 0; + int *nums = malloc(sizeof(int) * n); + printf("In recursion n = %d, nums length = %d\r\n", n, n); + int res = quadraticRecur(n - 1); + free(nums); + return res; + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{quadraticRecur} + /* Quadratic order (recursive implementation) */ + tailrec fun quadraticRecur(n: Int): Int { + if (n <= 0) + return 0 + // Array nums has length n, n-1, ..., 2, 1 + val nums = Array(n) { 0 } + println("In recursion n = $n, nums length = ${nums.size}") + return quadraticRecur(n - 1) + } ``` === "Ruby" ```ruby title="space_complexity.rb" - [class]{}-[func]{quadratic_recur} + ### Quadratic space (recursive) ### + def quadratic_recur(n) + return 0 unless n > 0 + + # Array nums has length n, n-1, ..., 2, 1 + nums = Array.new(n, 0) + quadratic_recur(n - 1) + end ``` -=== "Zig" +![Quadratic order space complexity generated by recursive function](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } - ```zig title="space_complexity.zig" - [class]{}-[func]{quadraticRecur} - ``` +

Figure 2-18   Quadratic order space complexity generated by recursive function

-![Recursive function generating quadratic order space complexity](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" } +### 4.   Exponential Order $O(2^n)$ {data-toc-label="4.   Exponential Order"} -

Figure 2-18   Recursive function generating quadratic order space complexity

- -### 4.   Exponential order $O(2^n)$ {data-toc-label="4.   Exponential order"} - -Exponential order is common in binary trees. Observe Figure 2-19, a "full binary tree" with $n$ levels has $2^n - 1$ nodes, occupying $O(2^n)$ space: +Exponential order is common in binary trees. Observe the following figure: a "full binary tree" with $n$ levels has $2^n - 1$ nodes, occupying $O(2^n)$ space: === "Python" ```python title="space_complexity.py" def build_tree(n: int) -> TreeNode | None: - """Exponential complexity (building a full binary tree)""" + """Exponential order (build full binary tree)""" if n == 0: return None root = TreeNode(0) @@ -1392,7 +2033,7 @@ Exponential order is common in binary trees. Observe Figure 2-19, a "full binary === "C++" ```cpp title="space_complexity.cpp" - /* Exponential complexity (building a full binary tree) */ + /* Driver Code */ TreeNode *buildTree(int n) { if (n == 0) return nullptr; @@ -1406,7 +2047,7 @@ Exponential order is common in binary trees. Observe Figure 2-19, a "full binary === "Java" ```java title="space_complexity.java" - /* Exponential complexity (building a full binary tree) */ + /* Driver Code */ TreeNode buildTree(int n) { if (n == 0) return null; @@ -1420,83 +2061,157 @@ Exponential order is common in binary trees. Observe Figure 2-19, a "full binary === "C#" ```csharp title="space_complexity.cs" - [class]{space_complexity}-[func]{BuildTree} + /* Driver Code */ + TreeNode? BuildTree(int n) { + if (n == 0) return null; + TreeNode root = new(0) { + left = BuildTree(n - 1), + right = BuildTree(n - 1) + }; + return root; + } ``` === "Go" ```go title="space_complexity.go" - [class]{}-[func]{buildTree} + /* Driver Code */ + 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" - [class]{}-[func]{buildTree} + /* Driver Code */ + 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" - [class]{}-[func]{buildTree} + /* Driver Code */ + 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" - [class]{}-[func]{buildTree} + /* Driver Code */ + 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" - [class]{}-[func]{buildTree} + /* Driver Code */ + 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" - [class]{}-[func]{build_tree} + /* Driver Code */ + 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" - [class]{}-[func]{buildTree} + /* Driver Code */ + TreeNode *buildTree(int n) { + if (n == 0) + return NULL; + TreeNode *root = newTreeNode(0); + root->left = buildTree(n - 1); + root->right = buildTree(n - 1); + return root; + } ``` === "Kotlin" ```kotlin title="space_complexity.kt" - [class]{}-[func]{buildTree} + /* Driver Code */ + fun buildTree(n: Int): TreeNode? { + if (n == 0) + return null + val root = TreeNode(0) + root.left = buildTree(n - 1) + root.right = buildTree(n - 1) + return root + } ``` === "Ruby" ```ruby title="space_complexity.rb" - [class]{}-[func]{build_tree} + ### Exponential space (build full binary tree) ### + def build_tree(n) + return if n == 0 + + TreeNode.new.tap do |root| + root.left = build_tree(n - 1) + root.right = build_tree(n - 1) + end + end ``` -=== "Zig" +![Exponential order space complexity generated by full binary tree](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } - ```zig title="space_complexity.zig" - [class]{}-[func]{buildTree} - ``` +

Figure 2-19   Exponential order space complexity generated by full binary tree

-![Full binary tree generating exponential order space complexity](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" } +### 5.   Logarithmic Order $O(\log n)$ {data-toc-label="5.   Logarithmic Order"} -

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

+Logarithmic order is common in divide-and-conquer algorithms. For example, merge sort: given an input array of length $n$, each recursion divides the array in half from the midpoint, forming a recursion tree of height $\log n$, using $O(\log n)$ stack frame space. -### 5.   Logarithmic order $O(\log n)$ {data-toc-label="5.   Logarithmic order"} +Another example is converting a number to a string. Given a positive integer $n$, it has $\lfloor \log_{10} n \rfloor + 1$ digits, i.e., the corresponding string length is $\lfloor \log_{10} n \rfloor + 1$, so the space complexity is $O(\log_{10} n + 1) = O(\log n)$. -Logarithmic order is common in divide-and-conquer algorithms. For example, in merge sort, an array of length $n$ is recursively divided in half each round, forming a recursion tree of height $\log n$, using $O(\log n)$ stack frame space. +## 2.4.4   Trading Time for Space -Another example is converting a number to a string. Given a positive integer $n$, its number of digits is $\log_{10} n + 1$, corresponding to the length of the string, thus the space complexity is $O(\log_{10} n + 1) = O(\log n)$. +Ideally, we hope that both the time complexity and space complexity of an algorithm can reach optimal. However, in practice, optimizing both time complexity and space complexity simultaneously is usually very difficult. -## 2.4.4   Balancing time and space +**Reducing time complexity usually comes at the cost of increasing space complexity, and vice versa**. The approach of sacrificing memory space to improve algorithm execution speed is called "trading space for time"; conversely, it is called "trading time for space". -Ideally, we aim for both time complexity and space complexity to be optimal. However, in practice, optimizing both simultaneously is often difficult. - -**Lowering time complexity usually comes at the cost of increased space complexity, and vice versa**. The approach of sacrificing memory space to improve algorithm speed is known as "space-time tradeoff"; the reverse is known as "time-space tradeoff". - -The choice depends on which aspect we value more. In most cases, time is more precious than space, so "space-time tradeoff" is often the more common strategy. Of course, controlling space complexity is also very important when dealing with large volumes of data. +The choice of which approach depends on which aspect we value more. In most cases, time is more precious than space, so "trading space for time" is usually the more common strategy. Of course, when the data volume is very large, controlling space complexity is also very important. diff --git a/en/docs/chapter_computational_complexity/summary.md b/en/docs/chapter_computational_complexity/summary.md index bb6ffffb2..5d4f0c24d 100644 --- a/en/docs/chapter_computational_complexity/summary.md +++ b/en/docs/chapter_computational_complexity/summary.md @@ -4,50 +4,56 @@ comments: true # 2.5   Summary -### 1.   Key review +### 1.   Key Review **Algorithm Efficiency Assessment** -- Time efficiency and space efficiency are the two main criteria for assessing the merits of an algorithm. -- We can assess algorithm efficiency through actual testing, but it's challenging to eliminate the influence of the test environment, and it consumes substantial computational resources. -- Complexity analysis can overcome the disadvantages of actual testing. Its results are applicable across all operating platforms and can reveal the efficiency of algorithms at different data scales. +- Time efficiency and space efficiency are the two primary evaluation metrics for measuring algorithm performance. +- We can evaluate algorithm efficiency through actual testing, but it is difficult to eliminate the influence of the testing environment, and it consumes substantial computational resources. +- Complexity analysis can eliminate the drawbacks of actual testing, with results applicable to all running platforms, and it can reveal algorithm efficiency under different data scales. **Time Complexity** -- Time complexity measures the trend of an algorithm's running time with the increase in data volume, effectively assessing algorithm efficiency. However, it can fail in certain cases, such as with small input data volumes or when time complexities are the same, making it challenging to precisely compare the efficiency of algorithms. -- Worst-case time complexity is denoted using big-$O$ notation, representing the asymptotic upper bound, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches infinity. -- Calculating time complexity involves two steps: first counting the number of operations, then determining the asymptotic upper bound. -- Common time complexities, arranged from low to high, include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$, among others. -- The time complexity of some algorithms is not fixed and depends on the distribution of input data. Time complexities are divided into worst, best, and average cases. The best case is rarely used because input data generally needs to meet strict conditions to achieve the best case. -- Average time complexity reflects the efficiency of an algorithm under random data inputs, closely resembling the algorithm's performance in actual applications. Calculating average time complexity requires accounting for the distribution of input data and the subsequent mathematical expectation. +- Time complexity is used to measure the trend of algorithm runtime as data volume increases. It can effectively evaluate algorithm efficiency, but may fail in certain situations, such as when the input data volume is small or when time complexities are identical, making it impossible to precisely compare algorithm efficiency. +- Worst-case time complexity is represented using Big $O$ notation, corresponding to the asymptotic upper bound of a function, reflecting the growth level of the number of operations $T(n)$ as $n$ approaches positive infinity. +- Deriving time complexity involves two steps: first, counting the number of operations, then determining the asymptotic upper bound. +- Common time complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n \log n)$, $O(n^2)$, $O(2^n)$, and $O(n!)$. +- The time complexity of some algorithms is not fixed, but rather depends on the distribution of input data. Time complexity is divided into worst-case, best-case, and average-case time complexity. Best-case time complexity is rarely used because input data generally needs to satisfy strict conditions to achieve the best case. +- Average time complexity reflects the algorithm's runtime efficiency under random data input, and is closest to the algorithm's performance in practical applications. Calculating average time complexity requires statistical analysis of input data distribution and the combined mathematical expectation. **Space Complexity** -- Space complexity, similar to time complexity, measures the trend of memory space occupied by an algorithm with the increase in data volume. -- The relevant memory space used during the algorithm's execution can be divided into input space, temporary space, and output space. Generally, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions. -- We usually focus only on the worst-case space complexity, which means calculating the space complexity of the algorithm under the worst input data and at the worst moment of operation. -- Common space complexities, arranged from low to high, include $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$, and $O(2^n)$, among others. +- Space complexity serves a similar purpose to time complexity, used to measure the trend of algorithm memory usage as data volume increases. +- The memory space related to algorithm execution can be divided into input space, temporary space, and output space. Typically, input space is not included in space complexity calculations. Temporary space can be divided into temporary data, stack frame space, and instruction space, where stack frame space usually affects space complexity only in recursive functions. +- We typically only focus on worst-case space complexity, which is the space complexity of an algorithm under worst-case input data and worst-case runtime. +- Common space complexities arranged from low to high include $O(1)$, $O(\log n)$, $O(n)$, $O(n^2)$, and $O(2^n)$. ### 2.   Q & A **Q**: Is the space complexity of tail recursion $O(1)$? -Theoretically, the space complexity of a tail-recursive function can be optimized to $O(1)$. However, most programming languages (such as Java, Python, C++, Go, C#) do not support automatic optimization of tail recursion, so it's generally considered to have a space complexity of $O(n)$. +Theoretically, the space complexity of tail recursive functions can be optimized to $O(1)$. However, most programming languages (such as Java, Python, C++, Go, C#, etc.) do not support automatic tail recursion optimization, so the space complexity is generally considered to be $O(n)$. -**Q**: What is the difference between the terms "function" and "method"? +**Q**: What is the difference between the terms function and method? -A function can be executed independently, with all parameters passed explicitly. A method is associated with an object and is implicitly passed to the object calling it, able to operate on the data contained within an instance of a class. +A function can be executed independently, with all parameters passed explicitly. A method is associated with an object, is implicitly passed to the object that invokes it, and can operate on data contained in class instances. -Here are some examples from common programming languages: +The following examples use several common programming languages for illustration. -- C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with these structures are equivalent to methods in other programming languages. +- C is a procedural programming language without object-oriented concepts, so it only has functions. However, we can simulate object-oriented programming by creating structures (struct), and functions associated with structures are equivalent to methods in other programming languages. - Java and C# are object-oriented programming languages where code blocks (methods) are typically part of a class. Static methods behave like functions because they are bound to the class and cannot access specific instance variables. - C++ and Python support both procedural programming (functions) and object-oriented programming (methods). -**Q**: Does the "Common Types of Space Complexity" figure reflect the absolute size of occupied space? +**Q**: Does the diagram for "common space complexity types" reflect the absolute size of occupied space? -No, the figure shows space complexities, which reflect growth trends, not the absolute size of the occupied space. +No, the diagram shows space complexity, which reflects growth trends rather than the absolute size of occupied space. -If you take $n = 8$, you might find that the values of each curve don't correspond to their functions. This is because each curve includes a constant term, intended to compress the value range into a visually comfortable range. +Assuming $n = 8$, you might find that the values of each curve do not correspond to the functions. This is because each curve contains a constant term used to compress the value range into a visually comfortable range. -In practice, since we usually don't know the "constant term" complexity of each method, it's generally not possible to choose the best solution for $n = 8$ based solely on complexity. However, for $n = 8^5$, it's much easier to choose, as the growth trend becomes dominant. +In practice, because we generally do not know what the "constant term" complexity of each method is, we usually cannot select the optimal solution for $n = 8$ based on complexity alone. But for $n = 8^5$, the choice is straightforward, as the growth trend already dominates. + +**Q**: Are there situations where algorithms are designed to sacrifice time (or space) based on actual use cases? + +In practical applications, most situations choose to sacrifice space for time. For example, with database indexes, we typically choose to build B+ trees or hash indexes, occupying substantial memory space in exchange for efficient queries of $O(\log n)$ or even $O(1)$. + +In scenarios where space resources are precious, time may be sacrificed for space. For example, in embedded development, device memory is precious, and engineers may forgo using hash tables and choose to use array sequential search to save memory usage, at the cost of slower searches. diff --git a/en/docs/chapter_computational_complexity/time_complexity.md b/en/docs/chapter_computational_complexity/time_complexity.md index 8693a52e8..cd1a91c79 100644 --- a/en/docs/chapter_computational_complexity/time_complexity.md +++ b/en/docs/chapter_computational_complexity/time_complexity.md @@ -2,25 +2,25 @@ comments: true --- -# 2.3   Time complexity +# 2.3   Time Complexity -The runtime can intuitively assess the efficiency of an algorithm. How can we accurately estimate the runtime of a piece of an algorithm? +Runtime can intuitively and accurately reflect the efficiency of an algorithm. If we want to accurately estimate the runtime of a piece of code, how should we proceed? -1. **Determining the Running Platform**: This includes hardware configuration, programming language, system environment, etc., all of which can affect the efficiency of code execution. -2. **Evaluating the Run Time for Various Computational Operations**: For instance, an addition operation `+` might take 1 ns, a multiplication operation `*` might take 10 ns, a print operation `print()` might take 5 ns, etc. -3. **Counting All the Computational Operations in the Code**: Summing the execution times of all these operations gives the total run time. +1. **Determine the running platform**, including hardware configuration, programming language, system environment, etc., as these factors all affect code execution efficiency. +2. **Evaluate the runtime required for various computational operations**, for example, an addition operation `+` requires 1 ns, a multiplication operation `*` requires 10 ns, a print operation `print()` requires 5 ns, etc. +3. **Count all computational operations in the code**, and sum the execution times of all operations to obtain the runtime. -For example, consider the following code with an input size of $n$: +For example, in the following code, the input data size is $n$: === "Python" ```python title="" - # Under an operating platform + # On a certain running platform def algorithm(n: int): a = 2 # 1 ns a = a + 1 # 1 ns a = a * 2 # 10 ns - # Cycle n times + # Loop n times for _ in range(n): # 1 ns print(0) # 5 ns ``` @@ -28,13 +28,13 @@ For example, consider the following code with an input size of $n$: === "C++" ```cpp title="" - // Under a particular operating platform + // On a certain running platform void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for (int i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for (int i = 0; i < n; i++) { // 1 ns cout << 0 << endl; // 5 ns } } @@ -43,13 +43,13 @@ For example, consider the following code with an input size of $n$: === "Java" ```java title="" - // Under a particular operating platform + // On a certain running platform void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for (int i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for (int i = 0; i < n; i++) { // 1 ns System.out.println(0); // 5 ns } } @@ -58,13 +58,13 @@ For example, consider the following code with an input size of $n$: === "C#" ```csharp title="" - // Under a particular operating platform + // On a certain running platform void Algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for (int i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for (int i = 0; i < n; i++) { // 1 ns Console.WriteLine(0); // 5 ns } } @@ -73,7 +73,7 @@ For example, consider the following code with an input size of $n$: === "Go" ```go title="" - // Under a particular operating platform + // On a certain running platform func algorithm(n int) { a := 2 // 1 ns a = a + 1 // 1 ns @@ -88,7 +88,7 @@ For example, consider the following code with an input size of $n$: === "Swift" ```swift title="" - // Under a particular operating platform + // On a certain running platform func algorithm(n: Int) { var a = 2 // 1 ns a = a + 1 // 1 ns @@ -103,13 +103,13 @@ For example, consider the following code with an input size of $n$: === "JS" ```javascript title="" - // Under a particular operating platform + // On a certain running platform function algorithm(n) { var a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for(let i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } @@ -118,13 +118,13 @@ For example, consider the following code with an input size of $n$: === "TS" ```typescript title="" - // Under a particular operating platform + // On a certain running platform function algorithm(n: number): void { var a: number = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for(let i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for(let i = 0; i < n; i++) { // 1 ns console.log(0); // 5 ns } } @@ -133,13 +133,13 @@ For example, consider the following code with an input size of $n$: === "Dart" ```dart title="" - // Under a particular operating platform + // On a certain running platform void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for (int i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for (int i = 0; i < n; i++) { // 1 ns print(0); // 5 ns } } @@ -148,13 +148,13 @@ For example, consider the following code with an input size of $n$: === "Rust" ```rust title="" - // Under a particular operating platform + // On a certain running platform fn algorithm(n: i32) { let mut a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for _ in 0..n { // 1 ns for each round i++ + for _ in 0..n { // 1 ns println!("{}", 0); // 5 ns } } @@ -163,13 +163,13 @@ For example, consider the following code with an input size of $n$: === "C" ```c title="" - // Under a particular operating platform + // On a certain running platform void algorithm(int n) { int a = 2; // 1 ns a = a + 1; // 1 ns a = a * 2; // 10 ns // Loop n times - for (int i = 0; i < n; i++) { // 1 ns , every round i++ is executed + for (int i = 0; i < n; i++) { // 1 ns printf("%d", 0); // 5 ns } } @@ -178,37 +178,46 @@ For example, consider the following code with an input size of $n$: === "Kotlin" ```kotlin title="" - - ``` - -=== "Zig" - - ```zig title="" - // Under a particular operating platform - fn algorithm(n: usize) void { - var a: i32 = 2; // 1 ns - a += 1; // 1 ns - a *= 2; // 10 ns + // On a certain running platform + fun algorithm(n: Int) { + var a = 2 // 1 ns + a = a + 1 // 1 ns + a = a * 2 // 10 ns // Loop n times - for (0..n) |_| { // 1 ns - std.debug.print("{}\n", .{0}); // 5 ns + for (i in 0.. Figure 2-7   Time growth trend of algorithms a, b, and c

+ # Time complexity of algorithm C: constant order + def algorithm_C(n) + (0...1_000_000).each { puts 0 } + end + ``` -Compared to directly counting the run time of an algorithm, what are the characteristics of time complexity analysis? +Figure 2-7 shows the time complexity of the above three algorithm functions. -- **Time complexity effectively assesses algorithm efficiency**. For instance, algorithm `B` has linearly growing run time, which is slower than algorithm `A` when $n > 1$ and slower than `C` when $n > 1,000,000$. In fact, as long as the input data size $n$ is sufficiently large, a "constant order" complexity algorithm will always be better than a "linear order" one, demonstrating the essence of time growth trend. -- **Time complexity analysis is more straightforward**. Obviously, the running platform and the types of computational operations are irrelevant to the trend of run time growth. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time," simplifying the "computational operation run time count" to a "computational operation count." This significantly reduces the complexity of estimation. -- **Time complexity has its limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual run times can be quite different. Similarly, even though algorithm `B` has a higher time complexity than `C`, it is clearly superior when the input data size $n$ is small. In these cases, it's difficult to judge the efficiency of algorithms based solely on time complexity. Nonetheless, despite these issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency. +- Algorithm `A` has only $1$ print operation, and the algorithm's runtime does not grow as $n$ increases. We call the time complexity of this algorithm "constant order". +- In algorithm `B`, the print operation needs to loop $n$ times, and the algorithm's runtime grows linearly as $n$ increases. The time complexity of this algorithm is called "linear order". +- In algorithm `C`, the print operation needs to loop $1000000$ times. Although the runtime is very long, it is independent of the input data size $n$. Therefore, the time complexity of `C` is the same as `A`, still "constant order". -## 2.3.2   Asymptotic upper bound +![Time growth trends of algorithms A, B, and C](time_complexity.assets/time_complexity_simple_example.png){ class="animation-figure" } -Consider a function with an input size of $n$: +

Figure 2-7   Time growth trends of algorithms A, B, and C

+ +Compared to directly counting the algorithm's runtime, what are the characteristics of time complexity analysis? + +- **Time complexity can effectively evaluate algorithm efficiency**. For example, the runtime of algorithm `B` grows linearly; when $n > 1$ it is slower than algorithm `A`, and when $n > 1000000$ it is slower than algorithm `C`. In fact, as long as the input data size $n$ is sufficiently large, an algorithm with "constant order" complexity will always be superior to one with "linear order" complexity, which is precisely the meaning of time growth trend. +- **The derivation method for time complexity is simpler**. Obviously, the running platform and the types of computational operations are both unrelated to the growth trend of the algorithm's runtime. Therefore, in time complexity analysis, we can simply treat the execution time of all computational operations as the same "unit time", thus simplifying "counting computational operation runtime" to "counting the number of computational operations", which greatly reduces the difficulty of estimation. +- **Time complexity also has certain limitations**. For example, although algorithms `A` and `C` have the same time complexity, their actual runtimes differ significantly. Similarly, although algorithm `B` has a higher time complexity than `C`, when the input data size $n$ is small, algorithm `B` is clearly superior to algorithm `C`. In such cases, it is often difficult to judge the efficiency of algorithms based solely on time complexity. Of course, despite the above issues, complexity analysis remains the most effective and commonly used method for evaluating algorithm efficiency. + +## 2.3.2   Asymptotic Upper Bound of Functions + +Given a function with input size $n$: === "Python" @@ -495,7 +515,7 @@ Consider a function with an input size of $n$: a = 1 # +1 a = a + 1 # +1 a = a * 2 # +1 - # Cycle n times + # Loop n times for i in range(n): # +1 print(0) # +1 ``` @@ -508,7 +528,7 @@ Consider a function with an input size of $n$: a = a + 1; // +1 a = a * 2; // +1 // Loop n times - for (int i = 0; i < n; i++) { // +1 (execute i ++ every round) + for (int i = 0; i < n; i++) { // +1 (i++ is executed each round) cout << 0 << endl; // +1 } } @@ -522,7 +542,7 @@ Consider a function with an input size of $n$: a = a + 1; // +1 a = a * 2; // +1 // Loop n times - for (int i = 0; i < n; i++) { // +1 (execute i ++ every round) + for (int i = 0; i < n; i++) { // +1 (i++ is executed each round) System.out.println(0); // +1 } } @@ -536,7 +556,7 @@ Consider a function with an input size of $n$: a = a + 1; // +1 a = a * 2; // +1 // Loop n times - for (int i = 0; i < n; i++) { // +1 (execute i ++ every round) + for (int i = 0; i < n; i++) { // +1 (i++ is executed each round) Console.WriteLine(0); // +1 } } @@ -578,7 +598,7 @@ Consider a function with an input size of $n$: a += 1; // +1 a *= 2; // +1 // Loop n times - for(let i = 0; i < n; i++){ // +1 (execute i ++ every round) + for(let i = 0; i < n; i++){ // +1 (i++ is executed each round) console.log(0); // +1 } } @@ -592,7 +612,7 @@ Consider a function with an input size of $n$: a += 1; // +1 a *= 2; // +1 // Loop n times - for(let i = 0; i < n; i++){ // +1 (execute i ++ every round) + for(let i = 0; i < n; i++){ // +1 (i++ is executed each round) console.log(0); // +1 } } @@ -606,7 +626,7 @@ Consider a function with an input size of $n$: a = a + 1; // +1 a = a * 2; // +1 // Loop n times - for (int i = 0; i < n; i++) { // +1 (execute i ++ every round) + for (int i = 0; i < n; i++) { // +1 (i++ is executed each round) print(0); // +1 } } @@ -621,7 +641,7 @@ Consider a function with an input size of $n$: a = a * 2; // +1 // Loop n times - for _ in 0..n { // +1 (execute i ++ every round) + for _ in 0..n { // +1 (i++ is executed each round) println!("{}", 0); // +1 } } @@ -635,78 +655,88 @@ Consider a function with an input size of $n$: a = a + 1; // +1 a = a * 2; // +1 // Loop n times - for (int i = 0; i < n; i++) { // +1 (execute i ++ every round) + for (int i = 0; i < n; i++) { // +1 (i++ is executed each round) printf("%d", 0); // +1 } - } + } ``` === "Kotlin" ```kotlin title="" - - ``` - -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +1 - a += 1; // +1 - a *= 2; // +1 + fun algorithm(n: Int) { + var a = 1 // +1 + a = a + 1 // +1 + a = a * 2 // +1 // Loop n times - for (0..n) |_| { // +1 (execute i ++ every round) - std.debug.print("{}\n", .{0}); // +1 + for (i in 0..big-O notation, represents the asymptotic upper bound of the function $T(n)$. +$T(n)$ is a linear function, indicating that its runtime growth trend is linear, and therefore its time complexity is linear order. -In essence, time complexity analysis is about finding the asymptotic upper bound of the "number of operations $T(n)$". It has a precise mathematical definition. +We denote the time complexity of linear order as $O(n)$. This mathematical symbol is called big-$O$ notation, representing the asymptotic upper bound of the function $T(n)$. -!!! note "Asymptotic Upper Bound" +Time complexity analysis essentially calculates the asymptotic upper bound of "the number of operations $T(n)$", which has a clear mathematical definition. - If there exist positive real numbers $c$ and $n_0$ such that for all $n > n_0$, $T(n) \leq c \cdot f(n)$, then $f(n)$ is considered an asymptotic upper bound of $T(n)$, denoted as $T(n) = O(f(n))$. +!!! note "Asymptotic upper bound of functions" -As shown in Figure 2-8, calculating the asymptotic upper bound involves finding a function $f(n)$ such that, as $n$ approaches infinity, $T(n)$ and $f(n)$ have the same growth order, differing only by a constant factor $c$. + If there exist positive real numbers $c$ and $n_0$ such that for all $n > n_0$, we have $T(n) \leq c \cdot f(n)$, then $f(n)$ can be considered as an asymptotic upper bound of $T(n)$, denoted as $T(n) = O(f(n))$. + +As shown in Figure 2-8, calculating the asymptotic upper bound is to find a function $f(n)$ such that when $n$ tends to infinity, $T(n)$ and $f(n)$ are at the same growth level, differing only by a constant coefficient $c$. ![Asymptotic upper bound of a function](time_complexity.assets/asymptotic_upper_bound.png){ class="animation-figure" }

Figure 2-8   Asymptotic upper bound of a function

-## 2.3.3   Calculation method +## 2.3.3   Derivation Method -While the concept of asymptotic upper bound might seem mathematically dense, you don't need to fully grasp it right away. Let's first understand the method of calculation, which can be practiced and comprehended over time. +The asymptotic upper bound has a bit of mathematical flavor. If you feel you haven't fully understood it, don't worry. We can first master the derivation method, and gradually grasp its mathematical meaning through continuous practice. -Once $f(n)$ is determined, we obtain the time complexity $O(f(n))$. But how do we determine the asymptotic upper bound $f(n)$? This process generally involves two steps: counting the number of operations and determining the asymptotic upper bound. +According to the definition, after determining $f(n)$, we can obtain the time complexity $O(f(n))$. So how do we determine the asymptotic upper bound $f(n)$? Overall, it is divided into two steps: first count the number of operations, then determine the asymptotic upper bound. -### 1.   Step 1: counting the number of operations +### 1.   Step 1: Count the Number of Operations -This step involves going through the code line by line. However, due to the presence of the constant $c$ in $c \cdot f(n)$, **all coefficients and constant terms in $T(n)$ can be ignored**. This principle allows for simplification techniques in counting operations. +For code, count from top to bottom line by line. However, since the constant coefficient $c$ in $c \cdot f(n)$ above can be of any size, **coefficients and constant terms in the number of operations $T(n)$ can all be ignored**. According to this principle, the following counting simplification techniques can be summarized. -1. **Ignore constant terms in $T(n)$**, as they do not affect the time complexity being independent of $n$. -2. **Omit all coefficients**. For example, looping $2n$, $5n + 1$ times, etc., can be simplified to $n$ times since the coefficient before $n$ does not impact the time complexity. -3. **Use multiplication for nested loops**. The total number of operations equals the product of the number of operations in each loop, applying the simplification techniques from points 1 and 2 for each loop level. +1. **Ignore constants in $T(n)$**. Because they are all independent of $n$, they do not affect time complexity. +2. **Omit all coefficients**. For example, looping $2n$ times, $5n + 1$ times, etc., can all be simplified as $n$ times, because the coefficient before $n$ does not affect time complexity. +3. **Use multiplication for nested loops**. The total number of operations equals the product of the number of operations in the outer and inner loops, with each layer of loop still able to apply techniques `1.` and `2.` separately. -Given a function, we can use these techniques to count operations: +Given a function, we can use the above techniques to count the number of operations: === "Python" ```python title="" def algorithm(n: int): - a = 1 # +0 (trick 1) - a = a + n # +0 (trick 1) - # +n (technique 2) + a = 1 # +0 (Technique 1) + a = a + n # +0 (Technique 1) + # +n (Technique 2) for i in range(5 * n + 1): print(0) - # +n*n (technique 3) + # +n*n (Technique 3) for i in range(2 * n): for j in range(n + 1): print(0) @@ -716,13 +746,13 @@ Given a function, we can use these techniques to count operations: ```cpp title="" void algorithm(int n) { - int a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + int a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (int i = 0; i < 5 * n + 1; i++) { cout << 0 << endl; } - // +n*n (technique 3) + // +n*n (Technique 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { cout << 0 << endl; @@ -735,13 +765,13 @@ Given a function, we can use these techniques to count operations: ```java title="" void algorithm(int n) { - int a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + int a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (int i = 0; i < 5 * n + 1; i++) { System.out.println(0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { System.out.println(0); @@ -754,13 +784,13 @@ Given a function, we can use these techniques to count operations: ```csharp title="" void Algorithm(int n) { - int a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + int a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (int i = 0; i < 5 * n + 1; i++) { Console.WriteLine(0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { Console.WriteLine(0); @@ -773,13 +803,13 @@ Given a function, we can use these techniques to count operations: ```go title="" func algorithm(n int) { - a := 1 // +0 (trick 1) - a = a + n // +0 (trick 1) - // +n (technique 2) + a := 1 // +0 (Technique 1) + a = a + n // +0 (Technique 1) + // +n (Technique 2) for i := 0; i < 5 * n + 1; i++ { fmt.Println(0) } - // +n*n (technique 3) + // +n*n (Technique 3) for i := 0; i < 2 * n; i++ { for j := 0; j < n + 1; j++ { fmt.Println(0) @@ -792,13 +822,13 @@ Given a function, we can use these techniques to count operations: ```swift title="" func algorithm(n: Int) { - var a = 1 // +0 (trick 1) - a = a + n // +0 (trick 1) - // +n (technique 2) + var a = 1 // +0 (Technique 1) + a = a + n // +0 (Technique 1) + // +n (Technique 2) for _ in 0 ..< (5 * n + 1) { print(0) } - // +n*n (technique 3) + // +n*n (Technique 3) for _ in 0 ..< (2 * n) { for _ in 0 ..< (n + 1) { print(0) @@ -811,13 +841,13 @@ Given a function, we can use these techniques to count operations: ```javascript title="" function algorithm(n) { - let a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + let a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); @@ -830,13 +860,13 @@ Given a function, we can use these techniques to count operations: ```typescript title="" function algorithm(n: number): void { - let a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + let a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (let i = 0; i < 5 * n + 1; i++) { console.log(0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (let i = 0; i < 2 * n; i++) { for (let j = 0; j < n + 1; j++) { console.log(0); @@ -849,13 +879,13 @@ Given a function, we can use these techniques to count operations: ```dart title="" void algorithm(int n) { - int a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + int a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (int i = 0; i < 5 * n + 1; i++) { print(0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { print(0); @@ -868,15 +898,15 @@ Given a function, we can use these techniques to count operations: ```rust title="" fn algorithm(n: i32) { - let mut a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) + let mut a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) - // +n (technique 2) + // +n (Technique 2) for i in 0..(5 * n + 1) { println!("{}", 0); } - // +n*n (technique 3) + // +n*n (Technique 3) for i in 0..(2 * n) { for j in 0..(n + 1) { println!("{}", 0); @@ -889,13 +919,13 @@ Given a function, we can use these techniques to count operations: ```c title="" void algorithm(int n) { - int a = 1; // +0 (trick 1) - a = a + n; // +0 (trick 1) - // +n (technique 2) + int a = 1; // +0 (Technique 1) + a = a + n; // +0 (Technique 1) + // +n (Technique 2) for (int i = 0; i < 5 * n + 1; i++) { printf("%d", 0); } - // +n*n (technique 3) + // +n*n (Technique 3) for (int i = 0; i < 2 * n; i++) { for (int j = 0; j < n + 1; j++) { printf("%d", 0); @@ -907,84 +937,93 @@ Given a function, we can use these techniques to count operations: === "Kotlin" ```kotlin title="" - - ``` - -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +0 (trick 1) - a = a + @as(i32, @intCast(n)); // +0 (trick 1) - - // +n (technique 2) - for(0..(5 * n + 1)) |_| { - std.debug.print("{}\n", .{0}); + fun algorithm(n: Int) { + var a = 1 // +0 (Technique 1) + a = a + n // +0 (Technique 1) + // +n (Technique 2) + for (i in 0..<5 * n + 1) { + println(0) } - - // +n*n (technique 3) - for(0..(2 * n)) |_| { - for(0..(n + 1)) |_| { - std.debug.print("{}\n", .{0}); + // +n*n (Technique 3) + for (i in 0..<2 * n) { + for (j in 0.. Table: Time complexity for different operation counts

+

Table 2-2   Time complexities corresponding to different numbers of operations

-| Operation Count $T(n)$ | Time Complexity $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)$ | +| Number of Operations $T(n)$ | Time Complexity $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   Common types of time complexity +## 2.3.4   Common Types -Let's consider the input data size as $n$. The common types of time complexities are shown in Figure 2-9, arranged from lowest to highest: +Let the input data size be $n$. Common time complexity types are shown in Figure 2-9 (arranged in order from low to high). $$ \begin{aligned} -& O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline -& \text{Constant} < \text{Log} < \text{Linear} < \text{Linear-Log} < \text{Quadratic} < \text{Exp} < \text{Factorial} +O(1) < O(\log n) < O(n) < O(n \log n) < O(n^2) < O(2^n) < O(n!) \newline +\text{Constant order} < \text{Logarithmic order} < \text{Linear order} < \text{Linearithmic order} < \text{Quadratic order} < \text{Exponential order} < \text{Factorial order} \end{aligned} $$ -![Common types of time complexity](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } +![Common time complexity types](time_complexity.assets/time_complexity_common_types.png){ class="animation-figure" } -

Figure 2-9   Common types of time complexity

+

Figure 2-9   Common time complexity types

-### 1.   Constant order $O(1)$ {data-toc-label="1.   Constant order"} +### 1.   Constant Order $O(1)$ {data-toc-label="1.   Constant Order"} -Constant order means the number of operations is independent of the input data size $n$. In the following function, although the number of operations `size` might be large, the time complexity remains $O(1)$ as it's unrelated to $n$: +The number of operations in constant order is independent of the input data size $n$, meaning it does not change as $n$ changes. + +In the following function, although the number of operations `size` may be large, since it is independent of the input data size $n$, the time complexity remains $O(1)$: === "Python" ```python title="time_complexity.py" def constant(n: int) -> int: - """Constant complexity""" + """Constant order""" count = 0 size = 100000 for _ in range(size): @@ -995,7 +1034,7 @@ Constant order means the number of operations is independent of the input data s === "C++" ```cpp title="time_complexity.cpp" - /* Constant complexity */ + /* Constant order */ int constant(int n) { int count = 0; int size = 100000; @@ -1008,7 +1047,7 @@ Constant order means the number of operations is independent of the input data s === "Java" ```java title="time_complexity.java" - /* Constant complexity */ + /* Constant order */ int constant(int n) { int count = 0; int size = 100000; @@ -1021,78 +1060,148 @@ Constant order means the number of operations is independent of the input data s === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + func constant(n int) int { + count := 0 + size := 100000 + for i := 0; i < size; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + 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" - [class]{}-[func]{constant} + /* Constant order */ + int constant(int n) { + int count = 0; + int size = 100000; + int i = 0; + for (int i = 0; i < size; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{constant} + /* Constant order */ + fun constant(n: Int): Int { + var count = 0 + val size = 100000 + for (i in 0.. int: - """Linear complexity""" + """Linear order""" count = 0 for _ in range(n): count += 1 @@ -1102,7 +1211,7 @@ Linear order indicates the number of operations grows linearly with the input da === "C++" ```cpp title="time_complexity.cpp" - /* Linear complexity */ + /* Linear order */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) @@ -1114,7 +1223,7 @@ Linear order indicates the number of operations grows linearly with the input da === "Java" ```java title="time_complexity.java" - /* Linear complexity */ + /* Linear order */ int linear(int n) { int count = 0; for (int i = 0; i < n; i++) @@ -1126,78 +1235,134 @@ Linear order indicates the number of operations grows linearly with the input da === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Linear} + /* Linear order */ + int Linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) + count++; + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{linear} + /* Linear order */ + func linear(n int) int { + count := 0 + for i := 0; i < n; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{linear} + /* Linear order */ + func linear(n: Int) -> Int { + var count = 0 + for _ in 0 ..< n { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{linear} + /* Linear order */ + function linear(n) { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{linear} + /* Linear order */ + function linear(n: number): number { + let count = 0; + for (let i = 0; i < n; i++) count++; + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{linear} + /* Linear order */ + int linear(int n) { + int count = 0; + for (var i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{linear} + /* Linear order */ + fn linear(n: i32) -> i32 { + let mut count = 0; + for _ in 0..n { + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{linear} + /* Linear order */ + int linear(int n) { + int count = 0; + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{linear} + /* Linear order */ + fun linear(n: Int): Int { + var count = 0 + for (i in 0.. int: - """Linear complexity (traversing an array)""" + """Linear order (traversing array)""" count = 0 - # Loop count is proportional to the length of the array + # Number of iterations is proportional to the array length for num in nums: count += 1 return count @@ -1206,10 +1371,10 @@ Operations like array traversal and linked list traversal have a time complexity === "C++" ```cpp title="time_complexity.cpp" - /* Linear complexity (traversing an array) */ + /* Linear order (traversing array) */ int arrayTraversal(vector &nums) { int count = 0; - // Loop count is proportional to the length of the array + // Number of iterations is proportional to the array length for (int num : nums) { count++; } @@ -1220,10 +1385,10 @@ Operations like array traversal and linked list traversal have a time complexity === "Java" ```java title="time_complexity.java" - /* Linear complexity (traversing an array) */ + /* Linear order (traversing array) */ int arrayTraversal(int[] nums) { int count = 0; - // Loop count is proportional to the length of the array + // Number of iterations is proportional to the array length for (int num : nums) { count++; } @@ -1234,82 +1399,158 @@ Operations like array traversal and linked list traversal have a time complexity === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{ArrayTraversal} + /* Linear order (traversing array) */ + int ArrayTraversal(int[] nums) { + int count = 0; + // Number of iterations is proportional to the array length + foreach (int num in nums) { + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + func arrayTraversal(nums []int) int { + count := 0 + // Number of iterations is proportional to the array length + for range nums { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + func arrayTraversal(nums: [Int]) -> Int { + var count = 0 + // Number of iterations is proportional to the array length + for _ in nums { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + function arrayTraversal(nums) { + let count = 0; + // Number of iterations is proportional to the array length + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + function arrayTraversal(nums: number[]): number { + let count = 0; + // Number of iterations is proportional to the array length + for (let i = 0; i < nums.length; i++) { + count++; + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + int arrayTraversal(List nums) { + int count = 0; + // Number of iterations is proportional to the array length + for (var _num in nums) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{array_traversal} + /* Linear order (traversing array) */ + fn array_traversal(nums: &[i32]) -> i32 { + let mut count = 0; + // Number of iterations is proportional to the array length + for _ in nums { + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + int arrayTraversal(int *nums, int n) { + int count = 0; + // Number of iterations is proportional to the array length + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{arrayTraversal} + /* Linear order (traversing array) */ + fun arrayTraversal(nums: IntArray): Int { + var count = 0 + // Number of iterations is proportional to the array length + for (num in nums) { + count++ + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{array_traversal} + ### Linear time (array traversal) ### + def array_traversal(nums) + count = 0 + + # Number of iterations is proportional to the array length + for num in nums + count += 1 + end + + count + end ``` -=== "Zig" +It is worth noting that **the input data size $n$ should be determined according to the type of input data**. For example, in the first example, the variable $n$ is the input data size; in the second example, the array length $n$ is the data size. - ```zig title="time_complexity.zig" - [class]{}-[func]{arrayTraversal} - ``` +### 3.   Quadratic Order $O(n^2)$ {data-toc-label="3.   Quadratic Order"} -It's important to note that **the input data size $n$ should be determined based on the type of input data**. For example, in the first example, $n$ represents the input data size, while in the second example, the length of the array $n$ is the data size. - -### 3.   Quadratic order $O(n^2)$ {data-toc-label="3.   Quadratic order"} - -Quadratic order means the number of operations grows quadratically with the input data size $n$. Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of $O(n)$, resulting in an overall complexity of $O(n^2)$: +The number of operations in quadratic order grows quadratically relative to the input data size $n$. Quadratic order typically appears in nested loops, where both the outer and inner loops have a time complexity of $O(n)$, resulting in an overall time complexity of $O(n^2)$: === "Python" ```python title="time_complexity.py" def quadratic(n: int) -> int: - """Quadratic complexity""" + """Quadratic order""" count = 0 - # Loop count is squared in relation to the data size n + # Number of iterations is quadratically related to the data size n for i in range(n): for j in range(n): count += 1 @@ -1319,10 +1560,10 @@ Quadratic order means the number of operations grows quadratically with the inpu === "C++" ```cpp title="time_complexity.cpp" - /* Quadratic complexity */ + /* Exponential order */ int quadratic(int n) { int count = 0; - // Loop count is squared in relation to the data size n + // Number of iterations is quadratically related to the data size n for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; @@ -1335,10 +1576,10 @@ Quadratic order means the number of operations grows quadratically with the inpu === "Java" ```java title="time_complexity.java" - /* Quadratic complexity */ + /* Exponential order */ int quadratic(int n) { int count = 0; - // Loop count is squared in relation to the data size n + // Number of iterations is quadratically related to the data size n for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; @@ -1351,112 +1592,208 @@ Quadratic order means the number of operations grows quadratically with the inpu === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Quadratic} + /* Exponential order */ + int Quadratic(int n) { + int count = 0; + // Number of iterations is quadratically related to the data size n + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{quadratic} + /* Exponential order */ + func quadratic(n int) int { + count := 0 + // Number of iterations is quadratically related to the data size n + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + count++ + } + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{quadratic} + /* Exponential order */ + func quadratic(n: Int) -> Int { + var count = 0 + // Number of iterations is quadratically related to the data size n + for _ in 0 ..< n { + for _ in 0 ..< n { + count += 1 + } + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{quadratic} + /* Exponential order */ + function quadratic(n) { + let count = 0; + // Number of iterations is quadratically related to the data size n + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{quadratic} + /* Exponential order */ + function quadratic(n: number): number { + let count = 0; + // Number of iterations is quadratically related to the data size n + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{quadratic} + /* Exponential order */ + int quadratic(int n) { + int count = 0; + // Number of iterations is quadratically related to the data size n + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{quadratic} + /* Exponential order */ + fn quadratic(n: i32) -> i32 { + let mut count = 0; + // Number of iterations is quadratically related to the data size n + for _ in 0..n { + for _ in 0..n { + count += 1; + } + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{quadratic} + /* Exponential order */ + int quadratic(int n) { + int count = 0; + // Number of iterations is quadratically related to the data size n + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + count++; + } + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{quadratic} + /* Exponential order */ + fun quadratic(n: Int): Int { + var count = 0 + // Number of iterations is quadratically related to the data size n + for (i in 0.. Figure 2-10   Constant, linear, and quadratic order time complexities

+

Figure 2-10   Time complexities of constant, linear, and quadratic orders

-For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner loop runs $n-1$, $n-2$, ..., $2$, $1$ times, averaging $n / 2$ times, resulting in a time complexity of $O((n - 1) n / 2) = O(n^2)$: +Taking bubble sort as an example, the outer loop executes $n - 1$ times, and the inner loop executes $n-1$, $n-2$, $\dots$, $2$, $1$ times, averaging $n / 2$ times, resulting in a time complexity of $O((n - 1) n / 2) = O(n^2)$: === "Python" ```python title="time_complexity.py" def bubble_sort(nums: list[int]) -> int: - """Quadratic complexity (bubble sort)""" + """Quadratic order (bubble sort)""" count = 0 # Counter # Outer loop: unsorted range is [0, i] for i in range(len(nums) - 1, 0, -1): - # Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for j in range(i): if nums[j] > nums[j + 1]: # Swap nums[j] and nums[j + 1] tmp: int = nums[j] nums[j] = nums[j + 1] nums[j + 1] = tmp - count += 3 # Element swap includes 3 individual operations + count += 3 # Element swap includes 3 unit operations return count ``` === "C++" ```cpp title="time_complexity.cpp" - /* Quadratic complexity (bubble sort) */ + /* Quadratic order (bubble sort) */ int bubbleSort(vector &nums) { int count = 0; // Counter // Outer loop: unsorted range is [0, i] for (int i = nums.size() - 1; i > 0; i--) { - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - count += 3; // Element swap includes 3 individual operations + count += 3; // Element swap includes 3 unit operations } } } @@ -1467,19 +1804,19 @@ For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner l === "Java" ```java title="time_complexity.java" - /* Quadratic complexity (bubble sort) */ + /* Quadratic order (bubble sort) */ int bubbleSort(int[] nums) { int count = 0; // Counter // Outer loop: unsorted range is [0, i] for (int i = nums.length - 1; i > 0; i--) { - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - count += 3; // Element swap includes 3 individual operations + count += 3; // Element swap includes 3 unit operations } } } @@ -1490,83 +1827,248 @@ For instance, in bubble sort, the outer loop runs $n - 1$ times, and the inner l === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{BubbleSort} + /* Quadratic order (bubble sort) */ + int BubbleSort(int[] nums) { + int count = 0; // Counter + // Outer loop: unsorted range is [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + count += 3; // Element swap includes 3 unit operations + } + } + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + func bubbleSort(nums []int) int { + count := 0 // Counter + // Outer loop: unsorted range is [0, i] + for i := len(nums) - 1; i > 0; i-- { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // Swap nums[j] and nums[j + 1] + tmp := nums[j] + nums[j] = nums[j+1] + nums[j+1] = tmp + count += 3 // Element swap includes 3 unit operations + } + } + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + func bubbleSort(nums: inout [Int]) -> Int { + var count = 0 // Counter + // Outer loop: unsorted range is [0, i] + for i in nums.indices.dropFirst().reversed() { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 // Element swap includes 3 unit operations + } + } + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + function bubbleSort(nums) { + let count = 0; // Counter + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // Element swap includes 3 unit operations + } + } + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + function bubbleSort(nums: number[]): number { + let count = 0; // Counter + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // Element swap includes 3 unit operations + } + } + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + int bubbleSort(List nums) { + int count = 0; // Counter + // Outer loop: unsorted range is [0, i] + for (var i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (var j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // Element swap includes 3 unit operations + } + } + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{bubble_sort} + /* Quadratic order (bubble sort) */ + fn bubble_sort(nums: &mut [i32]) -> i32 { + let mut count = 0; // Counter + + // Outer loop: unsorted range is [0, i] + for i in (1..nums.len()).rev() { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0..i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // Element swap includes 3 unit operations + } + } + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + int bubbleSort(int *nums, int n) { + int count = 0; // Counter + // Outer loop: unsorted range is [0, i] + for (int i = n - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + count += 3; // Element swap includes 3 unit operations + } + } + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{bubbleSort} + /* Quadratic order (bubble sort) */ + fun bubbleSort(nums: IntArray): Int { + var count = 0 // Counter + // Outer loop: unsorted range is [0, i] + for (i in nums.size - 1 downTo 1) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (j in 0.. nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + count += 3 // Element swap includes 3 unit operations + } + } + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{bubble_sort} + ### Quadratic time (bubble sort) ### + def bubble_sort(nums) + count = 0 # Counter + + # Outer loop: unsorted range is [0, i] + for i in (nums.length - 1).downto(0) + # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0...i + if nums[j] > nums[j + 1] + # Swap nums[j] and nums[j + 1] + tmp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = tmp + count += 3 # Element swap includes 3 unit operations + end + end + end + + count + end ``` -=== "Zig" +### 4.   Exponential Order $O(2^n)$ {data-toc-label="4.   Exponential Order"} - ```zig title="time_complexity.zig" - [class]{}-[func]{bubbleSort} - ``` +Biological "cell division" is a typical example of exponential order growth: the initial state is $1$ cell, after one round of division it becomes $2$, after two rounds it becomes $4$, and so on; after $n$ rounds of division there are $2^n$ cells. -### 4.   Exponential order $O(2^n)$ {data-toc-label="4.   Exponential order"} - -Biological "cell division" is a classic example of exponential order growth: starting with one cell, it becomes two after one division, four after two divisions, and so on, resulting in $2^n$ cells after $n$ divisions. - -Figure 2-11 and code simulate the cell division process, with a time complexity of $O(2^n)$: +Figure 2-11 and the following code simulate the cell division process, with a time complexity of $O(2^n)$. Note that the input $n$ represents the number of division rounds, and the return value `count` represents the total number of divisions. === "Python" ```python title="time_complexity.py" def exponential(n: int) -> int: - """Exponential complexity (loop implementation)""" + """Exponential order (loop implementation)""" count = 0 base = 1 - # Cells split into two every round, forming the sequence 1, 2, 4, 8, ..., 2^(n-1) + # Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1) for _ in range(n): for _ in range(base): count += 1 @@ -1578,10 +2080,10 @@ Figure 2-11 and code simulate the cell division process, with a time complexity === "C++" ```cpp title="time_complexity.cpp" - /* Exponential complexity (loop implementation) */ + /* Exponential order (loop implementation) */ int exponential(int n) { int count = 0, base = 1; - // Cells split into two every round, forming the sequence 1, 2, 4, 8, ..., 2^(n-1) + // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1) for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; @@ -1596,10 +2098,10 @@ Figure 2-11 and code simulate the cell division process, with a time complexity === "Java" ```java title="time_complexity.java" - /* Exponential complexity (loop implementation) */ + /* Exponential order (loop implementation) */ int exponential(int n) { int count = 0, base = 1; - // Cells split into two every round, forming the sequence 1, 2, 4, 8, ..., 2^(n-1) + // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1) for (int i = 0; i < n; i++) { for (int j = 0; j < base; j++) { count++; @@ -1614,80 +2116,200 @@ Figure 2-11 and code simulate the cell division process, with a time complexity === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Exponential} + /* Exponential order (loop implementation) */ + int Exponential(int n) { + int count = 0, bas = 1; + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + func exponential(n int) int { + count, base := 0, 1 + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + func exponential(n: Int) -> Int { + var count = 0 + var base = 1 + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + function exponential(n) { + let count = 0, + base = 1; + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + function exponential(n: number): number { + let count = 0, + base = 1; + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + int exponential(int n) { + int count = 0, base = 1; + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + fn exponential(n: i32) -> i32 { + let mut count = 0; + let mut base = 1; + // Cells divide into two every round, forming sequence 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" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + int exponential(int n) { + int count = 0; + int bas = 1; + // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1) + for (int i = 0; i < n; i++) { + for (int j = 0; j < bas; j++) { + count++; + } + bas *= 2; + } + // count = 1 + 2 + 4 + 8 + .. + 2^(n-1) = 2^n - 1 + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{exponential} + /* Exponential order (loop implementation) */ + fun exponential(n: Int): Int { + var count = 0 + var base = 1 + // Cells divide into two every round, forming sequence 1, 2, 4, 8, ..., 2^(n-1) + for (i in 0.. Figure 2-11   Time complexity of exponential order

-![Exponential order time complexity](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" } - -

Figure 2-11   Exponential order time complexity

- -In practice, exponential order often appears in recursive functions. For example, in the code below, it recursively splits into two halves, stopping after $n$ divisions: +In actual algorithms, exponential order often appears in recursive functions. For example, in the following code, it recursively splits in two, stopping after $n$ splits: === "Python" ```python title="time_complexity.py" def exp_recur(n: int) -> int: - """Exponential complexity (recursive implementation)""" + """Exponential order (recursive implementation)""" if n == 1: return 1 return exp_recur(n - 1) + exp_recur(n - 1) + 1 @@ -1696,7 +2318,7 @@ In practice, exponential order often appears in recursive functions. For example === "C++" ```cpp title="time_complexity.cpp" - /* Exponential complexity (recursive implementation) */ + /* Exponential order (recursive implementation) */ int expRecur(int n) { if (n == 1) return 1; @@ -1707,7 +2329,7 @@ In practice, exponential order often appears in recursive functions. For example === "Java" ```java title="time_complexity.java" - /* Exponential complexity (recursive implementation) */ + /* Exponential order (recursive implementation) */ int expRecur(int n) { if (n == 1) return 1; @@ -1718,82 +2340,125 @@ In practice, exponential order often appears in recursive functions. For example === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{ExpRecur} + /* Exponential order (recursive implementation) */ + int ExpRecur(int n) { + if (n == 1) return 1; + return ExpRecur(n - 1) + ExpRecur(n - 1) + 1; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + func expRecur(n int) int { + if n == 1 { + return 1 + } + return expRecur(n-1) + expRecur(n-1) + 1 + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + 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" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + function expRecur(n) { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + function expRecur(n: number): number { + if (n === 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + int expRecur(int n) { + if (n == 1) return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{exp_recur} + /* Exponential order (recursive implementation) */ + 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" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + int expRecur(int n) { + if (n == 1) + return 1; + return expRecur(n - 1) + expRecur(n - 1) + 1; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{expRecur} + /* Exponential order (recursive implementation) */ + fun expRecur(n: Int): Int { + if (n == 1) { + return 1 + } + return expRecur(n - 1) + expRecur(n - 1) + 1 + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{exp_recur} + ### Exponential time (recursive) ### + def exp_recur(n) + return 1 if n == 1 + exp_recur(n - 1) + exp_recur(n - 1) + 1 + end ``` -=== "Zig" +Exponential order growth is very rapid and is common in exhaustive methods (brute force search, backtracking, etc.). For problems with large data scales, exponential order is unacceptable and typically requires dynamic programming or greedy algorithms to solve. - ```zig title="time_complexity.zig" - [class]{}-[func]{expRecur} - ``` +### 5.   Logarithmic Order $O(\log n)$ {data-toc-label="5.   Logarithmic Order"} -Exponential order growth is extremely rapid and is commonly seen in exhaustive search methods (brute force, backtracking, etc.). For large-scale problems, exponential order is unacceptable, often requiring dynamic programming or greedy algorithms as solutions. +In contrast to exponential order, logarithmic order reflects the situation of "reducing to half each round". Let the input data size be $n$. Since it is reduced to half each round, the number of loops is $\log_2 n$, which is the inverse function of $2^n$. -### 5.   Logarithmic order $O(\log n)$ {data-toc-label="5.   Logarithmic order"} - -In contrast to exponential order, logarithmic order reflects situations where "the size is halved each round." Given an input data size $n$, since the size is halved each round, the number of iterations is $\log_2 n$, the inverse function of $2^n$. - -Figure 2-12 and code simulate the "halving each round" process, with a time complexity of $O(\log_2 n)$, commonly abbreviated as $O(\log n)$: +Figure 2-12 and the following code simulate the process of "reducing to half each round", with a time complexity of $O(\log_2 n)$, abbreviated as $O(\log n)$: === "Python" ```python title="time_complexity.py" def logarithmic(n: int) -> int: - """Logarithmic complexity (loop implementation)""" + """Logarithmic order (loop implementation)""" count = 0 while n > 1: n = n / 2 @@ -1804,7 +2469,7 @@ Figure 2-12 and code simulate the "halving each round" process, with a time comp === "C++" ```cpp title="time_complexity.cpp" - /* Logarithmic complexity (loop implementation) */ + /* Logarithmic order (loop implementation) */ int logarithmic(int n) { int count = 0; while (n > 1) { @@ -1818,7 +2483,7 @@ Figure 2-12 and code simulate the "halving each round" process, with a time comp === "Java" ```java title="time_complexity.java" - /* Logarithmic complexity (loop implementation) */ + /* Logarithmic order (loop implementation) */ int logarithmic(int n) { int count = 0; while (n > 1) { @@ -1832,80 +2497,158 @@ Figure 2-12 and code simulate the "halving each round" process, with a time comp === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{Logarithmic} + /* Logarithmic order (loop implementation) */ + int Logarithmic(int n) { + int count = 0; + while (n > 1) { + n /= 2; + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + func logarithmic(n int) int { + count := 0 + for n > 1 { + n = n / 2 + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + func logarithmic(n: Int) -> Int { + var count = 0 + var n = n + while n > 1 { + n = n / 2 + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + function logarithmic(n) { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + function logarithmic(n: number): number { + let count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n ~/ 2; + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + fn logarithmic(mut n: i32) -> i32 { + let mut count = 0; + while n > 1 { + n = n / 2; + count += 1; + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + int logarithmic(int n) { + int count = 0; + while (n > 1) { + n = n / 2; + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{logarithmic} + /* Logarithmic order (loop implementation) */ + fun logarithmic(n: Int): Int { + var n1 = n + var count = 0 + while (n1 > 1) { + n1 /= 2 + count++ + } + return count + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{logarithmic} + ### Logarithmic time (iterative) ### + def logarithmic(n) + count = 0 + + while n > 1 + n /= 2 + count += 1 + end + + count + end ``` -=== "Zig" +![Time complexity of logarithmic order](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } - ```zig title="time_complexity.zig" - [class]{}-[func]{logarithmic} - ``` +

Figure 2-12   Time complexity of logarithmic order

-![Logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" } - -

Figure 2-12   Logarithmic order time complexity

- -Like exponential order, logarithmic order also frequently appears in recursive functions. The code below forms a recursive tree of height $\log_2 n$: +Like exponential order, logarithmic order also commonly appears in recursive functions. The following code forms a recursion tree of height $\log_2 n$: === "Python" ```python title="time_complexity.py" def log_recur(n: int) -> int: - """Logarithmic complexity (recursive implementation)""" + """Logarithmic order (recursive implementation)""" if n <= 1: return 0 return log_recur(n / 2) + 1 @@ -1914,7 +2657,7 @@ Like exponential order, logarithmic order also frequently appears in recursive f === "C++" ```cpp title="time_complexity.cpp" - /* Logarithmic complexity (recursive implementation) */ + /* Logarithmic order (recursive implementation) */ int logRecur(int n) { if (n <= 1) return 0; @@ -1925,7 +2668,7 @@ Like exponential order, logarithmic order also frequently appears in recursive f === "Java" ```java title="time_complexity.java" - /* Logarithmic complexity (recursive implementation) */ + /* Logarithmic order (recursive implementation) */ int logRecur(int n) { if (n <= 1) return 0; @@ -1936,93 +2679,137 @@ Like exponential order, logarithmic order also frequently appears in recursive f === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{LogRecur} + /* Logarithmic order (recursive implementation) */ + int LogRecur(int n) { + if (n <= 1) return 0; + return LogRecur(n / 2) + 1; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + func logRecur(n int) int { + if n <= 1 { + return 0 + } + return logRecur(n/2) + 1 + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + func logRecur(n: Int) -> Int { + if n <= 1 { + return 0 + } + return logRecur(n: n / 2) + 1 + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + function logRecur(n) { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + function logRecur(n: number): number { + if (n <= 1) return 0; + return logRecur(n / 2) + 1; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + int logRecur(int n) { + if (n <= 1) return 0; + return logRecur(n ~/ 2) + 1; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{log_recur} + /* Logarithmic order (recursive implementation) */ + fn log_recur(n: i32) -> i32 { + if n <= 1 { + return 0; + } + log_recur(n / 2) + 1 + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + int logRecur(int n) { + if (n <= 1) + return 0; + return logRecur(n / 2) + 1; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{logRecur} + /* Logarithmic order (recursive implementation) */ + fun logRecur(n: Int): Int { + if (n <= 1) + return 0 + return logRecur(n / 2) + 1 + } ``` === "Ruby" ```ruby title="time_complexity.rb" - [class]{}-[func]{log_recur} + ### Logarithmic time (recursive) ### + def log_recur(n) + return 0 unless n > 1 + log_recur(n / 2) + 1 + end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{logRecur} - ``` - -Logarithmic order is typical in algorithms based on the divide-and-conquer strategy, embodying the "split into many" and "simplify complex problems" approach. It's slow-growing and is the most ideal time complexity after constant order. +Logarithmic order commonly appears in algorithms based on the divide-and-conquer strategy, embodying the algorithmic thinking of "dividing into many" and "simplifying complexity". It grows slowly and is the ideal time complexity second only to constant order. !!! tip "What is the base of $O(\log n)$?" - Technically, "splitting into $m$" corresponds to a time complexity of $O(\log_m n)$. Using the logarithm base change formula, we can equate different logarithmic complexities: + To be precise, "dividing into $m$" corresponds to a time complexity of $O(\log_m n)$. And through the logarithmic base change formula, we can obtain time complexities with different bases that are equal: $$ O(\log_m n) = O(\log_k n / \log_k m) = O(\log_k n) $$ - This means the base $m$ can be changed without affecting the complexity. Therefore, we often omit the base $m$ and simply denote logarithmic order as $O(\log n)$. + That is to say, the base $m$ can be converted without affecting the complexity. Therefore, we usually omit the base $m$ and denote logarithmic order simply as $O(\log n)$. -### 6.   Linear-logarithmic order $O(n \log n)$ {data-toc-label="6.   Linear-logarithmic order"} +### 6.   Linearithmic Order $O(n \log n)$ {data-toc-label="6.   Linearithmic Order"} -Linear-logarithmic order often appears in nested loops, with the complexities of the two loops being $O(\log n)$ and $O(n)$ respectively. The related code is as follows: +Linearithmic order commonly appears in nested loops, where the time complexities of the two layers of loops are $O(\log n)$ and $O(n)$ respectively. The relevant code is as follows: === "Python" ```python title="time_complexity.py" def linear_log_recur(n: int) -> int: - """Linear logarithmic complexity""" + """Linearithmic order""" if n <= 1: return 1 - count: int = linear_log_recur(n // 2) + linear_log_recur(n // 2) + # Divide into two, the scale of subproblems is reduced by half + count = linear_log_recur(n // 2) + linear_log_recur(n // 2) + # Current subproblem contains n operations for _ in range(n): count += 1 return count @@ -2031,7 +2818,7 @@ Linear-logarithmic order often appears in nested loops, with the complexities of === "C++" ```cpp title="time_complexity.cpp" - /* Linear logarithmic complexity */ + /* Linearithmic order */ int linearLogRecur(int n) { if (n <= 1) return 1; @@ -2046,7 +2833,7 @@ Linear-logarithmic order often appears in nested loops, with the complexities of === "Java" ```java title="time_complexity.java" - /* Linear logarithmic complexity */ + /* Linearithmic order */ int linearLogRecur(int n) { if (n <= 1) return 1; @@ -2061,96 +2848,178 @@ Linear-logarithmic order often appears in nested loops, with the complexities of === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{LinearLogRecur} + /* Linearithmic order */ + int LinearLogRecur(int n) { + if (n <= 1) return 1; + int count = LinearLogRecur(n / 2) + LinearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + func linearLogRecur(n int) int { + if n <= 1 { + return 1 + } + count := linearLogRecur(n/2) + linearLogRecur(n/2) + for i := 0; i < n; i++ { + count++ + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + func linearLogRecur(n: Int) -> Int { + if n <= 1 { + return 1 + } + var count = linearLogRecur(n: n / 2) + linearLogRecur(n: n / 2) + for _ in stride(from: 0, to: n, by: 1) { + count += 1 + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + 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" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + 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" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + int linearLogRecur(int n) { + if (n <= 1) return 1; + int count = linearLogRecur(n ~/ 2) + linearLogRecur(n ~/ 2); + for (var i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{linear_log_recur} + /* Linearithmic order */ + fn linear_log_recur(n: i32) -> i32 { + if n <= 1 { + return 1; + } + let mut count = linear_log_recur(n / 2) + linear_log_recur(n / 2); + for _ in 0..n { + count += 1; + } + return count; + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + int linearLogRecur(int n) { + if (n <= 1) + return 1; + int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); + for (int i = 0; i < n; i++) { + count++; + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{linearLogRecur} + /* Linearithmic order */ + fun linearLogRecur(n: Int): Int { + if (n <= 1) + return 1 + var count = linearLogRecur(n / 2) + linearLogRecur(n / 2) + for (i in 0.. 1 + + count = linear_log_recur(n / 2) + linear_log_recur(n / 2) + (0...n).each { count += 1 } + + count + end ``` -=== "Zig" +Figure 2-13 shows how linearithmic order is generated. Each level of the binary tree has a total of $n$ operations, and the tree has $\log_2 n + 1$ levels, resulting in a time complexity of $O(n \log n)$. - ```zig title="time_complexity.zig" - [class]{}-[func]{linearLogRecur} - ``` +![Time complexity of linearithmic order](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } -Figure 2-13 demonstrates how linear-logarithmic order is generated. Each level of a binary tree has $n$ operations, and the tree has $\log_2 n + 1$ levels, resulting in a time complexity of $O(n \log n)$. +

Figure 2-13   Time complexity of linearithmic order

-![Linear-logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } +Mainstream sorting algorithms typically have a time complexity of $O(n \log n)$, such as quicksort, merge sort, and heap sort. -

Figure 2-13   Linear-logarithmic order time complexity

+### 7.   Factorial Order $O(n!)$ {data-toc-label="7.   Factorial Order"} -Mainstream sorting algorithms typically have a time complexity of $O(n \log n)$, such as quicksort, mergesort, and heapsort. - -### 7.   Factorial order $O(n!)$ {data-toc-label="7.   Factorial order"} - -Factorial order corresponds to the mathematical problem of "full permutation." Given $n$ distinct elements, the total number of possible permutations is: +Factorial order corresponds to the mathematical "permutation" problem. Given $n$ distinct elements, find all possible permutation schemes; the number of schemes is: $$ n! = n \times (n - 1) \times (n - 2) \times \dots \times 2 \times 1 $$ -Factorials are typically implemented using recursion. As shown in the code and Figure 2-14, the first level splits into $n$ branches, the second level into $n - 1$ branches, and so on, stopping after the $n$th level: +Factorials are typically implemented using recursion. As shown in Figure 2-14 and the following code, the first level splits into $n$ branches, the second level splits into $n - 1$ branches, and so on, until the $n$-th level when splitting stops: === "Python" ```python title="time_complexity.py" def factorial_recur(n: int) -> int: - """Factorial complexity (recursive implementation)""" + """Factorial order (recursive implementation)""" if n == 0: return 1 count = 0 - # From 1 split into n + # Split from 1 into n for _ in range(n): count += factorial_recur(n - 1) return count @@ -2159,12 +3028,12 @@ Factorials are typically implemented using recursion. As shown in the code and F === "C++" ```cpp title="time_complexity.cpp" - /* Factorial complexity (recursive implementation) */ + /* Factorial order (recursive implementation) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; - // From 1 split into n + // Split from 1 into n for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } @@ -2175,12 +3044,12 @@ Factorials are typically implemented using recursion. As shown in the code and F === "Java" ```java title="time_complexity.java" - /* Factorial complexity (recursive implementation) */ + /* Factorial order (recursive implementation) */ int factorialRecur(int n) { if (n == 0) return 1; int count = 0; - // From 1 split into n + // Split from 1 into n for (int i = 0; i < n; i++) { count += factorialRecur(n - 1); } @@ -2191,89 +3060,180 @@ Factorials are typically implemented using recursion. As shown in the code and F === "C#" ```csharp title="time_complexity.cs" - [class]{time_complexity}-[func]{FactorialRecur} + /* Factorial order (recursive implementation) */ + int FactorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // Split from 1 into n + for (int i = 0; i < n; i++) { + count += FactorialRecur(n - 1); + } + return count; + } ``` === "Go" ```go title="time_complexity.go" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + func factorialRecur(n int) int { + if n == 0 { + return 1 + } + count := 0 + // Split from 1 into n + for i := 0; i < n; i++ { + count += factorialRecur(n - 1) + } + return count + } ``` === "Swift" ```swift title="time_complexity.swift" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + func factorialRecur(n: Int) -> Int { + if n == 0 { + return 1 + } + var count = 0 + // Split from 1 into n + for _ in 0 ..< n { + count += factorialRecur(n: n - 1) + } + return count + } ``` === "JS" ```javascript title="time_complexity.js" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + function factorialRecur(n) { + if (n === 0) return 1; + let count = 0; + // Split from 1 into n + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "TS" ```typescript title="time_complexity.ts" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + function factorialRecur(n: number): number { + if (n === 0) return 1; + let count = 0; + // Split from 1 into n + for (let i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Dart" ```dart title="time_complexity.dart" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + int factorialRecur(int n) { + if (n == 0) return 1; + int count = 0; + // Split from 1 into n + for (var i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Rust" ```rust title="time_complexity.rs" - [class]{}-[func]{factorial_recur} + /* Factorial order (recursive implementation) */ + fn factorial_recur(n: i32) -> i32 { + if n == 0 { + return 1; + } + let mut count = 0; + // Split from 1 into n + for _ in 0..n { + count += factorial_recur(n - 1); + } + count + } ``` === "C" ```c title="time_complexity.c" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + int factorialRecur(int n) { + if (n == 0) + return 1; + int count = 0; + for (int i = 0; i < n; i++) { + count += factorialRecur(n - 1); + } + return count; + } ``` === "Kotlin" ```kotlin title="time_complexity.kt" - [class]{}-[func]{factorialRecur} + /* Factorial order (recursive implementation) */ + fun factorialRecur(n: Int): Int { + if (n == 0) + return 1 + var count = 0 + // Split from 1 into n + for (i in 0.. Figure 2-14   Time complexity of factorial order

-![Factorial order time complexity](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" } +Note that because when $n \geq 4$ we always have $n! > 2^n$, factorial order grows faster than exponential order, and is also unacceptable for large $n$. -

Figure 2-14   Factorial order time complexity

+## 2.3.5   Worst, Best, and Average Time Complexities -Note that factorial order grows even faster than exponential order; it's unacceptable for larger $n$ values. +**The time efficiency of an algorithm is often not fixed, but is related to the distribution of the input data**. Suppose we input an array `nums` of length $n$, where `nums` consists of numbers from $1$ to $n$, with each number appearing only once, but the element order is randomly shuffled. The task is to return the index of element $1$. We can draw the following conclusions. -## 2.3.5   Worst, best, and average time complexities +- When `nums = [?, ?, ..., 1]`, i.e., when the last element is $1$, it requires a complete traversal of the array, **reaching worst-case time complexity $O(n)$**. +- When `nums = [1, ?, ?, ...]`, i.e., when the first element is $1$, no matter how long the array is, there is no need to continue traversing, **reaching best-case time complexity $\Omega(1)$**. -**The time efficiency of an algorithm is often not fixed but depends on the distribution of the input data**. Assume we have an array `nums` of length $n$, consisting of numbers from $1$ to $n$, each appearing only once, but in a randomly shuffled order. The task is to return the index of the element $1$. We can draw the following conclusions: - -- When `nums = [?, ?, ..., 1]`, that is, when the last element is $1$, it requires a complete traversal of the array, **achieving the worst-case time complexity of $O(n)$**. -- When `nums = [1, ?, ?, ...]`, that is, when the first element is $1$, no matter the length of the array, no further traversal is needed, **achieving the best-case time complexity of $\Omega(1)$**. - -The "worst-case time complexity" corresponds to the asymptotic upper bound, denoted by the big $O$ notation. Correspondingly, the "best-case time complexity" corresponds to the asymptotic lower bound, denoted by $\Omega$: +The "worst-case time complexity" corresponds to the function's asymptotic upper bound, denoted using big-$O$ notation. Correspondingly, the "best-case time complexity" corresponds to the function's asymptotic lower bound, denoted using $\Omega$ notation: === "Python" ```python title="worst_best_time_complexity.py" def random_numbers(n: int) -> list[int]: - """Generate an array with elements: 1, 2, ..., n, order shuffled""" + """Generate an array with elements: 1, 2, ..., n, shuffled in order""" # Generate array nums =: 1, 2, 3, ..., n nums = [i for i in range(1, n + 1)] # Randomly shuffle array elements @@ -2283,8 +3243,8 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno def find_one(nums: list[int]) -> int: """Find the index of number 1 in array nums""" for i in range(len(nums)): - # When element 1 is at the start of the array, achieve best time complexity O(1) - # When element 1 is at the end of the array, achieve worst time complexity O(n) + # When element 1 is at the head of the array, best time complexity O(1) is achieved + # When element 1 is at the tail of the array, worst time complexity O(n) is achieved if nums[i] == 1: return i return -1 @@ -2293,14 +3253,14 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno === "C++" ```cpp title="worst_best_time_complexity.cpp" - /* Generate an array with elements {1, 2, ..., n} in a randomly shuffled order */ + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ vector randomNumbers(int n) { vector nums(n); // Generate array nums = { 1, 2, 3, ..., n } for (int i = 0; i < n; i++) { nums[i] = i + 1; } - // Generate a random seed using system time + // Use system time to generate random seed unsigned seed = chrono::system_clock::now().time_since_epoch().count(); // Randomly shuffle array elements shuffle(nums.begin(), nums.end(), default_random_engine(seed)); @@ -2310,8 +3270,8 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno /* Find the index of number 1 in array nums */ int findOne(vector &nums) { for (int i = 0; i < nums.size(); i++) { - // When element 1 is at the start of the array, achieve best time complexity O(1) - // When element 1 is at the end of the array, achieve worst time complexity O(n) + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved if (nums[i] == 1) return i; } @@ -2322,7 +3282,7 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno === "Java" ```java title="worst_best_time_complexity.java" - /* Generate an array with elements {1, 2, ..., n} in a randomly shuffled order */ + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ int[] randomNumbers(int n) { Integer[] nums = new Integer[n]; // Generate array nums = { 1, 2, 3, ..., n } @@ -2342,8 +3302,8 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno /* Find the index of number 1 in array nums */ int findOne(int[] nums) { for (int i = 0; i < nums.length; i++) { - // When element 1 is at the start of the array, achieve best time complexity O(1) - // When element 1 is at the end of the array, achieve worst time complexity O(n) + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved if (nums[i] == 1) return i; } @@ -2354,99 +3314,303 @@ The "worst-case time complexity" corresponds to the asymptotic upper bound, deno === "C#" ```csharp title="worst_best_time_complexity.cs" - [class]{worst_best_time_complexity}-[func]{RandomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + int[] RandomNumbers(int n) { + int[] nums = new int[n]; + // Generate array nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } - [class]{worst_best_time_complexity}-[func]{FindOne} + // Randomly shuffle array elements + for (int i = 0; i < nums.Length; i++) { + int index = new Random().Next(i, nums.Length); + (nums[i], nums[index]) = (nums[index], nums[i]); + } + return nums; + } + + /* Find the index of number 1 in array nums */ + int FindOne(int[] nums) { + for (int i = 0; i < nums.Length; i++) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] == 1) + return i; + } + return -1; + } ``` === "Go" ```go title="worst_best_time_complexity.go" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + func randomNumbers(n int) []int { + nums := make([]int, n) + // Generate array nums = { 1, 2, 3, ..., n } + for i := 0; i < n; i++ { + nums[i] = i + 1 + } + // Randomly shuffle array elements + rand.Shuffle(len(nums), func(i, j int) { + nums[i], nums[j] = nums[j], nums[i] + }) + return nums + } - [class]{}-[func]{findOne} + /* Find the index of number 1 in array nums */ + func findOne(nums []int) int { + for i := 0; i < len(nums); i++ { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if nums[i] == 1 { + return i + } + } + return -1 + } ``` === "Swift" ```swift title="worst_best_time_complexity.swift" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + func randomNumbers(n: Int) -> [Int] { + // Generate array nums = { 1, 2, 3, ..., n } + var nums = Array(1 ... n) + // Randomly shuffle array elements + nums.shuffle() + return nums + } - [class]{}-[func]{findOne} + /* Find the index of number 1 in array nums */ + func findOne(nums: [Int]) -> Int { + for i in nums.indices { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if nums[i] == 1 { + return i + } + } + return -1 + } ``` === "JS" ```javascript title="worst_best_time_complexity.js" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + function randomNumbers(n) { + const nums = Array(n); + // Generate array nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // Randomly shuffle array elements + 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; + } - [class]{}-[func]{findOne} + /* Find the index of number 1 in array nums */ + function findOne(nums) { + for (let i = 0; i < nums.length; i++) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] === 1) { + return i; + } + } + return -1; + } ``` === "TS" ```typescript title="worst_best_time_complexity.ts" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + function randomNumbers(n: number): number[] { + const nums = Array(n); + // Generate array nums = { 1, 2, 3, ..., n } + for (let i = 0; i < n; i++) { + nums[i] = i + 1; + } + // Randomly shuffle array elements + 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; + } - [class]{}-[func]{findOne} + /* Find the index of number 1 in array nums */ + function findOne(nums: number[]): number { + for (let i = 0; i < nums.length; i++) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] === 1) { + return i; + } + } + return -1; + } ``` === "Dart" ```dart title="worst_best_time_complexity.dart" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + List randomNumbers(int n) { + final nums = List.filled(n, 0); + // Generate array nums = { 1, 2, 3, ..., n } + for (var i = 0; i < n; i++) { + nums[i] = i + 1; + } + // Randomly shuffle array elements + nums.shuffle(); - [class]{}-[func]{findOne} + return nums; + } + + /* Find the index of number 1 in array nums */ + int findOne(List nums) { + for (var i = 0; i < nums.length; i++) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] == 1) return i; + } + + return -1; + } ``` === "Rust" ```rust title="worst_best_time_complexity.rs" - [class]{}-[func]{random_numbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + fn random_numbers(n: i32) -> Vec { + // Generate array nums = { 1, 2, 3, ..., n } + let mut nums = (1..=n).collect::>(); + // Randomly shuffle array elements + nums.shuffle(&mut thread_rng()); + nums + } - [class]{}-[func]{find_one} + /* Find the index of number 1 in array nums */ + fn find_one(nums: &[i32]) -> Option { + for i in 0..nums.len() { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if nums[i] == 1 { + return Some(i); + } + } + None + } ``` === "C" ```c title="worst_best_time_complexity.c" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + int *randomNumbers(int n) { + // Allocate heap memory (create 1D variable-length array: n elements of type int) + int *nums = (int *)malloc(n * sizeof(int)); + // Generate array nums = { 1, 2, 3, ..., n } + for (int i = 0; i < n; i++) { + nums[i] = i + 1; + } + // Randomly shuffle array elements + 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; + } - [class]{}-[func]{findOne} + /* Find the index of number 1 in array nums */ + int findOne(int *nums, int n) { + for (int i = 0; i < n; i++) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] == 1) + return i; + } + return -1; + } ``` === "Kotlin" ```kotlin title="worst_best_time_complexity.kt" - [class]{}-[func]{randomNumbers} + /* Generate an array with elements { 1, 2, ..., n }, order shuffled */ + fun randomNumbers(n: Int): Array { + val nums = IntArray(n) + // Generate array nums = { 1, 2, 3, ..., n } + for (i in 0..(n) + for (i in 0..): Int { + for (i in nums.indices) { + // When element 1 is at the head of the array, best time complexity O(1) is achieved + // When element 1 is at the tail of the array, worst time complexity O(n) is achieved + if (nums[i] == 1) + return i + } + return -1 + } ``` === "Ruby" ```ruby title="worst_best_time_complexity.rb" - [class]{}-[func]{random_numbers} + ### Generate array with elements: 1, 2, ..., n, shuffled ### + def random_numbers(n) + # Generate array nums =: 1, 2, 3, ..., n + nums = Array.new(n) { |i| i + 1 } + # Randomly shuffle array elements + nums.shuffle! + end - [class]{}-[func]{find_one} + ### Find index of number 1 in array nums ### + def find_one(nums) + for i in 0...nums.length + # When element 1 is at the head of the array, best time complexity O(1) is achieved + # When element 1 is at the tail of the array, worst time complexity O(n) is achieved + return i if nums[i] == 1 + end + + -1 + end ``` -=== "Zig" +It is worth noting that we rarely use best-case time complexity in practice, because it can usually only be achieved with a very small probability and may be somewhat misleading. **The worst-case time complexity is more practical because it gives a safety value for efficiency**, allowing us to use the algorithm with confidence. - ```zig title="worst_best_time_complexity.zig" - [class]{}-[func]{randomNumbers} +From the above example, we can see that both worst-case and best-case time complexities only occur under "special data distributions", which may have a very small probability of occurrence and may not truly reflect the algorithm's running efficiency. In contrast, **average time complexity can reflect the algorithm's running efficiency under random input data**, denoted using the $\Theta$ notation. - [class]{}-[func]{findOne} - ``` +For some algorithms, we can simply derive the average case under random data distribution. For example, in the above example, since the input array is shuffled, the probability of element $1$ appearing at any index is equal, so the algorithm's average number of loops is half the array length $n / 2$, giving an average time complexity of $\Theta(n / 2) = \Theta(n)$. -It's important to note that the best-case time complexity is rarely used in practice, as it is usually only achievable under very low probabilities and might be misleading. **The worst-case time complexity is more practical as it provides a safety value for efficiency**, allowing us to confidently use the algorithm. - -From the above example, it's clear that both the worst-case and best-case time complexities only occur under "special data distributions," which may have a small probability of occurrence and may not accurately reflect the algorithm's run efficiency. In contrast, **the average time complexity can reflect the algorithm's efficiency under random input data**, denoted by the $\Theta$ notation. - -For some algorithms, we can simply estimate the average case under a random data distribution. For example, in the aforementioned example, since the input array is shuffled, the probability of element $1$ appearing at any index is equal. Therefore, the average number of loops for the algorithm is half the length of the array $n / 2$, giving an average time complexity of $\Theta(n / 2) = \Theta(n)$. - -However, calculating the average time complexity for more complex algorithms can be quite difficult, as it's challenging to analyze the overall mathematical expectation under the data distribution. In such cases, we usually use the worst-case time complexity as the standard for judging the efficiency of the algorithm. +But for more complex algorithms, calculating average time complexity is often quite difficult, because it is hard to analyze the overall mathematical expectation under data distribution. In this case, we usually use worst-case time complexity as the criterion for judging algorithm efficiency. !!! question "Why is the $\Theta$ symbol rarely seen?" - Possibly because the $O$ notation is more commonly spoken, it is often used to represent the average time complexity. However, strictly speaking, this practice is not accurate. In this book and other materials, if you encounter statements like "average time complexity $O(n)$", please understand it directly as $\Theta(n)$. + This may be because the $O$ symbol is too catchy, so we often use it to represent average time complexity. But strictly speaking, this practice is not standard. In this book and other materials, if you encounter expressions like "average time complexity $O(n)$", please understand it directly as $\Theta(n)$. diff --git a/en/docs/chapter_data_structure/basic_data_types.md b/en/docs/chapter_data_structure/basic_data_types.md index ed51cf820..25fde05bb 100644 --- a/en/docs/chapter_data_structure/basic_data_types.md +++ b/en/docs/chapter_data_structure/basic_data_types.md @@ -2,73 +2,73 @@ comments: true --- -# 3.2   Basic data types +# 3.2   Basic Data Types -When discussing data in computers, various forms like text, images, videos, voice and 3D models comes to mind. Despite their different organizational forms, they are all composed of various basic data types. +When we talk about data in computers, we think of various forms such as text, images, videos, audio, 3D models, and more. Although these data are organized in different ways, they are all composed of various basic data types. -**Basic data types are those that the CPU can directly operate on** and are directly used in algorithms, mainly including the following. +**Basic data types are types that the CPU can directly operate on**, and they are directly used in algorithms, mainly including the following. -- Integer types: `byte`, `short`, `int`, `long`. -- Floating-point types: `float`, `double`, used to represent decimals. -- Character type: `char`, used to represent letters, punctuation, and even emojis in various languages. -- Boolean type: `bool`, used to represent "yes" or "no" decisions. +- Integer types `byte`, `short`, `int`, `long`. +- Floating-point types `float`, `double`, used to represent decimal numbers. +- Character type `char`, used to represent letters, punctuation marks, and even emojis in various languages. +- Boolean type `bool`, used to represent "yes" and "no" judgments. -**Basic data types are stored in computers in binary form**. One binary digit is 1 bit. In most modern operating systems, 1 byte consists of 8 bits. +**Basic data types are stored in binary form in computers**. One binary bit is $1$ bit. In most modern operating systems, $1$ byte consists of $8$ bits. -The range of values for basic data types depends on the size of the space they occupy. Below, we take Java as an example. +The range of values for basic data types depends on the size of the space they occupy. Below is an example using Java. -- The integer type `byte` occupies 1 byte = 8 bits and can represent $2^8$ numbers. -- The integer type `int` occupies 4 bytes = 32 bits and can represent $2^{32}$ numbers. +- Integer type `byte` occupies $1$ byte = $8$ bits, and can represent $2^{8}$ numbers. +- Integer type `int` occupies $4$ bytes = $32$ bits, and can represent $2^{32}$ numbers. -The following table lists the space occupied, value range, and default values of various basic data types in Java. While memorizing this table isn't necessary, having a general understanding of it and referencing it when required is recommended. +The following table lists the space occupied, value ranges, and default values of various basic data types in Java. You don't need to memorize this table; a general understanding is sufficient, and you can refer to it when needed. -

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

+

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

-| Type | Symbol | Space Occupied | Minimum Value | Maximum Value | Default Value | -| ------- | -------- | -------------- | ------------------------ | ----------------------- | -------------- | -| Integer | `byte` | 1 byte | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | 0 | -| | `short` | 2 bytes | $-2^{15}$ | $2^{15} - 1$ | 0 | -| | `int` | 4 bytes | $-2^{31}$ | $2^{31} - 1$ | 0 | -| | `long` | 8 bytes | $-2^{63}$ | $2^{63} - 1$ | 0 | -| Float | `float` | 4 bytes | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ | -| | `double` | 8 bytes | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | 0.0 | -| Char | `char` | 2 bytes | 0 | $2^{16} - 1$ | 0 | -| Boolean | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ | +| Type | Symbol | Space Occupied | Minimum Value | Maximum Value | Default Value | +| ---------- | -------- | -------------- | ------------------------ | ----------------------- | -------------- | +| Integer | `byte` | 1 byte | $-2^7$ ($-128$) | $2^7 - 1$ ($127$) | $0$ | +| | `short` | 2 bytes | $-2^{15}$ | $2^{15} - 1$ | $0$ | +| | `int` | 4 bytes | $-2^{31}$ | $2^{31} - 1$ | $0$ | +| | `long` | 8 bytes | $-2^{63}$ | $2^{63} - 1$ | $0$ | +| Float | `float` | 4 bytes | $1.175 \times 10^{-38}$ | $3.403 \times 10^{38}$ | $0.0\text{f}$ | +| | `double` | 8 bytes | $2.225 \times 10^{-308}$ | $1.798 \times 10^{308}$ | $0.0$ | +| Character | `char` | 2 bytes | $0$ | $2^{16} - 1$ | $0$ | +| Boolean | `bool` | 1 byte | $\text{false}$ | $\text{true}$ | $\text{false}$ |
-Please note that the above table is specific to Java's basic data types. Every programming language has its own data type definitions, which might differ in space occupied, value ranges, and default values. +Please note that the above table is specific to Java's basic data types. Each programming language has its own data type definitions, and their space occupied, value ranges, and default values may vary. -- In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point `float` is double precision 64-bit; there is no `char` type, as a single character is actually a string `str` of length 1. -- C and C++ do not specify the size of basic data types, it varies with implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), used for Unix 64-bit operating systems including Linux and macOS. -- The size of `char` in C and C++ is 1 byte, while in most programming languages, it depends on the specific character encoding method, as detailed in the "Character Encoding" chapter. -- Even though representing a boolean only requires 1 bit (0 or 1), it is usually stored in memory as 1 byte. This is because modern computer CPUs typically use 1 byte as the smallest addressable memory unit. +- In Python, the integer type `int` can be of any size, limited only by available memory; the floating-point type `float` is double-precision 64-bit; there is no `char` type, a single character is actually a string `str` of length 1. +- C and C++ do not explicitly specify the size of basic data types, which varies by implementation and platform. The above table follows the LP64 [data model](https://en.cppreference.com/w/cpp/language/types#Properties), which is used in Unix 64-bit operating systems including Linux and macOS. +- The size of character `char` is 1 byte in C and C++, and in most programming languages it depends on the specific character encoding method, as detailed in the "Character Encoding" section. +- Even though representing a boolean value requires only 1 bit ($0$ or $1$), it is usually stored as 1 byte in memory. This is because modern computer CPUs typically use 1 byte as the minimum addressable memory unit. -So, what is the connection between basic data types and data structures? We know that data structures are ways to organize and store data in computers. The focus here is on "structure" rather than "data". +So, what is the relationship between basic data types and data structures? We know that data structures are ways of organizing and storing data in computers. The subject of this statement is "structure", not "data". -If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and the ordering of the numbers, but whether the stored content is an integer `int`, a decimal `float`, or a character `char`, is irrelevant to the "data structure". +If we want to represent "a row of numbers", we naturally think of using an array. This is because the linear structure of an array can represent the adjacency and order relationships of numbers, but the content stored—whether integer `int`, floating-point `float`, or character `char`—is unrelated to the "data structure". -In other words, **basic data types provide the "content type" of data, while data structures provide the "way of organizing" data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc. +In other words, **basic data types provide the "content type" of data, while data structures provide the "organization method" of data**. For example, in the following code, we use the same data structure (array) to store and represent different basic data types, including `int`, `float`, `char`, `bool`, etc. === "Python" ```python title="" - # Using various basic data types to initialize arrays + # Initialize arrays using various basic data types numbers: list[int] = [0] * 5 decimals: list[float] = [0.0] * 5 - # Python's characters are actually strings of length 1 + # In Python, characters are actually strings of length 1 characters: list[str] = ['0'] * 5 bools: list[bool] = [False] * 5 - # Python's lists can freely store various basic data types and object references + # Python lists can freely store various basic data types and object references data = [0, 0.0, 'a', False, ListNode(0)] ``` === "C++" ```cpp title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types int numbers[5]; float decimals[5]; char characters[5]; @@ -78,7 +78,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "Java" ```java title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -88,7 +88,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "C#" ```csharp title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types int[] numbers = new int[5]; float[] decimals = new float[5]; char[] characters = new char[5]; @@ -98,7 +98,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "Go" ```go title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types var numbers = [5]int{} var decimals = [5]float64{} var characters = [5]byte{} @@ -108,7 +108,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "Swift" ```swift title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types let numbers = Array(repeating: 0, count: 5) let decimals = Array(repeating: 0.0, count: 5) let characters: [Character] = Array(repeating: "a", count: 5) @@ -118,14 +118,14 @@ In other words, **basic data types provide the "content type" of data, while dat === "JS" ```javascript title="" - // JavaScript's arrays can freely store various basic data types and objects + // JavaScript arrays can freely store various basic data types and objects const array = [0, 0.0, 'a', false]; ``` === "TS" ```typescript title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types const numbers: number[] = []; const characters: string[] = []; const bools: boolean[] = []; @@ -134,7 +134,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "Dart" ```dart title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types List numbers = List.filled(5, 0); List decimals = List.filled(5, 0.0); List characters = List.filled(5, 'a'); @@ -144,9 +144,9 @@ In other words, **basic data types provide the "content type" of data, while dat === "Rust" ```rust title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types let numbers: Vec = vec![0; 5]; - let decimals: Vec = vec![0.0, 5]; + let decimals: Vec = vec![0.0; 5]; let characters: Vec = vec!['0'; 5]; let bools: Vec = vec![false; 5]; ``` @@ -154,7 +154,7 @@ In other words, **basic data types provide the "content type" of data, while dat === "C" ```c title="" - // Using various basic data types to initialize arrays + // Initialize arrays using various basic data types int numbers[10]; float decimals[10]; char characters[10]; @@ -164,15 +164,20 @@ In other words, **basic data types provide the "content type" of data, while dat === "Kotlin" ```kotlin title="" - + // Initialize arrays using various basic data types + val numbers = IntArray(5) + val decinals = FloatArray(5) + val characters = CharArray(5) + val bools = BooleanArray(5) ``` -=== "Zig" +=== "Ruby" - ```zig title="" - // Using various basic data types to initialize arrays - var numbers: [5]i32 = undefined; - var decimals: [5]f32 = undefined; - var characters: [5]u8 = undefined; - var bools: [5]bool = undefined; + ```ruby title="" + # Ruby lists can freely store various basic data types and object references + data = [0, 0.0, 'a', false, ListNode(0)] ``` + +??? pythontutor "Visualized Execution" + + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E4%BD%BF%E7%94%A8%E5%A4%9A%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E6%9D%A5%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0A%20%20%20%20numbers%20%3D%20%5B0%5D%20*%205%0A%20%20%20%20decimals%20%3D%20%5B0.0%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%AD%97%E7%AC%A6%E5%AE%9E%E9%99%85%E4%B8%8A%E6%98%AF%E9%95%BF%E5%BA%A6%E4%B8%BA%201%20%E7%9A%84%E5%AD%97%E7%AC%A6%E4%B8%B2%0A%20%20%20%20characters%20%3D%20%5B'0'%5D%20*%205%0A%20%20%20%20bools%20%3D%20%5BFalse%5D%20*%205%0A%20%20%20%20%23%20Python%20%E7%9A%84%E5%88%97%E8%A1%A8%E5%8F%AF%E4%BB%A5%E8%87%AA%E7%94%B1%E5%AD%98%E5%82%A8%E5%90%84%E7%A7%8D%E5%9F%BA%E6%9C%AC%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E5%92%8C%E5%AF%B9%E8%B1%A1%E5%BC%95%E7%94%A8%0A%20%20%20%20data%20%3D%20%5B0,%200.0,%20'a',%20False,%20ListNode%280%29%5D&cumulative=false&curInstr=12&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false diff --git a/en/docs/chapter_data_structure/character_encoding.md b/en/docs/chapter_data_structure/character_encoding.md index 6542a113d..cc1aa0046 100644 --- a/en/docs/chapter_data_structure/character_encoding.md +++ b/en/docs/chapter_data_structure/character_encoding.md @@ -2,96 +2,96 @@ comments: true --- -# 3.4   Character encoding * +# 3.4   Character Encoding * -In the computer system, all data is stored in binary form, and `char` is no exception. To represent characters, we need to develop a "character set" that defines a one-to-one mapping between each character and binary numbers. With the character set, computers can convert binary numbers to characters by looking up the table. +In computers, all data is stored in binary form, and character `char` is no exception. To represent characters, we need to establish a "character set" that defines a one-to-one correspondence between each character and binary numbers. With a character set, computers can convert binary numbers to characters by looking up the table. -## 3.4.1   ASCII character set +## 3.4.1   Ascii Character Set -The ASCII code is one of the earliest character sets, officially known as the American Standard Code for Information Interchange. It uses 7 binary digits (the lower 7 bits of a byte) to represent a character, allowing for a maximum of 128 different characters. As shown in Figure 3-6, ASCII includes uppercase and lowercase English letters, numbers 0 ~ 9, various punctuation marks, and certain control characters (such as newline and tab). +ASCII code is the earliest character set, with the full name American Standard Code for Information Interchange. It uses 7 binary bits (the lower 7 bits of one byte) to represent a character, and can represent a maximum of 128 different characters. As shown in Figure 3-6, ASCII code includes uppercase and lowercase English letters, numbers 0 ~ 9, some punctuation marks, and some control characters (such as newline and tab). ![ASCII code](character_encoding.assets/ascii_table.png){ class="animation-figure" }

Figure 3-6   ASCII code

-However, **ASCII can only represent English characters**. With the globalization of computers, a character set called EASCII was developed to represent more languages. It expands from the 7-bit structure of ASCII to 8 bits, enabling the representation of 256 characters. +However, **ASCII code can only represent English**. With the globalization of computers, a character set called EASCII that can represent more languages emerged. It expands from the 7-bit basis of ASCII to 8 bits, and can represent 256 different characters. -Globally, various region-specific EASCII character sets have been introduced. The first 128 characters of these sets are consistent with the ASCII, while the remaining 128 characters are defined differently to accommodate the requirements of different languages. +Worldwide, a batch of EASCII character sets suitable for different regions have appeared successively. The first 128 characters of these character sets are unified as ASCII code, and the last 128 characters are defined differently to adapt to the needs of different languages. -## 3.4.2   GBK character set +## 3.4.2   Gbk Character Set -Later, it was found that **EASCII still could not meet the character requirements of many languages**. For instance, there are nearly a hundred thousand Chinese characters, with several thousand used regularly. In 1980, the Standardization Administration of China released the GB2312 character set, which included 6763 Chinese characters, essentially fulfilling the computer processing needs for the Chinese language. +Later, people found that **EASCII code still cannot meet the character quantity requirements of many languages**. For example, there are nearly one hundred thousand Chinese characters, and several thousand are used daily. In 1980, the China National Standardization Administration released the GB2312 character set, which included 6,763 Chinese characters, basically meeting the needs for computer processing of Chinese characters. -However, GB2312 could not handle some rare and traditional characters. The GBK character set expands GB2312 and includes 21886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented with one byte, while Chinese characters use two bytes. +However, GB2312 cannot handle some rare characters and traditional Chinese characters. The GBK character set is an extension based on GB2312, which includes a total of 21,886 Chinese characters. In the GBK encoding scheme, ASCII characters are represented using one byte, and Chinese characters are represented using two bytes. -## 3.4.3   Unicode character set +## 3.4.3   Unicode Character Set -With the rapid evolution of computer technology and a plethora of character sets and encoding standards, numerous problems arose. On the one hand, these character sets generally only defined characters for specific languages and could not function properly in multilingual environments. On the other hand, the existence of multiple character set standards for the same language caused garbled text when information was exchanged between computers using different encoding standards. +With the vigorous development of computer technology, character sets and encoding standards flourished, which brought many problems. On the one hand, these character sets generally only define characters for specific languages and cannot work normally in multilingual environments. On the other hand, multiple character set standards exist for the same language, and if two computers use different encoding standards, garbled characters will appear during information transmission. -Researchers of that era thought: **What if a comprehensive character set encompassing all global languages and symbols was developed? Wouldn't this resolve the issues associated with cross-linguistic environments and garbled text?** Inspired by this idea, the extensive character set, Unicode, was born. +Researchers of that era thought: **If a sufficiently complete character set is released that includes all languages and symbols in the world, wouldn't it be possible to solve cross-language environment and garbled character problems**? Driven by this idea, a large and comprehensive character set, Unicode, was born. -Unicode is referred to as "统一码" (Unified Code) in Chinese, theoretically capable of accommodating over a million characters. It aims to incorporate characters from all over the world into a single set, providing a universal character set for processing and displaying various languages and reducing the issues of garbled text due to different encoding standards. +Unicode is called "统一码" (Unified Code) in Chinese and can theoretically accommodate over one million characters. It is committed to including characters from around the world into a unified character set, providing a universal character set to handle and display various language texts, reducing garbled character problems caused by different encoding standards. -Since its release in 1991, Unicode has continually expanded to include new languages and characters. As of September 2022, Unicode contains 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, while some rare characters may occupy 3 or even 4 bytes. +Since its release in 1991, Unicode has continuously expanded to include new languages and characters. As of September 2022, Unicode has included 149,186 characters, including characters, symbols, and even emojis from various languages. In the vast Unicode character set, commonly used characters occupy 2 bytes, and some rare characters occupy 3 bytes or even 4 bytes. -Unicode is a universal character set that assigns a number (called a "code point") to each character, **but it does not specify how these character code points should be stored in a computer system**. One might ask: How does a system interpret Unicode code points of varying lengths within a text? For example, given a 2-byte code, how does the system determine if it represents a single 2-byte character or two 1-byte characters? +Unicode is a universal character set that essentially assigns a number (called a "code point") to each character, **but it does not specify how to store these character code points in computers**. We can't help but ask: when Unicode code points of multiple lengths appear simultaneously in a text, how does the system parse the characters? For example, given an encoding with a length of 2 bytes, how does the system determine whether it is one 2-byte character or two 1-byte characters? -**A straightforward solution to this problem is to store all characters as equal-length encodings**. As shown in Figure 3-7, each character in "Hello" occupies 1 byte, while each character in "算法" (algorithm) occupies 2 bytes. We could encode all characters in "Hello 算法" as 2 bytes by padding the higher bits with zeros. This method would enable the system to interpret a character every 2 bytes, recovering the content of the phrase. +For the above problem, **a straightforward solution is to store all characters as equal-length encodings**. As shown in Figure 3-7, each character in "Hello" occupies 1 byte, and each character in "算法" (algorithm) occupies 2 bytes. We can encode all characters in "Hello 算法" as 2 bytes in length by padding the high bits with 0. In this way, the system can parse one character every 2 bytes and restore the content of this phrase. ![Unicode encoding example](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }

Figure 3-7   Unicode encoding example

-However, as ASCII has shown us, encoding English only requires 1 byte. Using the above approach would double the space occupied by English text compared to ASCII encoding, which is a waste of memory space. Therefore, a more efficient Unicode encoding method is needed. +However, ASCII code has already proven to us that encoding English only requires 1 byte. If the above scheme is adopted, the size of English text will be twice that under ASCII encoding, which is very wasteful of memory space. Therefore, we need a more efficient Unicode encoding method. -## 3.4.4   UTF-8 encoding +## 3.4.4   Utf-8 Encoding -Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding**, using 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters need only 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters need 3 bytes, and some other rare characters need 4 bytes. +Currently, UTF-8 has become the most widely used Unicode encoding method internationally. **It is a variable-length encoding** that uses 1 to 4 bytes to represent a character, depending on the complexity of the character. ASCII characters only require 1 byte, Latin and Greek letters require 2 bytes, commonly used Chinese characters require 3 bytes, and some other rare characters require 4 bytes. -The encoding rules for UTF-8 are not complex and can be divided into two cases: +The encoding rules of UTF-8 are not complicated and can be divided into the following two cases. -- For 1-byte characters, set the highest bit to $0$, and the remaining 7 bits to the Unicode code point. Notably, ASCII characters occupy the first 128 code points in the Unicode set. This means that **UTF-8 encoding is backward compatible with ASCII**. This implies that UTF-8 can be used to parse ancient ASCII text. -- For characters of length $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and the $(n + 1)^{\text{th}}$ bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; the rest of the bits are used to fill the Unicode code point. +- For 1-byte characters, set the highest bit to $0$, and set the remaining 7 bits to the Unicode code point. It is worth noting that ASCII characters occupy the first 128 code points in the Unicode character set. That is to say, **UTF-8 encoding is backward compatible with ASCII code**. This means we can use UTF-8 to parse very old ASCII code text. +- For characters with a length of $n$ bytes (where $n > 1$), set the highest $n$ bits of the first byte to $1$, and set the $(n + 1)$-th bit to $0$; starting from the second byte, set the highest 2 bits of each byte to $10$; use all remaining bits to fill in the Unicode code point of the character. -Figure 3-8 shows the UTF-8 encoding for "Hello算法". It can be observed that since the highest $n$ bits are set to $1$, the system can determine the length of the character as $n$ by counting the number of highest bits set to $1$. +Figure 3-8 shows the UTF-8 encoding corresponding to "Hello算法". It can be observed that since the highest $n$ bits are all set to $1$, the system can parse the length of the character as $n$ by reading the number of highest bits that are $1$. -But why set the highest 2 bits of the remaining bytes to $10$? Actually, this $10$ serves as a kind of checksum. If the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly detect anomalies. +But why set the highest 2 bits of all other bytes to $10$? In fact, this $10$ can serve as a check symbol. Assuming the system starts parsing text from an incorrect byte, the $10$ at the beginning of the byte can help the system quickly determine an anomaly. -The reason for using $10$ as a checksum is that, under UTF-8 encoding rules, it's impossible for the highest two bits of a character to be $10$. This can be proven by contradiction: If the highest two bits of a character are $10$, it indicates that the character's length is $1$, corresponding to ASCII. However, the highest bit of an ASCII character should be $0$, which contradicts the assumption. +The reason for using $10$ as a check symbol is that under UTF-8 encoding rules, it is impossible for a character's highest two bits to be $10$. This conclusion can be proven by contradiction: assuming the highest two bits of a character are $10$, it means the length of the character is $1$, corresponding to ASCII code. However, the highest bit of ASCII code should be $0$, which contradicts the assumption. ![UTF-8 encoding example](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }

Figure 3-8   UTF-8 encoding example

-Apart from UTF-8, other common encoding methods include: +In addition to UTF-8, common encoding methods also include the following two. -- **UTF-16 encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters require 4 bytes. For 2-byte characters, the UTF-16 encoding equals the Unicode code point. -- **UTF-32 encoding**: Every character uses 4 bytes. This means UTF-32 occupies more space than UTF-8 and UTF-16, especially for texts with a high proportion of ASCII characters. +- **UTF-16 encoding**: Uses 2 or 4 bytes to represent a character. All ASCII characters and commonly used non-English characters are represented with 2 bytes; a few characters need to use 4 bytes. For 2-byte characters, UTF-16 encoding is equal to the Unicode code point. +- **UTF-32 encoding**: Every character uses 4 bytes. This means that UTF-32 takes up more space than UTF-8 and UTF-16, especially for text with a high proportion of ASCII characters. -From the perspective of storage space, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 to encode some non-English characters (such as Chinese) can be more efficient because it only requires 2 bytes, while UTF-8 might need 3 bytes. +From the perspective of storage space occupation, using UTF-8 to represent English characters is very efficient because it only requires 1 byte; using UTF-16 encoding for some non-English characters (such as Chinese) will be more efficient because it only requires 2 bytes, while UTF-8 may require 3 bytes. -From a compatibility perspective, UTF-8 is the most versatile, with many tools and libraries supporting UTF-8 as a priority. +From a compatibility perspective, UTF-8 has the best universality, and many tools and libraries support UTF-8 first. -## 3.4.5   Character encoding in programming languages +## 3.4.5   Character Encoding in Programming Languages -Historically, many programming languages utilized fixed-length encodings such as UTF-16 or UTF-32 for processing strings during program execution. This allows strings to be handled as arrays, offering several advantages: +For most past programming languages, strings during program execution use fixed-length encodings such as UTF-16 or UTF-32. Under fixed-length encoding, we can treat strings as arrays for processing, and this approach has the following advantages. -- **Random access**: Strings encoded in UTF-16 can be accessed randomly with ease. For UTF-8, which is a variable-length encoding, locating the $i^{th}$ character requires traversing the string from the start to the $i^{th}$ position, taking $O(n)$ time. -- **Character counting**: Similar to random access, counting the number of characters in a UTF-16 encoded string is an $O(1)$ operation. However, counting characters in a UTF-8 encoded string requires traversing the entire string. -- **String operations**: Many string operations like splitting, concatenating, inserting, and deleting are easier on UTF-16 encoded strings. These operations generally require additional computation on UTF-8 encoded strings to ensure the validity of the UTF-8 encoding. +- **Random access**: UTF-16 encoded strings can be easily accessed randomly. UTF-8 is a variable-length encoding. To find the $i$-th character, we need to traverse from the beginning of the string to the $i$-th character, which requires $O(n)$ time. +- **Character counting**: Similar to random access, calculating the length of a UTF-16 encoded string is also an $O(1)$ operation. However, calculating the length of a UTF-8 encoded string requires traversing the entire string. +- **String operations**: Many string operations (such as splitting, joining, inserting, deleting, etc.) on UTF-16 encoded strings are easier to perform. Performing these operations on UTF-8 encoded strings usually requires additional calculations to ensure that invalid UTF-8 encoding is not generated. -The design of character encoding schemes in programming languages is an interesting topic involving various factors: +In fact, the design of character encoding schemes for programming languages is a very interesting topic involving many factors. -- Java’s `String` type uses UTF-16 encoding, with each character occupying 2 bytes. This was based on the initial belief that 16 bits were sufficient to represent all possible characters and proven incorrect later. As the Unicode standard expanded beyond 16 bits, characters in Java may now be represented by a pair of 16-bit values, known as “surrogate pairs.” -- JavaScript and TypeScript use UTF-16 encoding for similar reasons as Java. When JavaScript was first introduced by Netscape in 1995, Unicode was still in its early stages, and 16-bit encoding was sufficient to represent all Unicode characters. -- C# uses UTF-16 encoding, largely because the .NET platform, designed by Microsoft, and many Microsoft technologies, including the Windows operating system, extensively use UTF-16 encoding. +- Java's `String` type uses UTF-16 encoding, with each character occupying 2 bytes. This is because at the beginning of Java language design, people believed that 16 bits were sufficient to represent all possible characters. However, this was an incorrect judgment. Later, the Unicode specification expanded beyond 16 bits, so characters in Java may now be represented by a pair of 16-bit values (called "surrogate pairs"). +- The strings of JavaScript and TypeScript use UTF-16 encoding for reasons similar to Java. When Netscape first introduced the JavaScript language in 1995, Unicode was still in its early stages of development, and at that time, using 16-bit encoding was sufficient to represent all Unicode characters. +- C# uses UTF-16 encoding mainly because the .NET platform was designed by Microsoft, and many of Microsoft's technologies (including the Windows operating system) extensively use UTF-16 encoding. -Due to the underestimation of character counts, these languages had to use "surrogate pairs" to represent Unicode characters exceeding 16 bits. This approach has its drawbacks: strings containing surrogate pairs may have characters occupying 2 or 4 bytes, losing the advantage of fixed-length encoding. Additionally, handling surrogate pairs adds complexity and debugging difficulty to programming. +Due to the underestimation of character quantities by the above programming languages, they had to adopt the "surrogate pair" method to represent Unicode characters with lengths exceeding 16 bits. This is a reluctant compromise. On the one hand, in strings containing surrogate pairs, one character may occupy 2 bytes or 4 bytes, thus losing the advantage of fixed-length encoding. On the other hand, handling surrogate pairs requires additional code, which increases the complexity and difficulty of debugging in programming. -Addressing these challenges, some languages have adopted alternative encoding strategies: +For the above reasons, some programming languages have proposed different encoding schemes. -- Python’s `str` type uses Unicode encoding with a flexible representation where the storage length of characters depends on the largest Unicode code point in the string. If all characters are ASCII, each character occupies 1 byte, 2 bytes for characters within the Basic Multilingual Plane (BMP), and 4 bytes for characters beyond the BMP. -- Go’s `string` type internally uses UTF-8 encoding. Go also provides the `rune` type for representing individual Unicode code points. -- Rust’s `str` and `String` types use UTF-8 encoding internally. Rust also offers the `char` type for individual Unicode code points. +- Python's `str` uses Unicode encoding and adopts a flexible string representation where the stored character length depends on the largest Unicode code point in the string. If all characters in the string are ASCII characters, each character occupies 1 byte; if there are characters exceeding the ASCII range but all within the Basic Multilingual Plane (BMP), each character occupies 2 bytes; if there are characters exceeding the BMP, each character occupies 4 bytes. +- Go language's `string` type uses UTF-8 encoding internally. Go language also provides the `rune` type, which is used to represent a single Unicode code point. +- Rust language's `str` and `String` types use UTF-8 encoding internally. Rust also provides the `char` type for representing a single Unicode code point. -It’s important to note that the above discussion pertains to how strings are stored in programming languages, **which is different from how strings are stored in files or transmitted over networks**. For file storage or network transmission, strings are usually encoded in UTF-8 format for optimal compatibility and space efficiency. +It should be noted that the above discussion is about how strings are stored in programming languages, **which is different from how strings are stored in files or transmitted over networks**. In file storage or network transmission, we usually encode strings into UTF-8 format to achieve optimal compatibility and space efficiency. diff --git a/en/docs/chapter_data_structure/classification_of_data_structure.md b/en/docs/chapter_data_structure/classification_of_data_structure.md index 33a7f5a7a..01ce2c320 100644 --- a/en/docs/chapter_data_structure/classification_of_data_structure.md +++ b/en/docs/chapter_data_structure/classification_of_data_structure.md @@ -2,57 +2,57 @@ comments: true --- -# 3.1   Classification of data structures +# 3.1   Classification of Data Structures -Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified into "logical structure" and "physical structure". +Common data structures include arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. They can be classified from two dimensions: "logical structure" and "physical structure". -## 3.1.1   Logical structure: linear and non-linear +## 3.1.1   Logical Structure: Linear and Non-Linear -**The logical structures reveal the logical relationships between data elements**. In arrays and linked lists, data are arranged in a specific sequence, demonstrating the linear relationship between data; while in trees, data are arranged hierarchically from the top down, showing the derived relationship between "ancestors" and "descendants"; and graphs are composed of nodes and edges, reflecting the intricate network relationship. +**Logical structure reveals the logical relationships between data elements**. In arrays and linked lists, data is arranged in a certain order, embodying the linear relationship between data; while in trees, data is arranged hierarchically from top to bottom, showing the derived relationship between "ancestors" and "descendants"; graphs are composed of nodes and edges, reflecting complex network relationships. -As shown in Figure 3-1, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating data is arranged linearly in logical relationships; non-linear structures, conversely, are arranged non-linearly. +As shown in Figure 3-1, logical structures can be divided into two major categories: "linear" and "non-linear". Linear structures are more intuitive, indicating that data is linearly arranged in logical relationships; non-linear structures are the opposite, arranged non-linearly. -- **Linear data structures**: Arrays, Linked Lists, Stacks, Queues, Hash Tables, where elements have a one-to-one sequential relationship. -- **Non-linear data structures**: Trees, Heaps, Graphs, Hash Tables. +- **Linear data structures**: Arrays, linked lists, stacks, queues, hash tables, where elements have a one-to-one sequential relationship. +- **Non-linear data structures**: Trees, heaps, graphs, hash tables. Non-linear data structures can be further divided into tree structures and network structures. -- **Tree structures**: Trees, Heaps, Hash Tables, where elements have a one-to-many relationship. -- **Network structures**: Graphs, where elements have a many-to-many relationships. +- **Tree structures**: Trees, heaps, hash tables, where elements have a one-to-many relationship. +- **Network structures**: Graphs, where elements have a many-to-many relationship. ![Linear and non-linear data structures](classification_of_data_structure.assets/classification_logic_structure.png){ class="animation-figure" }

Figure 3-1   Linear and non-linear data structures

-## 3.1.2   Physical structure: contiguous and dispersed +## 3.1.2   Physical Structure: Contiguous and Dispersed -**During the execution of an algorithm, the data being processed is stored in memory**. Figure 3-2 shows a computer memory stick where each black square is a physical memory space. We can think of memory as a vast Excel spreadsheet, with each cell capable of storing a certain amount of data. +**When an algorithm program runs, the data being processed is mainly stored in memory**. Figure 3-2 shows a computer memory stick, where each black square contains a memory space. We can imagine memory as a huge Excel spreadsheet, where each cell can store a certain amount of data. -**The system accesses the data at the target location by means of a memory address**. As shown in Figure 3-2, the computer assigns a unique identifier to each cell in the table according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access the data stored in memory. +**The system accesses data at the target location through memory addresses**. As shown in Figure 3-2, the computer assigns a number to each cell in the spreadsheet according to specific rules, ensuring that each memory space has a unique memory address. With these addresses, the program can access data in memory. -![Memory stick, memory spaces, memory addresses](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } +![Memory stick, memory space, memory address](classification_of_data_structure.assets/computer_memory_location.png){ class="animation-figure" } -

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

+

Figure 3-2   Memory stick, memory space, memory address

!!! tip - It's worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is more complex, involving concepts like address space, memory management, cache mechanisms, virtual memory, and physical memory. + It is worth noting that comparing memory to an Excel spreadsheet is a simplified analogy. The actual working mechanism of memory is quite complex, involving concepts such as address space, memory management, cache mechanisms, virtual memory, and physical memory. -Memory is a shared resource for all programs. When a block of memory is occupied by one program, it cannot be simultaneously used by other programs. **Therefore, memory resources are an important consideration in the design of data structures and algorithms**. For instance, the algorithm's peak memory usage should not exceed the remaining free memory of the system; if there is a lack of contiguous memory blocks, then the data structure chosen must be able to be stored in non-contiguous memory blocks. +Memory is a shared resource for all programs. When a block of memory is occupied by a program, it usually cannot be used by other programs at the same time. **Therefore, in the design of data structures and algorithms, memory resources are an important consideration**. For example, the peak memory occupied by an algorithm should not exceed the remaining free memory of the system; if there is a lack of contiguous large memory blocks, then the data structure chosen must be able to be stored in dispersed memory spaces. -As illustrated in Figure 3-3, **the physical structure reflects the way data is stored in computer memory** and it can be divided into contiguous space storage (arrays) and non-contiguous space storage (linked lists). The two types of physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. +As shown in Figure 3-3, **physical structure reflects the way data is stored in computer memory**, and can be divided into contiguous space storage (arrays) and dispersed space storage (linked lists). The two physical structures exhibit complementary characteristics in terms of time efficiency and space efficiency. ![Contiguous space storage and dispersed space storage](classification_of_data_structure.assets/classification_phisical_structure.png){ class="animation-figure" }

Figure 3-3   Contiguous space storage and dispersed space storage

-**It is worth noting that all data structures are implemented based on arrays, linked lists, or a combination of both**. For example, stacks and queues can be implemented using either arrays or linked lists; while implementations of hash tables may involve both arrays and linked lists. +It is worth noting that **all data structures are implemented based on arrays, linked lists, or a combination of both**. For example, stacks and queues can be implemented using either arrays or linked lists; while the implementation of hash tables may include both arrays and linked lists. -- **Array-based implementations**: Stacks, Queues, Hash Tables, Trees, Heaps, Graphs, Matrices, Tensors (arrays with dimensions $\geq 3$). -- **Linked-list-based implementations**: Stacks, Queues, Hash Tables, Trees, Heaps, Graphs, etc. +- **Can be implemented based on arrays**: Stacks, queues, hash tables, trees, heaps, graphs, matrices, tensors (arrays with dimensions $\geq 3$), etc. +- **Can be implemented based on linked lists**: Stacks, queues, hash tables, trees, heaps, graphs, etc. -Data structures implemented based on arrays are also called “Static Data Structures,” meaning their length cannot be changed after initialization. Conversely, those based on linked lists are called “Dynamic Data Structures,” which can still adjust their size during program execution. +After initialization, linked lists can still adjust their length during program execution, so they are also called "dynamic data structures". After initialization, the length of arrays cannot be changed, so they are also called "static data structures". It is worth noting that arrays can achieve length changes by reallocating memory, thus possessing a certain degree of "dynamism". !!! tip - If you find it challenging to comprehend the physical structure, it is recommended that you read the next chapter, "Arrays and Linked Lists," and revisit this section later. + If you find it difficult to understand physical structure, it is recommended to read the next chapter first, and then review this section. diff --git a/en/docs/chapter_data_structure/index.md b/en/docs/chapter_data_structure/index.md index 58c2be9de..84de2ec5f 100644 --- a/en/docs/chapter_data_structure/index.md +++ b/en/docs/chapter_data_structure/index.md @@ -3,20 +3,20 @@ comments: true icon: material/shape-outline --- -# Chapter 3.   Data structures +# Chapter 3.   Data Structures ![Data structures](../assets/covers/chapter_data_structure.jpg){ class="cover-image" } !!! abstract - Data structures serve as a robust and diverse framework. + Data structure is like a sturdy and diverse framework. - They offer a blueprint for the orderly organization of data, upon which algorithms come to life. + It provides a blueprint for the orderly organization of data, upon which algorithms come to life. ## Chapter contents -- [3.1   Classification of data structures](classification_of_data_structure.md) -- [3.2   Basic data types](basic_data_types.md) -- [3.3   Number encoding *](number_encoding.md) -- [3.4   Character encoding *](character_encoding.md) +- [3.1   Classification of Data Structures](classification_of_data_structure.md) +- [3.2   Basic Data Types](basic_data_types.md) +- [3.3   Number Encoding *](number_encoding.md) +- [3.4   Character Encoding *](character_encoding.md) - [3.5   Summary](summary.md) diff --git a/en/docs/chapter_data_structure/number_encoding.md b/en/docs/chapter_data_structure/number_encoding.md index 29275072d..bfa282694 100644 --- a/en/docs/chapter_data_structure/number_encoding.md +++ b/en/docs/chapter_data_structure/number_encoding.md @@ -2,29 +2,29 @@ comments: true --- -# 3.3   Number encoding * +# 3.3   Number Encoding * !!! tip - In this book, chapters marked with an asterisk '*' are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters. + In this book, chapters marked with an asterisk * are optional readings. If you are short on time or find them challenging, you may skip these initially and return to them after completing the essential chapters. -## 3.3.1   Integer encoding +## 3.3.1   Sign-Magnitude, 1's Complement, and 2's Complement -In the table from the previous section, we observed that all integer types can represent one more negative number than positive numbers, such as the `byte` range of $[-128, 127]$. This phenomenon seems counterintuitive, and its underlying reason involves knowledge of sign-magnitude, one's complement, and two's complement encoding. +In the table from the previous section, we found that all integer types can represent one more negative number than positive numbers. For example, the `byte` range is $[-128, 127]$. This phenomenon is counterintuitive, and its underlying reason involves knowledge of sign-magnitude, 1's complement, and 2's complement. -Firstly, it's important to note that **numbers are stored in computers using the two's complement form**. Before analyzing why this is the case, let's define these three encoding methods: +First, it should be noted that **numbers are stored in computers in the form of "2's complement"**. Before analyzing the reasons for this, let's first define these three concepts. -- **Sign-magnitude**: The highest bit of a binary representation of a number is considered the sign bit, where $0$ represents a positive number and $1$ represents a negative number. The remaining bits represent the value of the number. -- **One's complement**: The one's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by inverting all bits except the sign bit. -- **Two's complement**: The two's complement of a positive number is the same as its sign-magnitude. For negative numbers, it's obtained by adding $1$ to their one's complement. +- **Sign-magnitude**: We treat the highest bit of the binary representation of a number as the sign bit, where $0$ represents a positive number and $1$ represents a negative number, and the remaining bits represent the value of the number. +- **1's complement**: The 1's complement of a positive number is the same as its sign-magnitude. For a negative number, the 1's complement is obtained by inverting all bits except the sign bit of its sign-magnitude. +- **2's complement**: The 2's complement of a positive number is the same as its sign-magnitude. For a negative number, the 2's complement is obtained by adding $1$ to its 1's complement. -Figure 3-4 illustrates the conversions among sign-magnitude, one's complement, and two's complement: +Figure 3-4 shows the conversion methods among sign-magnitude, 1's complement, and 2's complement. -![Conversions between sign-magnitude, one's complement, and two's complement](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } +![Conversions among sign-magnitude, 1's complement, and 2's complement](number_encoding.assets/1s_2s_complement.png){ class="animation-figure" } -

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

+

Figure 3-4   Conversions among sign-magnitude, 1's complement, and 2's complement

-Although sign-magnitude is the most intuitive, it has limitations. For one, **negative numbers in sign-magnitude cannot be directly used in calculations**. For example, in sign-magnitude, calculating $1 + (-2)$ results in $-3$, which is incorrect. +Sign-magnitude, although the most intuitive, has some limitations. On one hand, **the sign-magnitude of negative numbers cannot be directly used in operations**. For example, calculating $1 + (-2)$ in sign-magnitude yields $-3$, which is clearly incorrect. $$ \begin{aligned} @@ -35,20 +35,20 @@ $$ \end{aligned} $$ -To address this, computers introduced the one's complement. If we convert to one's complement and calculate $1 + (-2)$, then convert the result back to sign-magnitude, we get the correct result of $-1$. +To solve this problem, computers introduced 1's complement. If we first convert sign-magnitude to 1's complement and calculate $1 + (-2)$ in 1's complement, then convert the result back to sign-magnitude, we can obtain the correct result of $-1$. $$ \begin{aligned} & 1 + (-2) \newline & \rightarrow 0000 \; 0001 \; \text{(Sign-magnitude)} + 1000 \; 0010 \; \text{(Sign-magnitude)} \newline -& = 0000 \; 0001 \; \text{(One's complement)} + 1111 \; 1101 \; \text{(One's complement)} \newline -& = 1111 \; 1110 \; \text{(One's complement)} \newline +& = 0000 \; 0001 \; \text{(1's complement)} + 1111 \; 1101 \; \text{(1's complement)} \newline +& = 1111 \; 1110 \; \text{(1's complement)} \newline & = 1000 \; 0001 \; \text{(Sign-magnitude)} \newline & \rightarrow -1 \end{aligned} $$ -Additionally, **there are two representations of zero in sign-magnitude**: $+0$ and $-0$. This means two different binary encodings for zero, which could lead to ambiguity. For example, in conditional checks, not differentiating between positive and negative zero might result in incorrect outcomes. Addressing this ambiguity would require additional checks, potentially reducing computational efficiency. +On the other hand, **the sign-magnitude of the number zero has two representations, $+0$ and $-0$**. This means that the number zero corresponds to two different binary encodings, which may cause ambiguity. For example, in conditional judgments, if we don't distinguish between positive zero and negative zero, it may lead to incorrect judgment results. If we want to handle the ambiguity of positive and negative zero, we need to introduce additional judgment operations, which may reduce the computational efficiency of the computer. $$ \begin{aligned} @@ -57,67 +57,67 @@ $$ \end{aligned} $$ -Like sign-magnitude, one's complement also suffers from the positive and negative zero ambiguity. Therefore, computers further introduced the two's complement. Let's observe the conversion process for negative zero in sign-magnitude, one's complement, and two's complement: +Like sign-magnitude, 1's complement also has the problem of positive and negative zero ambiguity. Therefore, computers further introduced 2's complement. Let's first observe the conversion process of negative zero from sign-magnitude to 1's complement to 2's complement: $$ \begin{aligned} -0 \rightarrow \; & 1000 \; 0000 \; \text{(Sign-magnitude)} \newline -= \; & 1111 \; 1111 \; \text{(One's complement)} \newline -= 1 \; & 0000 \; 0000 \; \text{(Two's complement)} \newline += \; & 1111 \; 1111 \; \text{(1's complement)} \newline += 1 \; & 0000 \; 0000 \; \text{(2's complement)} \newline \end{aligned} $$ -Adding $1$ to the one's complement of negative zero produces a carry, but with `byte` length being only 8 bits, the carried-over $1$ to the 9th bit is discarded. Therefore, **the two's complement of negative zero is $0000 \; 0000$**, the same as positive zero, thus resolving the ambiguity. +Adding $1$ to the 1's complement of negative zero produces a carry, but since the `byte` type has a length of only 8 bits, the $1$ that overflows to the 9th bit is discarded. That is to say, **the 2's complement of negative zero is $0000 \; 0000$, which is the same as the 2's complement of positive zero**. This means that in 2's complement representation, there is only one zero, and the positive and negative zero ambiguity is thus resolved. -One last puzzle is the $[-128, 127]$ range for `byte`, with an additional negative number, $-128$. We observe that for the interval $[-127, +127]$, all integers have corresponding sign-magnitude, one's complement, and two's complement, allowing for mutual conversion between them. +One last question remains: the range of the `byte` type is $[-128, 127]$, and how is the extra negative number $-128$ obtained? We notice that all integers in the interval $[-127, +127]$ have corresponding sign-magnitude, 1's complement, and 2's complement, and sign-magnitude and 2's complement can be converted to each other. -However, **the two's complement $1000 \; 0000$ is an exception without a corresponding sign-magnitude**. According to the conversion method, its sign-magnitude would be $0000 \; 0000$, indicating zero. This presents a contradiction because its two's complement should represent itself. Computers designate this special two's complement $1000 \; 0000$ as representing $-128$. In fact, the calculation of $(-1) + (-127)$ in two's complement results in $-128$. +However, **the 2's complement $1000 \; 0000$ is an exception, and it does not have a corresponding sign-magnitude**. According to the conversion method, we get that the sign-magnitude of this 2's complement is $0000 \; 0000$. This is clearly contradictory because this sign-magnitude represents the number $0$, and its 2's complement should be itself. The computer specifies that this special 2's complement $1000 \; 0000$ represents $-128$. In fact, the result of calculating $(-1) + (-127)$ in 2's complement is $-128$. $$ \begin{aligned} & (-127) + (-1) \newline & \rightarrow 1111 \; 1111 \; \text{(Sign-magnitude)} + 1000 \; 0001 \; \text{(Sign-magnitude)} \newline -& = 1000 \; 0000 \; \text{(One's complement)} + 1111 \; 1110 \; \text{(One's complement)} \newline -& = 1000 \; 0001 \; \text{(Two's complement)} + 1111 \; 1111 \; \text{(Two's complement)} \newline -& = 1000 \; 0000 \; \text{(Two's complement)} \newline +& = 1000 \; 0000 \; \text{(1's complement)} + 1111 \; 1110 \; \text{(1's complement)} \newline +& = 1000 \; 0001 \; \text{(2's complement)} + 1111 \; 1111 \; \text{(2's complement)} \newline +& = 1000 \; 0000 \; \text{(2's complement)} \newline & \rightarrow -128 \end{aligned} $$ -As you might have noticed, all these calculations are additions, hinting at an important fact: **computers' internal hardware circuits are primarily designed around addition operations**. This is because addition is simpler to implement in hardware compared to other operations like multiplication, division, and subtraction, allowing for easier parallelization and faster computation. +You may have noticed that all the above calculations are addition operations. This hints at an important fact: **the hardware circuits inside computers are mainly designed based on addition operations**. This is because addition operations are simpler to implement in hardware compared to other operations (such as multiplication, division, and subtraction), easier to parallelize, and have faster operation speeds. -It's important to note that this doesn't mean computers can only perform addition. **By combining addition with basic logical operations, computers can execute a variety of other mathematical operations**. For example, the subtraction $a - b$ can be translated into $a + (-b)$; multiplication and division can be translated into multiple additions or subtractions. +Please note that this does not mean that computers can only perform addition. **By combining addition with some basic logical operations, computers can implement various other mathematical operations**. For example, calculating the subtraction $a - b$ can be converted to calculating the addition $a + (-b)$; calculating multiplication and division can be converted to calculating multiple additions or subtractions. -We can now summarize the reason for using two's complement in computers: with two's complement representation, computers can use the same circuits and operations to handle both positive and negative number addition, eliminating the need for special hardware circuits for subtraction and avoiding the ambiguity of positive and negative zero. This greatly simplifies hardware design and enhances computational efficiency. +Now we can summarize the reasons why computers use 2's complement: based on 2's complement representation, computers can use the same circuits and operations to handle the addition of positive and negative numbers, without the need to design special hardware circuits to handle subtraction, and without the need to specially handle the ambiguity problem of positive and negative zero. This greatly simplifies hardware design and improves operational efficiency. -The design of two's complement is quite ingenious, and due to space constraints, we'll stop here. Interested readers are encouraged to explore further. +The design of 2's complement is very ingenious. Due to space limitations, we will stop here. Interested readers are encouraged to explore further. -## 3.3.2   Floating-point number encoding +## 3.3.2   Floating-Point Number Encoding -You might have noticed something intriguing: despite having the same length of 4 bytes, why does a `float` have a much larger range of values compared to an `int`? This seems counterintuitive, as one would expect the range to shrink for `float` since it needs to represent fractions. +Careful readers may have noticed: `int` and `float` have the same length, both are 4 bytes, but why does `float` have a much larger range than `int`? This is very counterintuitive because it stands to reason that `float` needs to represent decimals, so the range should be smaller. -In fact, **this is due to the different representation method used by floating-point numbers (`float`)**. Let's consider a 32-bit binary number as: +In fact, **this is because floating-point number `float` uses a different representation method**. Let's denote a 32-bit binary number as: $$ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0 $$ -According to the IEEE 754 standard, a 32-bit `float` consists of the following three parts: +According to the IEEE 754 standard, a 32-bit `float` consists of the following three parts. -- Sign bit $\mathrm{S}$: Occupies 1 bit, corresponding to $b_{31}$. -- Exponent bit $\mathrm{E}$: Occupies 8 bits, corresponding to $b_{30} b_{29} \ldots b_{23}$. -- Fraction bit $\mathrm{N}$: Occupies 23 bits, corresponding to $b_{22} b_{21} \ldots b_0$. +- Sign bit $\mathrm{S}$: occupies 1 bit, corresponding to $b_{31}$. +- Exponent bit $\mathrm{E}$: occupies 8 bits, corresponding to $b_{30} b_{29} \ldots b_{23}$. +- Fraction bit $\mathrm{N}$: occupies 23 bits, corresponding to $b_{22} b_{21} \ldots b_0$. -The value of a binary `float` number is calculated as: +The calculation method for the value corresponding to the binary `float` is: $$ -\text{val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2 - 127} \times \left(1 . b_{22} b_{21} \ldots b_0\right)_2 +\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2 $$ -Converted to a decimal formula, this becomes: +Converted to decimal, the calculation formula is: $$ -\text{val} = (-1)^{\mathrm{S}} \times 2^{\mathrm{E} - 127} \times (1 + \mathrm{N}) +\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N}) $$ The range of each component is: @@ -125,23 +125,23 @@ The range of each component is: $$ \begin{aligned} \mathrm{S} \in & \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline -(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} \times 2^{-i}) \subset [1, 2 - 2^{-23}] +(1 + \mathrm{N}) = & (1 + \sum_{i=1}^{23} b_{23-i} 2^{-i}) \subset [1, 2 - 2^{-23}] \end{aligned} $$ -![Example calculation of a float in IEEE 754 standard](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } +![Calculation example of float under IEEE 754 standard](number_encoding.assets/ieee_754_float.png){ class="animation-figure" } -

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

+

Figure 3-5   Calculation example of float under IEEE 754 standard

-Observing Figure 3-5, given an example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have: +Observing Figure 3-5, given example data $\mathrm{S} = 0$, $\mathrm{E} = 124$, $\mathrm{N} = 2^{-2} + 2^{-3} = 0.375$, we have: $$ -\text{val} = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 +\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875 $$ -Now we can answer the initial question: **The representation of `float` includes an exponent bit, leading to a much larger range than `int`**. Based on the above calculation, the maximum positive number representable by `float` is approximately $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$, and the minimum negative number is obtained by switching the sign bit. +Now we can answer the initial question: **the representation of `float` includes an exponent bit, resulting in a range far greater than `int`**. According to the above calculation, the maximum positive number that `float` can represent is $2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}$, and the minimum negative number can be obtained by switching the sign bit. -**However, the trade-off for `float`'s expanded range is a sacrifice in precision**. The integer type `int` uses all 32 bits to represent the number, with values evenly distributed; but due to the exponent bit, the larger the value of a `float`, the greater the difference between adjacent numbers. +**Although floating-point number `float` expands the range, its side effect is sacrificing precision**. The integer type `int` uses all 32 bits to represent numbers, and the numbers are evenly distributed; however, due to the existence of the exponent bit, the larger the value of floating-point number `float`, the larger the difference between two adjacent numbers tends to be. As shown in Table 3-2, exponent bits $\mathrm{E} = 0$ and $\mathrm{E} = 255$ have special meanings, **used to represent zero, infinity, $\mathrm{NaN}$, etc.** @@ -151,12 +151,12 @@ As shown in Table 3-2, exponent bits $\mathrm{E} = 0$ and $\mathrm{E} = 255$ hav | Exponent Bit E | Fraction Bit $\mathrm{N} = 0$ | Fraction Bit $\mathrm{N} \ne 0$ | Calculation Formula | | ------------------ | ----------------------------- | ------------------------------- | ---------------------------------------------------------------------- | -| $0$ | $\pm 0$ | Subnormal Numbers | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ | -| $1, 2, \dots, 254$ | Normal Numbers | Normal Numbers | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ | +| $0$ | $\pm 0$ | Subnormal Number | $(-1)^{\mathrm{S}} \times 2^{-126} \times (0.\mathrm{N})$ | +| $1, 2, \dots, 254$ | Normal Number | Normal Number | $(-1)^{\mathrm{S}} \times 2^{(\mathrm{E} -127)} \times (1.\mathrm{N})$ | | $255$ | $\pm \infty$ | $\mathrm{NaN}$ | |
-It's worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is $2^{-126}$, and the smallest positive subnormal number is $2^{-126} \times 2^{-23}$. +It is worth noting that subnormal numbers significantly improve the precision of floating-point numbers. The smallest positive normal number is $2^{-126}$, and the smallest positive subnormal number is $2^{-126} \times 2^{-23}$. -Double-precision `double` also uses a similar representation method to `float`, which is not elaborated here for brevity. +Double-precision `double` also uses a representation method similar to `float`, which will not be elaborated here. diff --git a/en/docs/chapter_data_structure/summary.md b/en/docs/chapter_data_structure/summary.md index ca8330acc..644c5423a 100644 --- a/en/docs/chapter_data_structure/summary.md +++ b/en/docs/chapter_data_structure/summary.md @@ -4,67 +4,67 @@ comments: true # 3.5   Summary -### 1.   Key review +### 1.   Key Review -- Data structures can be categorized from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data, while physical structure describes how data is stored in memory. -- Frequently used logical structures include linear structures, trees, and networks. We usually divide data structures into linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. -- When a program is running, data is stored in memory. Each memory space has a corresponding address, and the program accesses data through these addresses. -- Physical structures can be divided into continuous space storage (arrays) and discrete space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both. -- The basic data types in computers include integers (`byte`, `short`, `int`, `long`), floating-point numbers (`float`, `double`), characters (`char`), and booleans (`bool`). The value range of a data type depends on its size and representation. -- Sign-magnitude, 1's complement, 2's complement are three methods of encoding integers in computers, and they can be converted into each other. The most significant bit of the sign-magnitude is the sign bit, and the remaining bits represent the value of the number. -- Integers are encoded by 2's complement in computers. The benefits of this representation include (i) the computer can unify the addition of positive and negative integers, (ii) no need to design special hardware circuits for subtraction, and (iii) no ambiguity of positive and negative zero. -- The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bit, the range of floating-point numbers is much greater than that of integers, but at the cost of precision. -- ASCII is the earliest English character set, with 1 byte in length and a total of 127 characters. GBK is a popular Chinese character set, which includes more than 20,000 Chinese characters. Unicode aims to provide a complete character set standard that includes characters from various languages in the world, thus solving the garbled character problem caused by inconsistent character encoding methods. -- UTF-8 is the most popular and general Unicode encoding method. It is a variable-length encoding method with good scalability and space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 takes up less space than UTF-8. Programming languages like Java and C# use UTF-16 encoding by default. +- Data structures can be classified from two perspectives: logical structure and physical structure. Logical structure describes the logical relationships between data elements, while physical structure describes how data is stored in computer memory. +- Common logical structures include linear, tree, and network structures. We typically classify data structures as linear (arrays, linked lists, stacks, queues) and non-linear (trees, graphs, heaps) based on their logical structure. The implementation of hash tables may involve both linear and non-linear data structures. +- When a program runs, data is stored in computer memory. Each memory space has a corresponding memory address, and the program accesses data through these memory addresses. +- Physical structures are primarily divided into contiguous space storage (arrays) and dispersed space storage (linked lists). All data structures are implemented using arrays, linked lists, or a combination of both. +- Basic data types in computers include integers `byte`, `short`, `int`, `long`, floating-point numbers `float`, `double`, characters `char`, and booleans `bool`. Their value ranges depend on the size of space they occupy and their representation method. +- Sign-magnitude, 1's complement, and 2's complement are three methods for encoding numbers in computers, and they can be converted into each other. The most significant bit of sign-magnitude is the sign bit, and the remaining bits represent the value of the number. +- Integers are stored in computers in 2's complement form. Under 2's complement representation, computers can treat the addition of positive and negative numbers uniformly, without needing to design special hardware circuits for subtraction, and there is no ambiguity of positive and negative zero. +- The encoding of floating-point numbers consists of 1 sign bit, 8 exponent bits, and 23 fraction bits. Due to the exponent bits, the range of floating-point numbers is much larger than that of integers, at the cost of sacrificing precision. +- ASCII is the earliest English character set, with a length of 1 byte, containing a total of 127 characters. GBK is a commonly used Chinese character set, containing over 20,000 Chinese characters. Unicode is committed to providing a complete character set standard, collecting characters from various languages around the world, thereby solving the garbled text problem caused by inconsistent character encoding methods. +- UTF-8 is the most popular Unicode encoding method, with excellent universality. It is a variable-length encoding method with good scalability, effectively improving storage space efficiency. UTF-16 and UTF-32 are fixed-length encoding methods. When encoding Chinese characters, UTF-16 occupies less space than UTF-8. Programming languages such as Java and C# use UTF-16 encoding by default. ### 2.   Q & A -**Q**: Why does a hash table contain both linear and non-linear data structures? +**Q**: Why do hash tables contain both linear and non-linear data structures? -The underlying structure of a hash table is an array. To resolve hash collisions, we may use "chaining" (discussed in a later section, "Hash collision"): each bucket in the array points to a linked list, which may transform into a tree (usually a red-black tree) when its length is larger than a certain threshold. -From a storage perspective, the underlying structure of a hash table is an array, where each bucket might contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees). +The underlying structure of a hash table is an array. To resolve hash collisions, we may use "chaining" (discussed in the subsequent "Hash Collision" section): each bucket in the array points to a linked list, which may be converted to a tree (usually a red-black tree) when the list length exceeds a certain threshold. + +From a storage perspective, the underlying structure of a hash table is an array, where each bucket slot may contain a value, a linked list, or a tree. Therefore, hash tables may contain both linear data structures (arrays, linked lists) and non-linear data structures (trees). **Q**: Is the length of the `char` type 1 byte? -The length of the `char` type is determined by the encoding method of the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to save Unicode code points), so the length of the `char` type is 2 bytes. +The length of the `char` type is determined by the encoding method used by the programming language. For example, Java, JavaScript, TypeScript, and C# all use UTF-16 encoding (to store Unicode code points), so the `char` type has a length of 2 bytes. -**Q**: Is there any ambiguity when we refer to array-based data structures as "static data structures"? The stack can also perform "dynamic" operations such as popping and pushing. +**Q**: Is there ambiguity in referring to array-based data structures as "static data structures"? Stacks can also perform "dynamic" operations such as push and pop. -The stack can implement dynamic data operations, but the data structure is still "static" (the length is fixed). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the stack size exceeds the pre-allocated size, then the old array will be copied into a newly created and larger array. +Stacks can indeed implement dynamic data operations, but the data structure is still "static" (fixed length). Although array-based data structures can dynamically add or remove elements, their capacity is fixed. If the data volume exceeds the pre-allocated size, a new larger array needs to be created, and the contents of the old array must be copied to the new array. -**Q**: When building a stack (queue), its size is not specified, so why are they "static data structures"? +**Q**: When constructing a stack (queue), its size is not specified. Why are they "static data structures"? -In high-level programming languages, we do not need to manually specify the initial capacity of stacks (queues); this task is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is usually 10. Furthermore, the expansion operation is also completed automatically. See the subsequent "List" chapter for details. +In high-level programming languages, we do not need to manually specify the initial capacity of a stack (queue); this work is automatically completed within the class. For example, the initial capacity of Java's `ArrayList` is typically 10. Additionally, the expansion operation is also automatically implemented. See the subsequent "List" section for details. -**Q**:The method of converting the sign-magnitude to the 2's complement is "first negate and then add 1", so converting the 2's complement to the sign-magnitude should be its inverse operation "first subtract 1 and then negate". -However, the 2's complement can also be converted to the sign-magnitude through "first negate and then add 1", why is this? +**Q**: The method of converting sign-magnitude to 2's complement is "first negate then add 1". So converting 2's complement to sign-magnitude should be the inverse operation "first subtract 1 then negate". However, 2's complement can also be converted to sign-magnitude through "first negate then add 1". Why is this? -**A**:This is because the mutual conversion between the sign-magnitude and the 2's complement is equivalent to computing the "complement". We first define the complement: assuming $a + b = c$, then we say that $a$ is the complement of $b$ to $c$, and vice versa, $b$ is the complement of $a$ to $c$. +This is because the mutual conversion between sign-magnitude and 2's complement is actually the process of computing the "complement". Let us first define the complement: assuming $a + b = c$, then we say that $a$ is the complement of $b$ to $c$, and conversely, $b$ is the complement of $a$ to $c$. -Given a binary number $0010$ with length $n = 4$, if this number is the sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained by "first negating and then adding 1": +Given an $n = 4$ bit binary number $0010$, if we treat this number as sign-magnitude (ignoring the sign bit), then its 2's complement can be obtained through "first negate then add 1": $$ 0010 \rightarrow 1101 \rightarrow 1110 $$ -Observe that the sum of the sign-magnitude and the 2's complement is $0010 + 1110 = 10000$, i.e., the 2's complement $1110$ is the "complement" of the sign-magnitude $0010$ to $10000$. **This means that the above "first negate and then add 1" is equivalent to computing the complement to $10000$**. +We find that the sum of sign-magnitude and 2's complement is $0010 + 1110 = 10000$, which means the 2's complement $1110$ is the "complement" of sign-magnitude $0010$ to $10000$. **This means the above "first negate then add 1" is actually the process of computing the complement to $10000$**. -So, what is the "complement" of $1110$ to $10000$? We can still compute it by "negating first and then adding 1": +So, what is the "complement" of 2's complement $1110$ to $10000$? We can still use "first negate then add 1" to obtain it: $$ 1110 \rightarrow 0001 \rightarrow 0010 $$ -In other words, the sign-magnitude and the 2's complement are each other's "complement" to $10000$, so "sign-magnitude to 2's complement" and "2's complement to sign-magnitude" can be implemented with the same operation (first negate and then add 1). +In other words, sign-magnitude and 2's complement are each other's "complement" to $10000$, so "sign-magnitude to 2's complement" and "2's complement to sign-magnitude" can be implemented using the same operation (first negate then add 1). -Of course, we can also use the inverse operation of "first negate and then add 1" to find the sign-magnitude of the 2's complement $1110$, that is, "first subtract 1 and then negate": +Of course, we can also use the inverse operation to find the sign-magnitude of 2's complement $1110$, that is, "first subtract 1 then negate": $$ 1110 \rightarrow 1101 \rightarrow 0010 $$ -To sum up, "first negate and then add 1" and "first subtract 1 and then negate" are both computing the complement to $10000$, and they are equivalent. +In summary, both "first negate then add 1" and "first subtract 1 then negate" are computing the complement to $10000$, and they are equivalent. -Essentially, the "negate" operation is actually to find the complement to $1111$ (because `sign-magnitude + 1's complement = 1111` always holds); and the 1's complement plus 1 is equal to the 2's complement to $10000$. +Essentially, the "negate" operation is actually finding the complement to $1111$ (because `sign-magnitude + 1's complement = 1111` always holds); and adding 1 to the 1's complement yields the 2's complement, which is the complement to $10000$. -We take $n = 4$ as an example in the above, and it can be generalized to any binary number with any number of digits. \ No newline at end of file +The above uses $n = 4$ as an example, and it can be generalized to binary numbers of any number of bits. diff --git a/en/docs/chapter_divide_and_conquer/binary_search_recur.md b/en/docs/chapter_divide_and_conquer/binary_search_recur.md index 4f560db15..31192cd6b 100644 --- a/en/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/en/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -2,47 +2,47 @@ comments: true --- -# 12.2   Divide and conquer search strategy +# 12.2   Divide and Conquer Search Strategy -We have learned that search algorithms fall into two main categories. +We have already learned that search algorithms are divided into two major categories. -- **Brute-force search**: It is implemented by traversing the data structure, with a time complexity of $O(n)$. -- **Adaptive search**: It utilizes a unique data organization form or prior information, and its time complexity can reach $O(\log n)$ or even $O(1)$. +- **Brute-force search**: Implemented by traversing the data structure, with a time complexity of $O(n)$. +- **Adaptive search**: Utilizes unique data organization forms or prior information, with time complexity reaching $O(\log n)$ or even $O(1)$. -In fact, **search algorithms with a time complexity of $O(\log n)$ are usually based on the divide-and-conquer strategy**, such as binary search and trees. +In fact, **search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy**, such as binary search and trees. - Each step of binary search divides the problem (searching for a target element in an array) into a smaller problem (searching for the target element in half of the array), continuing until the array is empty or the target element is found. -- Trees represent the divide-and-conquer idea, where in data structures like binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$. +- Trees are representative of the divide and conquer idea. In data structures such as binary search trees, AVL trees, and heaps, the time complexity of various operations is $O(\log n)$. -The divide-and-conquer strategy of binary search is as follows. +The divide and conquer strategy of binary search is as follows. -- **The problem can be divided**: Binary search recursively divides the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element. -- **Subproblems are independent**: In binary search, each round handles one subproblem, unaffected by other subproblems. -- **The solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved. +- **The problem can be decomposed**: Binary search recursively decomposes the original problem (searching in an array) into subproblems (searching in half of the array), achieved by comparing the middle element with the target element. +- **Subproblems are independent**: In binary search, each round only processes one subproblem, which is not affected by other subproblems. +- **Solutions of subproblems do not need to be merged**: Binary search aims to find a specific element, so there is no need to merge the solutions of subproblems. When a subproblem is solved, the original problem is also solved. -Divide-and-conquer can enhance search efficiency because brute-force search can only eliminate one option per round, **whereas divide-and-conquer can eliminate half of the options**. +Divide and conquer can improve search efficiency because brute-force search can only eliminate one option per round, **while divide and conquer search can eliminate half of the options per round**. -### 1.   Implementing binary search based on divide-and-conquer +### 1.   Implementing Binary Search Based on Divide and Conquer -In previous chapters, binary search was implemented based on iteration. Now, we implement it based on divide-and-conquer (recursion). +In previous sections, binary search was implemented based on iteration. Now we implement it based on divide and conquer (recursion). !!! question - Given an ordered array `nums` of length $n$, where all elements are unique, please find the element `target`. + Given a sorted array `nums` of length $n$, where all elements are unique, find the element `target`. -From a divide-and-conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$. +From a divide and conquer perspective, we denote the subproblem corresponding to the search interval $[i, j]$ as $f(i, j)$. -Starting from the original problem $f(0, n-1)$, perform the binary search through the following steps. +Starting from the original problem $f(0, n-1)$, perform binary search through the following steps. 1. Calculate the midpoint $m$ of the search interval $[i, j]$, and use it to eliminate half of the search interval. 2. Recursively solve the subproblem reduced by half in size, which could be $f(i, m-1)$ or $f(m+1, j)$. -3. Repeat steps `1.` and `2.`, until `target` is found or the interval is empty and returns. +3. Repeat steps `1.` and `2.` until `target` is found or the interval is empty and return. -Figure 12-4 shows the divide-and-conquer process of binary search for element $6$ in an array. +Figure 12-4 shows the divide and conquer process of binary search for element $6$ in an array. -![The divide-and-conquer process of binary search](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } +![Divide and conquer process of binary search](binary_search_recur.assets/binary_search_recur.png){ class="animation-figure" } -

Figure 12-4   The divide-and-conquer process of binary search

+

Figure 12-4   Divide and conquer process of binary search

In the implementation code, we declare a recursive function `dfs()` to solve the problem $f(i, j)$: @@ -51,25 +51,25 @@ In the implementation code, we declare a recursive function `dfs()` to solve the ```python title="binary_search_recur.py" def dfs(nums: list[int], target: int, i: int, j: int) -> int: """Binary search: problem f(i, j)""" - # If the interval is empty, indicating no target element, return -1 + # If the interval is empty, it means there is no target element, return -1 if i > j: return -1 - # Calculate midpoint index m + # Calculate the midpoint index m m = (i + j) // 2 if nums[m] < target: - # Recursive subproblem f(m+1, j) + # Recursion subproblem f(m+1, j) return dfs(nums, target, m + 1, j) elif nums[m] > target: - # Recursive subproblem f(i, m-1) + # Recursion subproblem f(i, m-1) return dfs(nums, target, i, m - 1) else: - # Found the target element, thus return its index + # Found the target element, return its index return m def binary_search(nums: list[int], target: int) -> int: """Binary search""" n = len(nums) - # Solve problem f(0, n-1) + # Solve the problem f(0, n-1) return dfs(nums, target, 0, n - 1) ``` @@ -78,20 +78,20 @@ In the implementation code, we declare a recursive function `dfs()` to solve the ```cpp title="binary_search_recur.cpp" /* Binary search: problem f(i, j) */ int dfs(vector &nums, int target, int i, int j) { - // If the interval is empty, indicating no target element, return -1 + // If the interval is empty, it means there is no target element, return -1 if (i > j) { return -1; } - // Calculate midpoint index m - int m = i + (j - i) / 2; + // Calculate the midpoint index m + int m = (i + j) / 2; if (nums[m] < target) { - // Recursive subproblem f(m+1, j) + // Recursion subproblem f(m+1, j) return dfs(nums, target, m + 1, j); } else if (nums[m] > target) { - // Recursive subproblem f(i, m-1) + // Recursion subproblem f(i, m-1) return dfs(nums, target, i, m - 1); } else { - // Found the target element, thus return its index + // Found the target element, return its index return m; } } @@ -99,7 +99,7 @@ In the implementation code, we declare a recursive function `dfs()` to solve the /* Binary search */ int binarySearch(vector &nums, int target) { int n = nums.size(); - // Solve problem f(0, n-1) + // Solve the problem f(0, n-1) return dfs(nums, target, 0, n - 1); } ``` @@ -109,20 +109,20 @@ In the implementation code, we declare a recursive function `dfs()` to solve the ```java title="binary_search_recur.java" /* Binary search: problem f(i, j) */ int dfs(int[] nums, int target, int i, int j) { - // If the interval is empty, indicating no target element, return -1 + // If the interval is empty, it means there is no target element, return -1 if (i > j) { return -1; } - // Calculate midpoint index m - int m = i + (j - i) / 2; + // Calculate the midpoint index m + int m = (i + j) / 2; if (nums[m] < target) { - // Recursive subproblem f(m+1, j) + // Recursion subproblem f(m+1, j) return dfs(nums, target, m + 1, j); } else if (nums[m] > target) { - // Recursive subproblem f(i, m-1) + // Recursion subproblem f(i, m-1) return dfs(nums, target, i, m - 1); } else { - // Found the target element, thus return its index + // Found the target element, return its index return m; } } @@ -130,7 +130,7 @@ In the implementation code, we declare a recursive function `dfs()` to solve the /* Binary search */ int binarySearch(int[] nums, int target) { int n = nums.length; - // Solve problem f(0, n-1) + // Solve the problem f(0, n-1) return dfs(nums, target, 0, n - 1); } ``` @@ -138,87 +138,314 @@ In the implementation code, we declare a recursive function `dfs()` to solve the === "C#" ```csharp title="binary_search_recur.cs" - [class]{binary_search_recur}-[func]{DFS} + /* Binary search: problem f(i, j) */ + int DFS(int[] nums, int target, int i, int j) { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1; + } + // Calculate the midpoint index m + int m = (i + j) / 2; + if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + return DFS(nums, target, m + 1, j); + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + return DFS(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{binary_search_recur}-[func]{BinarySearch} + /* Binary search */ + int BinarySearch(int[] nums, int target) { + int n = nums.Length; + // Solve the problem f(0, n-1) + return DFS(nums, target, 0, n - 1); + } ``` === "Go" ```go title="binary_search_recur.go" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + func dfs(nums []int, target, i, j int) int { + // If interval is empty, indicating no target element, return -1 + if i > j { + return -1 + } + // Calculate midpoint index + m := i + ((j - i) >> 1) + // Compare midpoint with target element + if nums[m] < target { + // If smaller, recurse on right half of array + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m+1, j) + } else if nums[m] > target { + // If larger, recurse on left half of array + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m-1) + } else { + // Found the target element, return its index + return m + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + func binarySearch(nums []int, target int) int { + n := len(nums) + return dfs(nums, target, 0, n-1) + } ``` === "Swift" ```swift title="binary_search_recur.swift" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + func dfs(nums: [Int], target: Int, i: Int, j: Int) -> Int { + // If the interval is empty, it means there is no target element, return -1 + if i > j { + return -1 + } + // Calculate the midpoint index m + let m = (i + j) / 2 + if nums[m] < target { + // Recursion subproblem f(m+1, j) + return dfs(nums: nums, target: target, i: m + 1, j: j) + } else if nums[m] > target { + // Recursion subproblem f(i, m-1) + return dfs(nums: nums, target: target, i: i, j: m - 1) + } else { + // Found the target element, return its index + return m + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + func binarySearch(nums: [Int], target: Int) -> Int { + // Solve the problem f(0, n-1) + dfs(nums: nums, target: target, i: nums.startIndex, j: nums.endIndex - 1) + } ``` === "JS" ```javascript title="binary_search_recur.js" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + function dfs(nums, target, i, j) { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1; + } + // Calculate the midpoint index m + const m = i + ((j - i) >> 1); + if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + function binarySearch(nums, target) { + const n = nums.length; + // Solve the problem f(0, n-1) + return dfs(nums, target, 0, n - 1); + } ``` === "TS" ```typescript title="binary_search_recur.ts" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + function dfs(nums: number[], target: number, i: number, j: number): number { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1; + } + // Calculate the midpoint index m + const m = i + ((j - i) >> 1); + if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + function binarySearch(nums: number[], target: number): number { + const n = nums.length; + // Solve the problem f(0, n-1) + return dfs(nums, target, 0, n - 1); + } ``` === "Dart" ```dart title="binary_search_recur.dart" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + int dfs(List nums, int target, int i, int j) { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1; + } + // Calculate the midpoint index m + int m = (i + j) ~/ 2; + if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + int binarySearch(List nums, int target) { + int n = nums.length; + // Solve the problem f(0, n-1) + return dfs(nums, target, 0, n - 1); + } ``` === "Rust" ```rust title="binary_search_recur.rs" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + fn dfs(nums: &[i32], target: i32, i: i32, j: i32) -> i32 { + // If the interval is empty, it means there is no target element, return -1 + if i > j { + return -1; + } + let m: i32 = i + (j - i) / 2; + if nums[m as usize] < target { + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if nums[m as usize] > target { + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{}-[func]{binary_search} + /* Binary search */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + let n = nums.len() as i32; + // Solve the problem f(0, n-1) + dfs(nums, target, 0, n - 1) + } ``` === "C" ```c title="binary_search_recur.c" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + int dfs(int nums[], int target, int i, int j) { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1; + } + // Calculate the midpoint index m + int m = (i + j) / 2; + if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j); + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1); + } else { + // Found the target element, return its index + return m; + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + int binarySearch(int nums[], int target, int numsSize) { + int n = numsSize; + // Solve the problem f(0, n-1) + return dfs(nums, target, 0, n - 1); + } ``` === "Kotlin" ```kotlin title="binary_search_recur.kt" - [class]{}-[func]{dfs} + /* Binary search: problem f(i, j) */ + fun dfs( + nums: IntArray, + target: Int, + i: Int, + j: Int + ): Int { + // If the interval is empty, it means there is no target element, return -1 + if (i > j) { + return -1 + } + // Calculate the midpoint index m + val m = (i + j) / 2 + return if (nums[m] < target) { + // Recursion subproblem f(m+1, j) + dfs(nums, target, m + 1, j) + } else if (nums[m] > target) { + // Recursion subproblem f(i, m-1) + dfs(nums, target, i, m - 1) + } else { + // Found the target element, return its index + m + } + } - [class]{}-[func]{binarySearch} + /* Binary search */ + fun binarySearch(nums: IntArray, target: Int): Int { + val n = nums.size + // Solve the problem f(0, n-1) + return dfs(nums, target, 0, n - 1) + } ``` === "Ruby" ```ruby title="binary_search_recur.rb" - [class]{}-[func]{dfs} + ### Binary search: problem f(i, j) ### + def dfs(nums, target, i, j) + # If the interval is empty, it means there is no target element, return -1 + return -1 if i > j + + # Calculate the midpoint index m + m = (i + j) / 2 - [class]{}-[func]{binary_search} - ``` - -=== "Zig" - - ```zig title="binary_search_recur.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} + if nums[m] < target + # Recursion subproblem f(m+1, j) + return dfs(nums, target, m + 1, j) + elsif nums[m] > target + # Recursion subproblem f(i, m-1) + return dfs(nums, target, i, m - 1) + else + # Found the target element, return its index + return m + end + end + + ### Binary search ### + def binary_search(nums, target) + n = nums.length + # Solve the problem f(0, n-1) + dfs(nums, target, 0, n - 1) + end ``` diff --git a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 98dcb4252..ff25ffea9 100644 --- a/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/en/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -2,74 +2,74 @@ comments: true --- -# 12.3   Building a binary tree problem +# 12.3   Building a Binary Tree Problem !!! question - Given the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence of a binary tree, construct the binary tree and return its root node. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5). + Given the preorder traversal `preorder` and inorder traversal `inorder` of a binary tree, construct the binary tree and return the root node of the binary tree. Assume there are no duplicate node values in the binary tree (as shown in Figure 12-5). ![Example data for building a binary tree](build_binary_tree_problem.assets/build_tree_example.png){ class="animation-figure" }

Figure 12-5   Example data for building a binary tree

-### 1.   Determining if it is a divide-and-conquer problem +### 1.   Determining If It Is a Divide and Conquer Problem -The original problem of building a binary tree from the `preorder` and the `inorder` sequences is a typical divide-and-conquer problem. +The original problem is defined as constructing a binary tree from `preorder` and `inorder`, which is a typical divide and conquer problem. -- **The problem can be decomposed**: From the perspective of divide-and-conquer, we can divide the original problem into two subproblems—building the left subtree and building the right subtree—plus one operation of initializing the root node. For each subtree (subproblem), we continue applying the same approach, partitioning it into smaller subtrees (subproblems), until reaching the smallest subproblem (an empty subtree). -- **The subproblems are independent**: The left and right subtrees do not overlap. When building the left subtree, we only need the segments of the in-order and pre-order traversals that correspond to the left subtree. The same approach applies to the right subtree. -- **Solutions to subproblems can be combined**: Once we have constructed the left and right subtrees (the subproblem solutions), we can attach them to the root node to obtain the solution to the original problem. +- **The problem can be decomposed**: From a divide and conquer perspective, we can divide the original problem into two subproblems: constructing the left subtree and constructing the right subtree, plus one operation: initializing the root node. For each subtree (subproblem), we can still reuse the above division method, dividing it into smaller subtrees (subproblems) until the smallest subproblem (empty subtree) is reached. +- **Subproblems are independent**: The left and right subtrees are independent of each other; there is no overlap between them. When constructing the left subtree, we only need to focus on the parts of the inorder and preorder traversals corresponding to the left subtree. The same applies to the right subtree. +- **Solutions of subproblems can be merged**: Once we have the left and right subtrees (solutions of subproblems), we can link them to the root node to obtain the solution to the original problem. -### 2.   How to divide the subtrees +### 2.   How to Divide Subtrees -Based on the above analysis, this problem can be solved using divide-and-conquer. **However, how do we use the pre-order traversal `preorder` sequence and the in-order traversal `inorder` sequence to divide the left and right subtrees?** +Based on the above analysis, this problem can be solved using divide and conquer, **but how do we divide the left and right subtrees through the preorder traversal `preorder` and inorder traversal `inorder`**? -By definition, both the `preorder` and `inorder` sequences can be divided into three parts: +According to the definition, both `preorder` and `inorder` can be divided into three parts. -- Pre-order traversal: `[ Root | Left Subtree | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 3 | 9 | 2 1 7 ]`. -- In-order traversal: `[ Left Subtree | Root | Right Subtree ]`. For example, in the figure, the tree corresponds to `[ 9 | 3 | 1 2 7 ]`. +- Preorder traversal: `[ Root Node | Left Subtree | Right Subtree ]`, for example, the tree in Figure 12-5 corresponds to `[ 3 | 9 | 2 1 7 ]`. +- Inorder traversal: `[ Left Subtree | Root Node | Right Subtree ]`, for example, the tree in Figure 12-5 corresponds to `[ 9 | 3 | 1 2 7 ]`. -Using the data from the preceding figure, we can follow the steps shown in the next figure to obtain the division results: +Using the data from the figure above as an example, we can obtain the division results through the steps shown in Figure 12-6. -1. The first element 3 in the pre-order traversal is the value of the root node. -2. Find the index of the root node 3 in the `inorder` sequence, and use this index to split `inorder` into `[ 9 | 3 | 1 2 7 ]`. -3. According to the split of the `inorder` sequence, it is straightforward to determine that the left and right subtrees contain 1 and 3 nodes, respectively, so we can split the `preorder` sequence into `[ 3 | 9 | 2 1 7 ]` accordingly. +1. The first element 3 in the preorder traversal is the value of the root node. +2. Find the index of root node 3 in `inorder`, and use this index to divide `inorder` into `[ 9 | 3 | 1 2 7 ]`. +3. Based on the division result of `inorder`, it is easy to determine that the left and right subtrees have 1 and 3 nodes respectively, allowing us to divide `preorder` into `[ 3 | 9 | 2 1 7 ]`. -![Dividing the subtrees in pre-order and in-order traversals](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" } +![Dividing subtrees in preorder and inorder traversals](build_binary_tree_problem.assets/build_tree_preorder_inorder_division.png){ class="animation-figure" } -

Figure 12-6   Dividing the subtrees in pre-order and in-order traversals

+

Figure 12-6   Dividing subtrees in preorder and inorder traversals

-### 3.   Describing subtree ranges based on variables +### 3.   Describing Subtree Intervals Based on Variables -Based on the above division method, **we have now obtained the index ranges of the root, left subtree, and right subtree in the `preorder` and `inorder` sequences**. To describe these index ranges, we use several pointer variables. +Based on the above division method, **we have obtained the index intervals of the root node, left subtree, and right subtree in `preorder` and `inorder`**. To describe these index intervals, we need to use several pointer variables. -- Let the index of the current tree's root node in the `preorder` sequence be denoted as $i$. -- Let the index of the current tree's root node in the `inorder` sequence be denoted as $m$. -- Let the index range of the current tree in the `inorder` sequence be denoted as $[l, r]$. +- Denote the index of the current tree's root node in `preorder` as $i$. +- Denote the index of the current tree's root node in `inorder` as $m$. +- Denote the index interval of the current tree in `inorder` as $[l, r]$. -As shown in Table 12-1, these variables represent the root node’s index in the `preorder` sequence and the index ranges of the subtrees in the `inorder` sequence. +As shown in Table 12-1, through these variables we can represent the index of the root node in `preorder` and the index intervals of the subtrees in `inorder`. -

Table 12-1   Indexes of the root node and subtrees in pre-order and in-order traversals

+

Table 12-1   Indices of root node and subtrees in preorder and inorder traversals

-| | Root node index in `preorder` | Subtree index range in `inorder` | -| ------------- | ----------------------------- | ----------------------------------- | -| Current tree | $i$ | $[l, r]$ | -| Left subtree | $i + 1$ | $[l, m-1]$ | -| Right subtree | $i + 1 + (m - l)$ | $[m+1, r]$ | +| | Root node index in `preorder` | Subtree index interval in `inorder` | +| ------------ | ----------------------------- | ----------------------------------- | +| Current tree | $i$ | $[l, r]$ | +| Left subtree | $i + 1$ | $[l, m-1]$ | +| Right subtree| $i + 1 + (m - l)$ | $[m+1, r]$ |
-Please note that $(m-l)$ in the right subtree root index represents "the number of nodes in the left subtree." It may help to consult Figure 12-7 for a clearer understanding. +Please note that $(m-l)$ in the right subtree root node index means "the number of nodes in the left subtree". It is recommended to understand this in conjunction with Figure 12-7. -![Indexes of the root node and left and right subtrees](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } +![Index interval representation of root node and left and right subtrees](build_binary_tree_problem.assets/build_tree_division_pointers.png){ class="animation-figure" } -

Figure 12-7   Indexes of the root node and left and right subtrees

+

Figure 12-7   Index interval representation of root node and left and right subtrees

-### 4.   Code implementation +### 4.   Code Implementation -To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` sequence to their indexes: +To improve the efficiency of querying $m$, we use a hash table `hmap` to store the mapping from elements in the `inorder` array to their indices: === "Python" @@ -81,24 +81,24 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t l: int, r: int, ) -> TreeNode | None: - """Build binary tree: Divide and conquer""" - # Terminate when subtree interval is empty + """Build binary tree: divide and conquer""" + # Terminate when the subtree interval is empty if r - l < 0: return None - # Initialize root node + # Initialize the root node root = TreeNode(preorder[i]) - # Query m to divide left and right subtrees + # Query m to divide the left and right subtrees m = inorder_map[preorder[i]] - # Subproblem: build left subtree + # Subproblem: build the left subtree root.left = dfs(preorder, inorder_map, i + 1, l, m - 1) - # Subproblem: build right subtree + # Subproblem: build the right subtree root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r) - # Return root node + # Return the root node return root def build_tree(preorder: list[int], inorder: list[int]) -> TreeNode | None: """Build binary tree""" - # Initialize hash table, storing in-order elements to indices mapping + # Initialize hash map, storing the mapping from inorder elements to indices inorder_map = {val: i for i, val in enumerate(inorder)} root = dfs(preorder, inorder_map, 0, 0, len(inorder) - 1) return root @@ -107,26 +107,26 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t === "C++" ```cpp title="build_tree.cpp" - /* Build binary tree: Divide and conquer */ + /* Build binary tree: divide and conquer */ TreeNode *dfs(vector &preorder, unordered_map &inorderMap, int i, int l, int r) { - // Terminate when subtree interval is empty + // Terminate when the subtree interval is empty if (r - l < 0) return NULL; - // Initialize root node + // Initialize the root node TreeNode *root = new TreeNode(preorder[i]); - // Query m to divide left and right subtrees + // Query m to divide the left and right subtrees int m = inorderMap[preorder[i]]; - // Subproblem: build left subtree + // Subproblem: build the left subtree root->left = dfs(preorder, inorderMap, i + 1, l, m - 1); - // Subproblem: build right subtree + // Subproblem: build the right subtree root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); - // Return root node + // Return the root node return root; } /* Build binary tree */ TreeNode *buildTree(vector &preorder, vector &inorder) { - // Initialize hash table, storing in-order elements to indices mapping + // Initialize hash map, storing the mapping from inorder elements to indices unordered_map inorderMap; for (int i = 0; i < inorder.size(); i++) { inorderMap[inorder[i]] = i; @@ -139,26 +139,26 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t === "Java" ```java title="build_tree.java" - /* Build binary tree: Divide and conquer */ + /* Build binary tree: divide and conquer */ TreeNode dfs(int[] preorder, Map inorderMap, int i, int l, int r) { - // Terminate when subtree interval is empty + // Terminate when the subtree interval is empty if (r - l < 0) return null; - // Initialize root node + // Initialize the root node TreeNode root = new TreeNode(preorder[i]); - // Query m to divide left and right subtrees + // Query m to divide the left and right subtrees int m = inorderMap.get(preorder[i]); - // Subproblem: build left subtree + // Subproblem: build the left subtree root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); - // Subproblem: build right subtree + // Subproblem: build the right subtree root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); - // Return root node + // Return the root node return root; } /* Build binary tree */ TreeNode buildTree(int[] preorder, int[] inorder) { - // Initialize hash table, storing in-order elements to indices mapping + // Initialize hash map, storing the mapping from inorder elements to indices Map inorderMap = new HashMap<>(); for (int i = 0; i < inorder.length; i++) { inorderMap.put(inorder[i], i); @@ -171,92 +171,348 @@ To improve the efficiency of querying $m$, we use a hash table `hmap` to store t === "C#" ```csharp title="build_tree.cs" - [class]{build_tree}-[func]{DFS} + /* Build binary tree: divide and conquer */ + TreeNode? DFS(int[] preorder, Dictionary inorderMap, int i, int l, int r) { + // Terminate when the subtree interval is empty + if (r - l < 0) + return null; + // Initialize the root node + TreeNode root = new(preorder[i]); + // Query m to divide the left and right subtrees + int m = inorderMap[preorder[i]]; + // Subproblem: build the left subtree + root.left = DFS(preorder, inorderMap, i + 1, l, m - 1); + // Subproblem: build the right subtree + root.right = DFS(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // Return the root node + return root; + } - [class]{build_tree}-[func]{BuildTree} + /* Build binary tree */ + TreeNode? BuildTree(int[] preorder, int[] inorder) { + // Initialize hash map, storing the mapping from inorder elements to indices + Dictionary inorderMap = []; + for (int i = 0; i < inorder.Length; i++) { + inorderMap.TryAdd(inorder[i], i); + } + TreeNode? root = DFS(preorder, inorderMap, 0, 0, inorder.Length - 1); + return root; + } ``` === "Go" ```go title="build_tree.go" - [class]{}-[func]{dfsBuildTree} + /* Build binary tree: divide and conquer */ + func dfsBuildTree(preorder []int, inorderMap map[int]int, i, l, r int) *TreeNode { + // Terminate when the subtree interval is empty + if r-l < 0 { + return nil + } + // Initialize the root node + root := NewTreeNode(preorder[i]) + // Query m to divide the left and right subtrees + m := inorderMap[preorder[i]] + // Subproblem: build the left subtree + root.Left = dfsBuildTree(preorder, inorderMap, i+1, l, m-1) + // Subproblem: build the right subtree + root.Right = dfsBuildTree(preorder, inorderMap, i+1+m-l, m+1, r) + // Return the root node + return root + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + func buildTree(preorder, inorder []int) *TreeNode { + // Initialize hash map, storing the mapping from inorder elements to indices + 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" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + func dfs(preorder: [Int], inorderMap: [Int: Int], i: Int, l: Int, r: Int) -> TreeNode? { + // Terminate when the subtree interval is empty + if r - l < 0 { + return nil + } + // Initialize the root node + let root = TreeNode(x: preorder[i]) + // Query m to divide the left and right subtrees + let m = inorderMap[preorder[i]]! + // Subproblem: build the left subtree + root.left = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1, l: l, r: m - 1) + // Subproblem: build the right subtree + root.right = dfs(preorder: preorder, inorderMap: inorderMap, i: i + 1 + m - l, l: m + 1, r: r) + // Return the root node + return root + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + func buildTree(preorder: [Int], inorder: [Int]) -> TreeNode? { + // Initialize hash map, storing the mapping from inorder elements to indices + let inorderMap = inorder.enumerated().reduce(into: [:]) { $0[$1.element] = $1.offset } + return dfs(preorder: preorder, inorderMap: inorderMap, i: inorder.startIndex, l: inorder.startIndex, r: inorder.endIndex - 1) + } ``` === "JS" ```javascript title="build_tree.js" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + function dfs(preorder, inorderMap, i, l, r) { + // Terminate when the subtree interval is empty + if (r - l < 0) return null; + // Initialize the root node + const root = new TreeNode(preorder[i]); + // Query m to divide the left and right subtrees + const m = inorderMap.get(preorder[i]); + // Subproblem: build the left subtree + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // Subproblem: build the right subtree + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // Return the root node + return root; + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + function buildTree(preorder, inorder) { + // Initialize hash map, storing the mapping from inorder elements to indices + 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" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + function dfs( + preorder: number[], + inorderMap: Map, + i: number, + l: number, + r: number + ): TreeNode | null { + // Terminate when the subtree interval is empty + if (r - l < 0) return null; + // Initialize the root node + const root: TreeNode = new TreeNode(preorder[i]); + // Query m to divide the left and right subtrees + const m = inorderMap.get(preorder[i]); + // Subproblem: build the left subtree + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // Subproblem: build the right subtree + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // Return the root node + return root; + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + function buildTree(preorder: number[], inorder: number[]): TreeNode | null { + // Initialize hash map, storing the mapping from inorder elements to indices + 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" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + TreeNode? dfs( + List preorder, + Map inorderMap, + int i, + int l, + int r, + ) { + // Terminate when the subtree interval is empty + if (r - l < 0) { + return null; + } + // Initialize the root node + TreeNode? root = TreeNode(preorder[i]); + // Query m to divide the left and right subtrees + int m = inorderMap[preorder[i]]!; + // Subproblem: build the left subtree + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1); + // Subproblem: build the right subtree + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r); + // Return the root node + return root; + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + TreeNode? buildTree(List preorder, List inorder) { + // Initialize hash map, storing the mapping from inorder elements to indices + 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" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + fn dfs( + preorder: &[i32], + inorder_map: &HashMap, + i: i32, + l: i32, + r: i32, + ) -> Option>> { + // Terminate when the subtree interval is empty + if r - l < 0 { + return None; + } + // Initialize the root node + let root = TreeNode::new(preorder[i as usize]); + // Query m to divide the left and right subtrees + let m = inorder_map.get(&preorder[i as usize]).unwrap(); + // Subproblem: build the left subtree + root.borrow_mut().left = dfs(preorder, inorder_map, i + 1, l, m - 1); + // Subproblem: build the right subtree + root.borrow_mut().right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r); + // Return the root node + Some(root) + } - [class]{}-[func]{build_tree} + /* Build binary tree */ + fn build_tree(preorder: &[i32], inorder: &[i32]) -> Option>> { + // Initialize hash map, storing the mapping from inorder elements to indices + 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} + /* Build binary tree: divide and conquer */ + TreeNode *dfs(int *preorder, int *inorderMap, int i, int l, int r, int size) { + // Terminate when the subtree interval is empty + if (r - l < 0) + return NULL; + // Initialize the root node + TreeNode *root = (TreeNode *)malloc(sizeof(TreeNode)); + root->val = preorder[i]; + root->left = NULL; + root->right = NULL; + // Query m to divide the left and right subtrees + int m = inorderMap[preorder[i]]; + // Subproblem: build the left subtree + root->left = dfs(preorder, inorderMap, i + 1, l, m - 1, size); + // Subproblem: build the right subtree + root->right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r, size); + // Return the root node + return root; + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + TreeNode *buildTree(int *preorder, int preorderSize, int *inorder, int inorderSize) { + // Initialize hash map, storing the mapping from inorder elements to indices + int *inorderMap = (int *)malloc(sizeof(int) * MAX_SIZE); + for (int i = 0; i < inorderSize; i++) { + inorderMap[inorder[i]] = i; + } + TreeNode *root = dfs(preorder, inorderMap, 0, 0, inorderSize - 1, inorderSize); + free(inorderMap); + return root; + } ``` === "Kotlin" ```kotlin title="build_tree.kt" - [class]{}-[func]{dfs} + /* Build binary tree: divide and conquer */ + fun dfs( + preorder: IntArray, + inorderMap: Map, + i: Int, + l: Int, + r: Int + ): TreeNode? { + // Terminate when the subtree interval is empty + if (r - l < 0) return null + // Initialize the root node + val root = TreeNode(preorder[i]) + // Query m to divide the left and right subtrees + val m = inorderMap[preorder[i]]!! + // Subproblem: build the left subtree + root.left = dfs(preorder, inorderMap, i + 1, l, m - 1) + // Subproblem: build the right subtree + root.right = dfs(preorder, inorderMap, i + 1 + m - l, m + 1, r) + // Return the root node + return root + } - [class]{}-[func]{buildTree} + /* Build binary tree */ + fun buildTree(preorder: IntArray, inorder: IntArray): TreeNode? { + // Initialize hash map, storing the mapping from inorder elements to indices + val inorderMap = HashMap() + for (i in inorder.indices) { + inorderMap[inorder[i]] = i + } + val root = dfs(preorder, inorderMap, 0, 0, inorder.size - 1) + return root + } ``` === "Ruby" ```ruby title="build_tree.rb" - [class]{}-[func]{dfs} + ### Build binary tree: divide and conquer ### + def dfs(preorder, inorder_map, i, l, r) + # Terminate when the subtree interval is empty + return if r - l < 0 - [class]{}-[func]{build_tree} + # Initialize the root node + root = TreeNode.new(preorder[i]) + # Query m to divide the left and right subtrees + m = inorder_map[preorder[i]] + # Subproblem: build the left subtree + root.left = dfs(preorder, inorder_map, i + 1, l, m - 1) + # Subproblem: build the right subtree + root.right = dfs(preorder, inorder_map, i + 1 + m - l, m + 1, r) + + # Return the root node + root + end + + ### Build binary tree ### + def build_tree(preorder, inorder) + # Initialize hash map, storing the mapping from inorder elements to indices + inorder_map = {} + inorder.each_with_index { |val, i| inorder_map[val] = i } + dfs(preorder, inorder_map, 0, 0, inorder.length - 1) + end ``` -=== "Zig" - - ```zig title="build_tree.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - -Figure 12-8 shows the recursive process of building the binary tree. Each node is created during the "descending" phase of the recursion, and each edge (reference) is formed during the "ascending" phase. +Figure 12-8 shows the recursive process of building the binary tree. Each node is established during the downward "recursion" process, while each edge (reference) is established during the upward "return" process. === "<1>" ![Recursive process of building a binary tree](build_binary_tree_problem.assets/built_tree_step1.png){ class="animation-figure" } @@ -287,12 +543,12 @@ Figure 12-8 shows the recursive process of building the binary tree. Each node i

Figure 12-8   Recursive process of building a binary tree

-Each recursive function's division of the `preorder` and `inorder` sequences is illustrated in Figure 12-9. +The division results of the preorder traversal `preorder` and inorder traversal `inorder` within each recursive function are shown in Figure 12-9. -![Division in each recursive function](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" } +![Division results in each recursive function](build_binary_tree_problem.assets/built_tree_overall.png){ class="animation-figure" } -

Figure 12-9   Division in each recursive function

+

Figure 12-9   Division results in each recursive function

-Assuming the binary tree has $n$ nodes, initializing each node (calling the recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**. +Let the number of nodes in the tree be $n$. Initializing each node (executing one recursive function `dfs()`) takes $O(1)$ time. **Therefore, the overall time complexity is $O(n)$**. -Because the hash table stores the mapping from `inorder` elements to their indexes, it requires $O(n)$ space. In the worst case, if the binary tree degenerates into a linked list, the recursive depth can reach $n$, consuming $O(n)$ stack space. **Hence, the overall space complexity is $O(n)$**. +The hash table stores the mapping from `inorder` elements to their indices, with a space complexity of $O(n)$. In the worst case, when the binary tree degenerates into a linked list, the recursion depth reaches $n$, using $O(n)$ stack frame space. **Therefore, the overall space complexity is $O(n)$**. diff --git a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md index dcf01bb0b..c51d9b154 100644 --- a/en/docs/chapter_divide_and_conquer/divide_and_conquer.md +++ b/en/docs/chapter_divide_and_conquer/divide_and_conquer.md @@ -2,55 +2,55 @@ comments: true --- -# 12.1   Divide and conquer algorithms +# 12.1   Divide and Conquer Algorithms -Divide and conquer is an important and popular algorithm strategy. As the name suggests, the algorithm is typically implemented recursively and consists of two steps: "divide" and "conquer". +Divide and conquer is a very important and common algorithm strategy. Divide and conquer is typically implemented based on recursion, consisting of two steps: "divide" and "conquer". -1. **Divide (partition phase)**: Recursively break down the original problem into two or more smaller sub-problems until the smallest sub-problem is reached. -2. **Conquer (merge phase)**: Starting from the smallest sub-problem with known solution, we construct the solution to the original problem by merging the solutions of sub-problems in a bottom-up manner. +1. **Divide (partition phase)**: Recursively divide the original problem into two or more subproblems until the smallest subproblem is reached. +2. **Conquer (merge phase)**: Starting from the smallest subproblems with known solutions, merge the solutions of subproblems from bottom to top to construct the solution to the original problem. As shown in Figure 12-1, "merge sort" is one of the typical applications of the divide and conquer strategy. -1. **Divide**: Recursively divide the original array (original problem) into two sub-arrays (sub-problems), until the sub-array has only one element (smallest sub-problem). -2. **Conquer**: Merge the ordered sub-arrays (solutions to the sub-problems) from bottom to top to obtain an ordered original array (solution to the original problem). +1. **Divide**: Recursively divide the original array (original problem) into two subarrays (subproblems) until the subarray has only one element (smallest subproblem). +2. **Conquer**: Merge the sorted subarrays (solutions to subproblems) from bottom to top to obtain a sorted original array (solution to the original problem). -![Merge sort's divide and conquer strategy](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" } +![Divide and conquer strategy of merge sort](divide_and_conquer.assets/divide_and_conquer_merge_sort.png){ class="animation-figure" } -

Figure 12-1   Merge sort's divide and conquer strategy

+

Figure 12-1   Divide and conquer strategy of merge sort

-## 12.1.1   How to identify divide and conquer problems +## 12.1.1   How to Determine Divide and Conquer Problems -Whether a problem is suitable for a divide-and-conquer solution can usually be decided based on the following criteria. +Whether a problem is suitable for solving with divide and conquer can usually be determined based on the following criteria. -1. **The problem can be broken down into smaller ones**: The original problem can be divided into smaller, similar sub-problems and such process can be recursively done in the same manner. -2. **Sub-problems are independent**: There is no overlap between sub-problems, and they are independent and can be solved separately. -3. **Solutions to sub-problems can be merged**: The solution to the original problem is derived by combining the solutions of the sub-problems. +1. **The problem can be decomposed**: The original problem can be divided into smaller, similar subproblems, and can be recursively divided in the same way. +2. **Subproblems are independent**: There is no overlap between subproblems, they are independent of each other and can be solved independently. +3. **Solutions of subproblems can be merged**: The solution to the original problem is obtained by merging the solutions of subproblems. -Clearly, merge sort meets these three criteria. +Clearly, merge sort satisfies these three criteria. -1. **The problem can be broken down into smaller ones**: Recursively divide the array (original problem) into two sub-arrays (sub-problems). -2. **Sub-problems are independent**: Each sub-array can be sorted independently (sub-problems can be solved independently). -3. **Solutions to sub-problems can be merged**: Two ordered sub-arrays (solutions to the sub-problems) can be merged into one ordered array (solution to the original problem). +1. **The problem can be decomposed**: Recursively divide the array (original problem) into two subarrays (subproblems). +2. **Subproblems are independent**: Each subarray can be sorted independently (subproblems can be solved independently). +3. **Solutions of subproblems can be merged**: Two sorted subarrays (solutions of subproblems) can be merged into one sorted array (solution of the original problem). -## 12.1.2   Improve efficiency through divide and conquer +## 12.1.2   Improving Efficiency Through Divide and Conquer -The **divide-and-conquer strategy not only effectively solves algorithm problems but also often enhances efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection sort, bubble sort, and insertion sort because they apply the divide-and-conquer strategy. +**Divide and conquer can not only effectively solve algorithmic problems but often also improve algorithm efficiency**. In sorting algorithms, quick sort, merge sort, and heap sort are faster than selection, bubble, and insertion sort because they apply the divide and conquer strategy. -We may have a question in mind: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic?** In other words, why is breaking a problem into sub-problems, solving them, and combining their solutions to address the original problem offer more efficiency than directly solving the original problem? This question can be analyzed from two aspects: operation count and parallel computation. +This raises the question: **Why can divide and conquer improve algorithm efficiency, and what is the underlying logic**? In other words, why is dividing a large problem into multiple subproblems, solving the subproblems, and merging their solutions more efficient than directly solving the original problem? This question can be discussed from two aspects: operation count and parallel computation. -### 1.   Optimization of operation count +### 1.   Operation Count Optimization -Taking "bubble sort" as an example, it requires $O(n^2)$ time to process an array of length $n$. Suppose we divide the array from the midpoint into two sub-arrays as shown in Figure 12-2, such division requires $O(n)$ time. Sorting each sub-array requires $O((n / 2)^2)$ time. And merging the two sub-arrays requires $O(n)$ time. Thus, the overall time complexity is: +Taking "bubble sort" as an example, processing an array of length $n$ requires $O(n^2)$ time. Suppose we divide the array into two subarrays from the midpoint as shown in Figure 12-2, the division requires $O(n)$ time, sorting each subarray requires $O((n / 2)^2)$ time, and merging the two subarrays requires $O(n)$ time, resulting in an overall time complexity of: $$ O(n + (\frac{n}{2})^2 \times 2 + n) = O(\frac{n^2}{2} + 2n) $$ -![Bubble sort before and after array partition](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" } +![Bubble sort before and after array division](divide_and_conquer.assets/divide_and_conquer_bubble_sort.png){ class="animation-figure" } -

Figure 12-2   Bubble sort before and after array partition

+

Figure 12-2   Bubble sort before and after array division

-Let's calculate the following inequality, where the left side represents the total number of operations before division and the right side represents the total number of operations after division, respectively: +Next, we compute the following inequality, where the left and right sides represent the total number of operations before and after division, respectively: $$ \begin{aligned} @@ -60,42 +60,42 @@ n(n - 4) & > 0 \end{aligned} $$ -**This means that when $n > 4$, the number of operations after partitioning is fewer, leading to better performance**. Please note that the time complexity after partitioning is still quadratic $O(n^2)$, but the constant factor in the complexity has decreased. +**This means that when $n > 4$, the number of operations after division is smaller, and sorting efficiency should be higher**. Note that the time complexity after division is still quadratic $O(n^2)$, but the constant term in the complexity has become smaller. -We can go even further. **How about keeping dividing the sub-arrays from their midpoints into two sub-arrays** until the sub-arrays have only one element left? This idea is actually "merge sort," with a time complexity of $O(n \log n)$. +Going further, **what if we continuously divide the subarrays from their midpoints into two subarrays** until the subarrays have only one element? This approach is actually "merge sort", with a time complexity of $O(n \log n)$. -Let's try something a bit different again. **How about splitting into more partitions instead of just two?** For example, we evenly divide the original array into $k$ sub-arrays? This approach is very similar to "bucket sort," which is very suitable for sorting massive data. Theoretically, the time complexity can reach $O(n + k)$. +Thinking further, **what if we set multiple division points** and evenly divide the original array into $k$ subarrays? This situation is very similar to "bucket sort", which is well-suited for sorting massive amounts of data, with a theoretical time complexity of $O(n + k)$. -### 2.   Optimization through parallel computation +### 2.   Parallel Computation Optimization -We know that the sub-problems generated by divide and conquer are independent of each other, **which means that they can be solved in parallel.** As a result, divide and conquer not only reduces the algorithm's time complexity, **but also facilitates parallel optimization by modern operating systems.** +We know that the subproblems generated by divide and conquer are independent of each other, **so they can typically be solved in parallel**. This means divide and conquer can not only reduce the time complexity of algorithms, **but also benefits from parallel optimization by operating systems**. -Parallel optimization is particularly effective in environments with multiple cores or processors. As the system can process multiple sub-problems simultaneously, fully utilizing computing resources, the overall runtime is significantly reduced. +Parallel optimization is particularly effective in multi-core or multi-processor environments, as the system can simultaneously handle multiple subproblems, making fuller use of computing resources and significantly reducing overall runtime. -For example, in the "bucket sort" shown in Figure 12-3, we break massive data evenly into various buckets. The jobs of sorting each bucket can be allocated to available computing units. Once all jobs are done, all sorted buckets are merged to produce the final result. +For example, in the "bucket sort" shown in Figure 12-3, we evenly distribute massive data into various buckets, and the sorting tasks for all buckets can be distributed to various computing units. After completion, the results are merged. -![Bucket sort's parallel computation](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" } +![Parallel computation in bucket sort](divide_and_conquer.assets/divide_and_conquer_parallel_computing.png){ class="animation-figure" } -

Figure 12-3   Bucket sort's parallel computation

+

Figure 12-3   Parallel computation in bucket sort

-## 12.1.3   Common applications of divide and conquer +## 12.1.3   Common Applications of Divide and Conquer -Divide and conquer can be used to solve many classic algorithm problems. +On one hand, divide and conquer can be used to solve many classic algorithmic problems. -- **Finding the closest pair of points**: This algorithm works by dividing the set of points into two halves. Then it recursively finds the closest pair in each half. Finally it considers pairs that span the two halves to find the overall closest pair. -- **Large integer multiplication**: One algorithm is called Karatsuba. It breaks down large integer multiplication into several smaller integer multiplications and additions. -- **Matrix multiplication**: One example is the Strassen algorithm. It breaks down a large matrix multiplication into multiple small matrix multiplications and additions. -- **Tower of Hanoi problem**: The Tower of Hanoi problem can be solved recursively, a typical application of the divide-and-conquer strategy. -- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, then these two numbers constitute an inversion pair. Solving inversion pair problem can utilize the idea of divide and conquer, with the aid of merge sort. +- **Finding the closest pair of points**: This algorithm first divides the point set into two parts, then finds the closest pair of points in each part separately, and finally finds the closest pair of points that spans both parts. +- **Large integer multiplication**: For example, the Karatsuba algorithm, which decomposes large integer multiplication into several smaller integer multiplications and additions. +- **Matrix multiplication**: For example, the Strassen algorithm, which decomposes large matrix multiplication into multiple small matrix multiplications and additions. +- **Hanota problem**: The hanota problem can be solved through recursion, which is a typical application of the divide and conquer strategy. +- **Solving inversion pairs**: In a sequence, if a preceding number is greater than a following number, these two numbers form an inversion pair. Solving the inversion pair problem can utilize the divide and conquer approach with the help of merge sort. -Divide and conquer is also widely applied in the design of algorithms and data structures. +On the other hand, divide and conquer is widely applied in the design of algorithms and data structures. -- **Binary search**: Binary search divides a sorted array into two halves from the midpoint index. And then based on the comparison result between the target value and the middle element value, one half is discarded. The search continues on the remaining half with the same process until the target is found or there is no remaining element. -- **Merge sort**: Already introduced at the beginning of this section, no further elaboration is needed. -- **Quicksort**: Quicksort picks a pivot value to divide the array into two sub-arrays, one with elements smaller than the pivot and the other with elements larger than the pivot. Such process goes on against each of these two sub-arrays until they hold only one element. -- **Bucket sort**: The basic idea of bucket sort is to distribute data to multiple buckets. After sorting the elements within each bucket, retrieve the elements from the buckets in order to obtain an ordered array. -- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, and B+ trees, etc. Their operations, such as search, insertion, and deletion, can all be regarded as applications of the divide-and-conquer strategy. -- **Heap**: A heap is a special type of complete binary tree. Its various operations, such as insertion, deletion, and heapify, actually imply the idea of divide and conquer. -- **Hash table**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the strategy. For example, long lists in chained addressing may be converted to red-black trees to improve query efficiency. +- **Binary search**: Binary search divides a sorted array into two parts from the midpoint index, then decides which half to eliminate based on the comparison result between the target value and the middle element value, and performs the same binary operation on the remaining interval. +- **Merge sort**: Already introduced at the beginning of this section, no further elaboration needed. +- **Quick sort**: Quick sort selects a pivot value, then divides the array into two subarrays, one with elements smaller than the pivot and the other with elements larger than the pivot, then performs the same division operation on these two parts until the subarrays have only one element. +- **Bucket sort**: The basic idea of bucket sort is to scatter data into multiple buckets, then sort the elements within each bucket, and finally extract the elements from each bucket in sequence to obtain a sorted array. +- **Trees**: For example, binary search trees, AVL trees, red-black trees, B-trees, B+ trees, etc. Their search, insertion, and deletion operations can all be viewed as applications of the divide and conquer strategy. +- **Heaps**: A heap is a special complete binary tree, and its various operations, such as insertion, deletion, and heapify, actually imply the divide and conquer idea. +- **Hash tables**: Although hash tables do not directly apply divide and conquer, some hash collision resolution solutions indirectly apply the divide and conquer strategy. For example, long linked lists in chaining may be converted to red-black trees to improve query efficiency. -It can be seen that **divide and conquer is a subtly pervasive algorithmic idea**, embedded within various algorithms and data structures. +It can be seen that **divide and conquer is a "subtly pervasive" algorithmic idea**, embedded in various algorithms and data structures. diff --git a/en/docs/chapter_divide_and_conquer/hanota_problem.md b/en/docs/chapter_divide_and_conquer/hanota_problem.md index f4d7434bb..70818ef0f 100644 --- a/en/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/en/docs/chapter_divide_and_conquer/hanota_problem.md @@ -2,27 +2,27 @@ comments: true --- -# 12.4   Tower of Hanoi Problem +# 12.4   Hanota Problem -In both merge sort and binary tree construction, we break the original problem into two subproblems, each half the size of the original problem. However, for the Tower of Hanoi, we adopt a different decomposition strategy. +In merge sort and building binary trees, we decompose the original problem into two subproblems, each half the size of the original problem. However, for the hanota problem, we adopt a different decomposition strategy. !!! question - We are given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs, arranged from top to bottom in ascending size. Our task is to move these $n$ discs to pillar `C`, maintaining their original order (as shown in Figure 12-10). The following rules apply during the movement: - - 1. A disc can be removed only from the top of a pillar and must be placed on the top of another pillar. + Given three pillars, denoted as `A`, `B`, and `C`. Initially, pillar `A` has $n$ discs stacked on it, arranged from top to bottom in ascending order of size. Our task is to move these $n$ discs to pillar `C` while maintaining their original order (as shown in Figure 12-10). The following rules must be followed when moving the discs. + + 1. A disc can only be taken from the top of one pillar and placed on top of another pillar. 2. Only one disc can be moved at a time. 3. A smaller disc must always be on top of a larger disc. -![Example of the Tower of Hanoi](hanota_problem.assets/hanota_example.png){ class="animation-figure" } +![Example of the hanota problem](hanota_problem.assets/hanota_example.png){ class="animation-figure" } -

Figure 12-10   Example of the Tower of Hanoi

+

Figure 12-10   Example of the hanota problem

-**We denote the Tower of Hanoi problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from pillar `A` to pillar `C`. +**We denote the hanota problem of size $i$ as $f(i)$**. For example, $f(3)$ represents moving $3$ discs from `A` to `C`. -### 1.   Consider the base cases +### 1.   Considering the Base Cases -As shown in Figure 12-11, for the problem $f(1)$—which has only one disc—we can directly move it from `A` to `C`. +As shown in Figure 12-11, for problem $f(1)$, when there is only one disc, we can move it directly from `A` to `C`. === "<1>" ![Solution for a problem of size 1](hanota_problem.assets/hanota_f1_step1.png){ class="animation-figure" } @@ -32,7 +32,7 @@ As shown in Figure 12-11, for the problem $f(1)$—which has only one disc—we

Figure 12-11   Solution for a problem of size 1

-For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smaller disc above the larger disc**, as illustrated in the following figure: +As shown in Figure 12-12, for problem $f(2)$, when there are two discs, **since we must always keep the smaller disc on top of the larger disc, we need to use `B` to assist in the move**. 1. First, move the smaller disc from `A` to `B`. 2. Then move the larger disc from `A` to `C`. @@ -52,17 +52,17 @@ For $f(2)$—which has two discs—**we rely on pillar `B` to help keep the smal

Figure 12-12   Solution for a problem of size 2

-The process of solving $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar. +The process of solving problem $f(2)$ can be summarized as: **moving two discs from `A` to `C` with the help of `B`**. Here, `C` is called the target pillar, and `B` is called the buffer pillar. -### 2.   Decomposition of subproblems +### 2.   Subproblem Decomposition -For the problem $f(3)$—that is, when there are three discs—the situation becomes slightly more complicated. +For problem $f(3)$, when there are three discs, the situation becomes slightly more complex. -Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-and-conquer perspective and **treat the top two discs on `A` as a single unit**, performing the steps shown in Figure 12-13. This allows the three discs to be successfully moved from `A` to `C`. +Since we already know the solutions to $f(1)$ and $f(2)$, we can think from a divide and conquer perspective, **treating the top two discs on `A` as a whole**, and execute the steps shown in Figure 12-13. This successfully moves the three discs from `A` to `C`. -1. Let `B` be the target pillar and `C` the buffer pillar, then move the two discs from `A` to `B`. +1. Let `B` be the target pillar and `C` be the buffer pillar, and move two discs from `A` to `B`. 2. Move the remaining disc from `A` directly to `C`. -3. Let `C` be the target pillar and `A` the buffer pillar, then move the two discs from `B` to `C`. +3. Let `C` be the target pillar and `A` be the buffer pillar, and move two discs from `B` to `C`. === "<1>" ![Solution for a problem of size 3](hanota_problem.assets/hanota_f3_step1.png){ class="animation-figure" } @@ -78,85 +78,85 @@ Since we already know the solutions to $f(1)$ and $f(2)$, we can adopt a divide-

Figure 12-13   Solution for a problem of size 3

-Essentially, **we decompose $f(3)$ into two $f(2)$ subproblems and one $f(1)$ subproblem**. By solving these three subproblems in sequence, the original problem is solved, indicating that the subproblems are independent and their solutions can be merged. +Essentially, **we divide problem $f(3)$ into two subproblems $f(2)$ and one subproblem $f(1)$**. By solving these three subproblems in order, the original problem is solved. This shows that the subproblems are independent and their solutions can be merged. -From this, we can summarize the divide-and-conquer strategy for the Tower of Hanoi, illustrated in Figure 12-14. We divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order: +From this, we can summarize the divide and conquer strategy for solving the hanota problem shown in Figure 12-14: divide the original problem $f(n)$ into two subproblems $f(n-1)$ and one subproblem $f(1)$, and solve these three subproblems in the following order. -1. Move $n-1$ discs from `A` to `B`, using `C` as a buffer. -2. Move the remaining disc directly from `A` to `C`. -3. Move $n-1$ discs from `B` to `C`, using `A` as a buffer. +1. Move $n-1$ discs from `A` to `B` with the help of `C`. +2. Move the remaining $1$ disc directly from `A` to `C`. +3. Move $n-1$ discs from `B` to `C` with the help of `A`. -For each $f(n-1)$ subproblem, **we can apply the same recursive partition** until we reach the smallest subproblem $f(1)$. Because $f(1)$ is already known to require just a single move, it is trivial to solve. +For these two subproblems $f(n-1)$, **we can recursively divide them in the same way** until reaching the smallest subproblem $f(1)$. The solution to $f(1)$ is known and requires only one move operation. -![Divide-and-conquer strategy for solving the Tower of Hanoi](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } +![Divide and conquer strategy for solving the hanota problem](hanota_problem.assets/hanota_divide_and_conquer.png){ class="animation-figure" } -

Figure 12-14   Divide-and-conquer strategy for solving the Tower of Hanoi

+

Figure 12-14   Divide and conquer strategy for solving the hanota problem

-### 3.   Code implementation +### 3.   Code Implementation -In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves the top $i$ discs from pillar `src` to pillar `tar`, using pillar `buf` as a buffer: +In the code, we declare a recursive function `dfs(i, src, buf, tar)`, whose purpose is to move the top $i$ discs from pillar `src` to target pillar `tar` with the help of buffer pillar `buf`: === "Python" ```python title="hanota.py" def move(src: list[int], tar: list[int]): - """Move a disc""" - # Take out a disc from the top of src + """Move a disk""" + # Take out a disk from the top of src pan = src.pop() - # Place the disc on top of tar + # Place the disk on top of tar tar.append(pan) def dfs(i: int, src: list[int], buf: list[int], tar: list[int]): """Solve the Tower of Hanoi problem f(i)""" - # If only one disc remains on src, move it to tar + # If there is only one disk left in src, move it directly to tar if i == 1: move(src, tar) return - # Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf + # Subproblem f(i-1): move the top i-1 disks from src to buf using tar dfs(i - 1, src, tar, buf) - # Subproblem f(1): move the remaining one disc from src to tar + # Subproblem f(1): move the remaining disk from src to tar move(src, tar) - # Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar + # Subproblem f(i-1): move the top i-1 disks from buf to tar using src dfs(i - 1, buf, src, tar) def solve_hanota(A: list[int], B: list[int], C: list[int]): """Solve the Tower of Hanoi problem""" n = len(A) - # Move the top n discs from A with the help of B to C + # Move the top n disks from A to C using B dfs(n, A, B, C) ``` === "C++" ```cpp title="hanota.cpp" - /* Move a disc */ + /* Move a disk */ void move(vector &src, vector &tar) { - // Take out a disc from the top of src + // Take out a disk from the top of src int pan = src.back(); src.pop_back(); - // Place the disc on top of tar + // Place the disk on top of tar tar.push_back(pan); } /* Solve the Tower of Hanoi problem f(i) */ void dfs(int i, vector &src, vector &buf, vector &tar) { - // If only one disc remains on src, move it to tar + // If there is only one disk left in src, move it directly to tar if (i == 1) { move(src, tar); return; } - // Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar dfs(i - 1, src, tar, buf); - // Subproblem f(1): move the remaining one disc from src to tar + // Subproblem f(1): move the remaining disk from src to tar move(src, tar); - // Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src dfs(i - 1, buf, src, tar); } /* Solve the Tower of Hanoi problem */ void solveHanota(vector &A, vector &B, vector &C) { int n = A.size(); - // Move the top n discs from A with the help of B to C + // Move the top n disks from A to C using B dfs(n, A, B, C); } ``` @@ -164,33 +164,33 @@ In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves === "Java" ```java title="hanota.java" - /* Move a disc */ + /* Move a disk */ void move(List src, List tar) { - // Take out a disc from the top of src + // Take out a disk from the top of src Integer pan = src.remove(src.size() - 1); - // Place the disc on top of tar + // Place the disk on top of tar tar.add(pan); } /* Solve the Tower of Hanoi problem f(i) */ void dfs(int i, List src, List buf, List tar) { - // If only one disc remains on src, move it to tar + // If there is only one disk left in src, move it directly to tar if (i == 1) { move(src, tar); return; } - // Subproblem f(i-1): move the top i-1 discs from src with the help of tar to buf + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar dfs(i - 1, src, tar, buf); - // Subproblem f(1): move the remaining one disc from src to tar + // Subproblem f(1): move the remaining disk from src to tar move(src, tar); - // Subproblem f(i-1): move the top i-1 discs from buf with the help of src to tar + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src dfs(i - 1, buf, src, tar); } /* Solve the Tower of Hanoi problem */ void solveHanota(List A, List B, List C) { int n = A.size(); - // Move the top n discs from A with the help of B to C + // Move the top n disks from A to C using B dfs(n, A, B, C); } ``` @@ -198,121 +198,358 @@ In the code, we define a recursive function `dfs(i, src, buf, tar)` which moves === "C#" ```csharp title="hanota.cs" - [class]{hanota}-[func]{Move} + /* Move a disk */ + void Move(List src, List tar) { + // Take out a disk from the top of src + int pan = src[^1]; + src.RemoveAt(src.Count - 1); + // Place the disk on top of tar + tar.Add(pan); + } - [class]{hanota}-[func]{DFS} + /* Solve the Tower of Hanoi problem f(i) */ + void DFS(int i, List src, List buf, List tar) { + // If there is only one disk left in src, move it directly to tar + if (i == 1) { + Move(src, tar); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + DFS(i - 1, src, tar, buf); + // Subproblem f(1): move the remaining disk from src to tar + Move(src, tar); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + DFS(i - 1, buf, src, tar); + } - [class]{hanota}-[func]{SolveHanota} + /* Solve the Tower of Hanoi problem */ + void SolveHanota(List A, List B, List C) { + int n = A.Count; + // Move the top n disks from A to C using B + DFS(n, A, B, C); + } ``` === "Go" ```go title="hanota.go" - [class]{}-[func]{move} + /* Move a disk */ + func move(src, tar *list.List) { + // Take out a disk from the top of src + pan := src.Back() + // Place the disk on top of tar + tar.PushBack(pan.Value) + // Remove top disk from src + src.Remove(pan) + } - [class]{}-[func]{dfsHanota} + /* Solve the Tower of Hanoi problem f(i) */ + func dfsHanota(i int, src, buf, tar *list.List) { + // If there is only one disk left in src, move it directly to tar + if i == 1 { + move(src, tar) + return + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfsHanota(i-1, src, tar, buf) + // Subproblem f(1): move the remaining disk from src to tar + move(src, tar) + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfsHanota(i-1, buf, src, tar) + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + func solveHanota(A, B, C *list.List) { + n := A.Len() + // Move the top n disks from A to C using B + dfsHanota(n, A, B, C) + } ``` === "Swift" ```swift title="hanota.swift" - [class]{}-[func]{move} + /* Move a disk */ + func move(src: inout [Int], tar: inout [Int]) { + // Take out a disk from the top of src + let pan = src.popLast()! + // Place the disk on top of tar + tar.append(pan) + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + func dfs(i: Int, src: inout [Int], buf: inout [Int], tar: inout [Int]) { + // If there is only one disk left in src, move it directly to tar + if i == 1 { + move(src: &src, tar: &tar) + return + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i: i - 1, src: &src, buf: &tar, tar: &buf) + // Subproblem f(1): move the remaining disk from src to tar + move(src: &src, tar: &tar) + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i: i - 1, src: &buf, buf: &src, tar: &tar) + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + func solveHanota(A: inout [Int], B: inout [Int], C: inout [Int]) { + let n = A.count + // The tail of the list is the top of the rod + // Move top n disks from src to C using B + dfs(i: n, src: &A, buf: &B, tar: &C) + } ``` === "JS" ```javascript title="hanota.js" - [class]{}-[func]{move} + /* Move a disk */ + function move(src, tar) { + // Take out a disk from the top of src + const pan = src.pop(); + // Place the disk on top of tar + tar.push(pan); + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + function dfs(i, src, buf, tar) { + // If there is only one disk left in src, move it directly to tar + if (i === 1) { + move(src, tar); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf); + // Subproblem f(1): move the remaining disk from src to tar + move(src, tar); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + function solveHanota(A, B, C) { + const n = A.length; + // Move the top n disks from A to C using B + dfs(n, A, B, C); + } ``` === "TS" ```typescript title="hanota.ts" - [class]{}-[func]{move} + /* Move a disk */ + function move(src: number[], tar: number[]): void { + // Take out a disk from the top of src + const pan = src.pop(); + // Place the disk on top of tar + tar.push(pan); + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + function dfs(i: number, src: number[], buf: number[], tar: number[]): void { + // If there is only one disk left in src, move it directly to tar + if (i === 1) { + move(src, tar); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf); + // Subproblem f(1): move the remaining disk from src to tar + move(src, tar); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + function solveHanota(A: number[], B: number[], C: number[]): void { + const n = A.length; + // Move the top n disks from A to C using B + dfs(n, A, B, C); + } ``` === "Dart" ```dart title="hanota.dart" - [class]{}-[func]{move} + /* Move a disk */ + void move(List src, List tar) { + // Take out a disk from the top of src + int pan = src.removeLast(); + // Place the disk on top of tar + tar.add(pan); + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + void dfs(int i, List src, List buf, List tar) { + // If there is only one disk left in src, move it directly to tar + if (i == 1) { + move(src, tar); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf); + // Subproblem f(1): move the remaining disk from src to tar + move(src, tar); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + void solveHanota(List A, List B, List C) { + int n = A.length; + // Move the top n disks from A to C using B + dfs(n, A, B, C); + } ``` === "Rust" ```rust title="hanota.rs" - [class]{}-[func]{move_pan} + /* Move a disk */ + fn move_pan(src: &mut Vec, tar: &mut Vec) { + // Take out a disk from the top of src + let pan = src.pop().unwrap(); + // Place the disk on top of tar + tar.push(pan); + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + fn dfs(i: i32, src: &mut Vec, buf: &mut Vec, tar: &mut Vec) { + // If there is only one disk left in src, move it directly to tar + if i == 1 { + move_pan(src, tar); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf); + // Subproblem f(1): move the remaining disk from src to tar + move_pan(src, tar); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar); + } - [class]{}-[func]{solve_hanota} + /* Solve the Tower of Hanoi problem */ + fn solve_hanota(A: &mut Vec, B: &mut Vec, C: &mut Vec) { + let n = A.len() as i32; + // Move the top n disks from A to C using B + dfs(n, A, B, C); + } ``` === "C" ```c title="hanota.c" - [class]{}-[func]{move} + /* Move a disk */ + void move(int *src, int *srcSize, int *tar, int *tarSize) { + // Take out a disk from the top of src + int pan = src[*srcSize - 1]; + src[*srcSize - 1] = 0; + (*srcSize)--; + // Place the disk on top of tar + tar[*tarSize] = pan; + (*tarSize)++; + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + void dfs(int i, int *src, int *srcSize, int *buf, int *bufSize, int *tar, int *tarSize) { + // If there is only one disk left in src, move it directly to tar + if (i == 1) { + move(src, srcSize, tar, tarSize); + return; + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, srcSize, tar, tarSize, buf, bufSize); + // Subproblem f(1): move the remaining disk from src to tar + move(src, srcSize, tar, tarSize); + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, bufSize, src, srcSize, tar, tarSize); + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + void solveHanota(int *A, int *ASize, int *B, int *BSize, int *C, int *CSize) { + // Move the top n disks from A to C using B + dfs(*ASize, A, ASize, B, BSize, C, CSize); + } ``` === "Kotlin" ```kotlin title="hanota.kt" - [class]{}-[func]{move} + /* Move a disk */ + fun move(src: MutableList, tar: MutableList) { + // Take out a disk from the top of src + val pan = src.removeAt(src.size - 1) + // Place the disk on top of tar + tar.add(pan) + } - [class]{}-[func]{dfs} + /* Solve the Tower of Hanoi problem f(i) */ + fun dfs(i: Int, src: MutableList, buf: MutableList, tar: MutableList) { + // If there is only one disk left in src, move it directly to tar + if (i == 1) { + move(src, tar) + return + } + // Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf) + // Subproblem f(1): move the remaining disk from src to tar + move(src, tar) + // Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar) + } - [class]{}-[func]{solveHanota} + /* Solve the Tower of Hanoi problem */ + fun solveHanota(A: MutableList, B: MutableList, C: MutableList) { + val n = A.size + // Move the top n disks from A to C using B + dfs(n, A, B, C) + } ``` === "Ruby" ```ruby title="hanota.rb" - [class]{}-[func]{move} + ### Move one disk ### + def move(src, tar) + # Take out a disk from the top of src + pan = src.pop + # Place the disk on top of tar + tar << pan + end - [class]{}-[func]{dfs} + ### Solve Tower of Hanoi f(i) ### + def dfs(i, src, buf, tar) + # If there is only one disk left in src, move it directly to tar + if i == 1 + move(src, tar) + return + end - [class]{}-[func]{solve_hanota} + # Subproblem f(i-1): move the top i-1 disks from src to buf using tar + dfs(i - 1, src, tar, buf) + # Subproblem f(1): move the remaining disk from src to tar + move(src, tar) + # Subproblem f(i-1): move the top i-1 disks from buf to tar using src + dfs(i - 1, buf, src, tar) + end + + ### Solve Tower of Hanoi ### + def solve_hanota(_A, _B, _C) + n = _A.length + # Move the top n disks from A to C using B + dfs(n, _A, _B, _C) + end ``` -=== "Zig" +As shown in Figure 12-15, the hanota problem forms a recursion tree of height $n$, where each node represents a subproblem corresponding to an invocation of the `dfs()` function, **therefore the time complexity is $O(2^n)$ and the space complexity is $O(n)$**. - ```zig title="hanota.zig" - [class]{}-[func]{move} +![Recursion tree of the hanota problem](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - -As shown in Figure 12-15, the Tower of Hanoi problem can be visualized as a recursive tree of height $n$. Each node represents a subproblem, corresponding to a call to `dfs()`, **Hence, the time complexity is $O(2^n)$, and the space complexity is $O(n)$.** - -![Recursive tree of the Tower of Hanoi](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } - -

Figure 12-15   Recursive tree of the Tower of Hanoi

+

Figure 12-15   Recursion tree of the hanota problem

!!! quote - The Tower of Hanoi originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ differently sized golden discs. They believed that when the last disc was correctly placed, the world would end. + The hanota problem originates from an ancient legend. In a temple in ancient India, monks had three tall diamond pillars and $64$ golden discs of different sizes. The monks continuously moved the discs, believing that when the last disc was correctly placed, the world would come to an end. - However, even if the monks moved one disc every second, it would take about $2^{64} \approx 1.84×10^{19}$ —approximately 585 billion years—far exceeding current estimates of the age of the universe. Thus, if the legend is true, we probably do not need to worry about the world ending. + However, even if the monks moved one disc per second, it would take approximately $2^{64} \approx 1.84×10^{19}$ seconds, which is about $5850$ billion years, far exceeding current estimates of the age of the universe. Therefore, if this legend is true, we should not need to worry about the end of the world. diff --git a/en/docs/chapter_divide_and_conquer/index.md b/en/docs/chapter_divide_and_conquer/index.md index f1e97f74b..c00a8daa1 100644 --- a/en/docs/chapter_divide_and_conquer/index.md +++ b/en/docs/chapter_divide_and_conquer/index.md @@ -3,20 +3,20 @@ comments: true icon: material/set-split --- -# Chapter 12.   Divide and conquer +# Chapter 12.   Divide and Conquer -![Divide and Conquer](../assets/covers/chapter_divide_and_conquer.jpg){ class="cover-image" } +![Divide and conquer](../assets/covers/chapter_divide_and_conquer.jpg){ class="cover-image" } !!! abstract Difficult problems are decomposed layer by layer, with each decomposition making them simpler. - Divide and conquer unveils a profound truth: begin with simplicity, and complexity dissolves. + Divide and conquer reveals an important truth: start with simplicity, and nothing remains complex. ## Chapter contents -- [12.1   Divide and conquer algorithms](divide_and_conquer.md) -- [12.2   Divide and conquer search strategy](binary_search_recur.md) -- [12.3   Building binary tree problem](build_binary_tree_problem.md) -- [12.4   Tower of Hanoi Problem](hanota_problem.md) +- [12.1   Divide and Conquer Algorithms](divide_and_conquer.md) +- [12.2   Divide and Conquer Search Strategy](binary_search_recur.md) +- [12.3   Building a Binary Tree Problem](build_binary_tree_problem.md) +- [12.4   Hanoi Tower Problem](hanota_problem.md) - [12.5   Summary](summary.md) diff --git a/en/docs/chapter_divide_and_conquer/summary.md b/en/docs/chapter_divide_and_conquer/summary.md index 5fb7b9d7a..9f562aaea 100644 --- a/en/docs/chapter_divide_and_conquer/summary.md +++ b/en/docs/chapter_divide_and_conquer/summary.md @@ -4,12 +4,14 @@ comments: true # 12.5   Summary -- Divide and conquer is a common algorithm design strategy that consists of two stages—divide (partition) and conquer (merge)—and is generally implemented using recursion. -- To determine whether a problem is suited for a divide and conquer approach, we check if the problem can be decomposed, whether the subproblems are independent, and whether the subproblems can be merged. -- Merge sort is a typical example of the divide and conquer strategy. It recursively splits an array into two equal-length subarrays until only one element remains, and then merges these subarrays layer by layer to complete the sorting. -- Introducing the divide and conquer strategy often improves algorithm efficiency. On one hand, it reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division. -- Divide and conquer can be applied to numerous algorithmic problems and is widely used in data structures and algorithm design, appearing in many scenarios. -- Compared to brute force search, adaptive search is more efficient. Search algorithms with a time complexity of $O(\log n)$ are typically based on the divide and conquer strategy. -- Binary search is another classic application of the divide-and-conquer strategy. It does not involve merging subproblem solutions and can be implemented via a recursive divide-and-conquer approach. -- In the problem of constructing binary trees, building the tree (the original problem) can be divided into building the left subtree and right subtree (the subproblems). This can be achieved by partitioning the index ranges of the preorder and inorder traversals. -- In the Tower of Hanoi problem, a problem of size $n$ can be broken down into two subproblems of size $n-1$ and one subproblem of size $1$. By solving these three subproblems in sequence, the original problem is resolved. +### 1.   Key Review + +- Divide and conquer is a common algorithm design strategy, consisting of two phases: divide (partition) and conquer (merge), typically implemented based on recursion. +- The criteria for determining whether a problem is a divide and conquer problem include: whether the problem can be decomposed, whether subproblems are independent, and whether subproblems can be merged. +- Merge sort is a typical application of the divide and conquer strategy. It recursively divides an array into two equal-length subarrays until only one element remains, then merges them layer by layer to complete the sorting. +- Introducing the divide and conquer strategy can often improve algorithm efficiency. On one hand, the divide and conquer strategy reduces the number of operations; on the other hand, it facilitates parallel optimization of the system after division. +- Divide and conquer can both solve many algorithmic problems and is widely applied in data structure and algorithm design, appearing everywhere. +- Compared to brute-force search, adaptive search is more efficient. Search algorithms with time complexity of $O(\log n)$ are typically implemented based on the divide and conquer strategy. +- Binary search is another typical application of divide and conquer. It does not include the step of merging solutions of subproblems. We can implement binary search through recursive divide and conquer. +- In the problem of building a binary tree, building the tree (original problem) can be divided into building the left subtree and right subtree (subproblems), which can be achieved by dividing the index intervals of the preorder and inorder traversals. +- In the hanota problem, a problem of size $n$ can be divided into two subproblems of size $n-1$ and one subproblem of size $1$. After solving these three subproblems in order, the original problem is solved. diff --git a/en/docs/chapter_dynamic_programming/dp_problem_features.md b/en/docs/chapter_dynamic_programming/dp_problem_features.md index dcb60fbbf..562401fee 100644 --- a/en/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/en/docs/chapter_dynamic_programming/dp_problem_features.md @@ -2,55 +2,55 @@ comments: true --- -# 14.2   Characteristics of dynamic programming problems +# 14.2   Characteristics of Dynamic Programming Problems In the previous section, we learned how dynamic programming solves the original problem by decomposing it into subproblems. In fact, subproblem decomposition is a general algorithmic approach, with different emphases in divide and conquer, dynamic programming, and backtracking. -- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and combine the solutions of the subproblems during backtracking to ultimately obtain the solution to the original problem. -- Dynamic programming also decomposes the problem recursively, but the main difference from divide and conquer algorithms is that the subproblems in dynamic programming are interdependent, and many overlapping subproblems will appear during the decomposition process. -- Backtracking algorithms exhaust all possible solutions through trial and error and avoid unnecessary search branches by pruning. The solution to the original problem consists of a series of decision steps, and we can consider each sub-sequence before each decision step as a subproblem. +- Divide and conquer algorithms recursively divide the original problem into multiple independent subproblems until the smallest subproblems are reached, and merge the solutions to the subproblems during backtracking to ultimately obtain the solution to the original problem. +- Dynamic programming also recursively decomposes problems, but the main difference from divide and conquer algorithms is that subproblems in dynamic programming are interdependent, and many overlapping subproblems appear during the decomposition process. +- Backtracking algorithms enumerate all possible solutions through trial and error, and avoid unnecessary search branches through pruning. The solution to the original problem consists of a series of decision steps, and we can regard the subsequence before each decision step as a subproblem. -In fact, dynamic programming is commonly used to solve optimization problems, which not only include overlapping subproblems but also have two other major characteristics: optimal substructure and statelessness. +In fact, dynamic programming is commonly used to solve optimization problems, which not only contain overlapping subproblems but also have two other major characteristics: optimal substructure and no aftereffects. -## 14.2.1   Optimal substructure +## 14.2.1   Optimal Substructure -We make a slight modification to the stair climbing problem to make it more suitable to demonstrate the concept of optimal substructure. +We make a slight modification to the stair climbing problem to make it more suitable for demonstrating the concept of optimal substructure. -!!! question "Minimum cost of climbing stairs" +!!! question "Climbing stairs with minimum cost" - Given a staircase, you can step up 1 or 2 steps at a time, and each step on the staircase has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost you need to pay at the $i$-th step, $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top? + Given a staircase, where you can climb $1$ or $2$ steps at a time, and each step has a non-negative integer representing the cost you need to pay at that step. Given a non-negative integer array $cost$, where $cost[i]$ represents the cost at the $i$-th step, and $cost[0]$ is the ground (starting point). What is the minimum cost required to reach the top? -As shown in Figure 14-6, if the costs of the 1st, 2nd, and 3rd steps are $1$, $10$, and $1$ respectively, then the minimum cost to climb to the 3rd step from the ground is $2$. +As shown in Figure 14-6, if the costs of the $1$st, $2$nd, and $3$rd steps are $1$, $10$, and $1$ respectively, then climbing from the ground to the $3$rd step requires a minimum cost of $2$. ![Minimum cost to climb to the 3rd step](dp_problem_features.assets/min_cost_cs_example.png){ class="animation-figure" }

Figure 14-6   Minimum cost to climb to the 3rd step

-Let $dp[i]$ be the cumulative cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$ or $i-2$ step, $dp[i]$ can only be either $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two: +Let $dp[i]$ be the accumulated cost of climbing to the $i$-th step. Since the $i$-th step can only come from the $i-1$-th or $i-2$-th step, $dp[i]$ can only equal $dp[i-1] + cost[i]$ or $dp[i-2] + cost[i]$. To minimize the cost, we should choose the smaller of the two: $$ dp[i] = \min(dp[i-1], dp[i-2]) + cost[i] $$ -This leads us to the meaning of optimal substructure: **The optimal solution to the original problem is constructed from the optimal solutions of subproblems**. +This leads us to the meaning of optimal substructure: **the optimal solution to the original problem is constructed from the optimal solutions to the subproblems**. -This problem obviously has optimal substructure: we select the better one from the optimal solutions of the two subproblems, $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution for the original problem $dp[i]$. +This problem clearly has optimal substructure: we select the better one from the optimal solutions to the two subproblems $dp[i-1]$ and $dp[i-2]$, and use it to construct the optimal solution to the original problem $dp[i]$. -So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to solve for the number of solutions, which seems to be a counting problem, but if we ask in another way: "Solve for the maximum number of solutions". We surprisingly find that **although the problem has changed, the optimal substructure has emerged**: the maximum number of solutions at the $n$-th step equals the sum of the maximum number of solutions at the $n-1$ and $n-2$ steps. Thus, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems. +So, does the stair climbing problem from the previous section have optimal substructure? Its goal is to find the number of ways, which seems to be a counting problem, but if we change the question: "Find the maximum number of ways". We surprisingly discover that **although the problem before and after modification are equivalent, the optimal substructure has emerged**: the maximum number of ways for the $n$-th step equals the sum of the maximum number of ways for the $n-1$-th and $n-2$-th steps. Therefore, the interpretation of optimal substructure is quite flexible and will have different meanings in different problems. -According to the state transition equation, and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code: +According to the state transition equation and the initial states $dp[1] = cost[1]$ and $dp[2] = cost[2]$, we can obtain the dynamic programming code: === "Python" ```python title="min_cost_climbing_stairs_dp.py" def min_cost_climbing_stairs_dp(cost: list[int]) -> int: - """Climbing stairs with minimum cost: Dynamic programming""" + """Minimum cost climbing stairs: Dynamic programming""" n = len(cost) - 1 if n == 1 or n == 2: return cost[n] - # Initialize dp table, used to store subproblem solutions + # Initialize dp table, used to store solutions to subproblems dp = [0] * (n + 1) - # Initial state: preset the smallest subproblem solution + # Initial state: preset the solution to the smallest subproblem dp[1], dp[2] = cost[1], cost[2] # State transition: gradually solve larger subproblems from smaller ones for i in range(3, n + 1): @@ -61,14 +61,14 @@ According to the state transition equation, and the initial states $dp[1] = cost === "C++" ```cpp title="min_cost_climbing_stairs_dp.cpp" - /* Climbing stairs with minimum cost: Dynamic programming */ + /* Minimum cost climbing stairs: Dynamic programming */ int minCostClimbingStairsDP(vector &cost) { int n = cost.size() - 1; if (n == 1 || n == 2) return cost[n]; - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems vector dp(n + 1); - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1] = cost[1]; dp[2] = cost[2]; // State transition: gradually solve larger subproblems from smaller ones @@ -82,14 +82,14 @@ According to the state transition equation, and the initial states $dp[1] = cost === "Java" ```java title="min_cost_climbing_stairs_dp.java" - /* Climbing stairs with minimum cost: Dynamic programming */ + /* Minimum cost climbing stairs: Dynamic programming */ int minCostClimbingStairsDP(int[] cost) { int n = cost.length - 1; if (n == 1 || n == 2) return cost[n]; - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems int[] dp = new int[n + 1]; - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1] = cost[1]; dp[2] = cost[2]; // State transition: gradually solve larger subproblems from smaller ones @@ -103,82 +103,234 @@ According to the state transition equation, and the initial states $dp[1] = cost === "C#" ```csharp title="min_cost_climbing_stairs_dp.cs" - [class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + int MinCostClimbingStairsDP(int[] cost) { + int n = cost.Length - 1; + if (n == 1 || n == 2) + return cost[n]; + // Initialize dp table, used to store solutions to subproblems + int[] dp = new int[n + 1]; + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + func minCostClimbingStairsDP(cost []int) int { + n := len(cost) - 1 + if n == 1 || n == 2 { + return cost[n] + } + min := func(a, b int) int { + if a < b { + return a + } + return b + } + // Initialize dp table, used to store solutions to subproblems + dp := make([]int, n+1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1] + dp[2] = cost[2] + // State transition: gradually solve larger subproblems from smaller ones + for i := 3; i <= n; i++ { + dp[i] = min(dp[i-1], dp[i-2]) + cost[i] + } + return dp[n] + } ``` === "Swift" ```swift title="min_cost_climbing_stairs_dp.swift" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + func minCostClimbingStairsDP(cost: [Int]) -> Int { + let n = cost.count - 1 + if n == 1 || n == 2 { + return cost[n] + } + // Initialize dp table, used to store solutions to subproblems + var dp = Array(repeating: 0, count: n + 1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1] + dp[2] = cost[2] + // State transition: gradually solve larger subproblems from smaller ones + for i in 3 ... n { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] + } + return dp[n] + } ``` === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + function minCostClimbingStairsDP(cost) { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // Initialize dp table, used to store solutions to subproblems + const dp = new Array(n + 1); + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + function minCostClimbingStairsDP(cost: Array): number { + const n = cost.length - 1; + if (n === 1 || n === 2) { + return cost[n]; + } + // Initialize dp table, used to store solutions to subproblems + const dp = new Array(n + 1); + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + int minCostClimbingStairsDP(List cost) { + int n = cost.length - 1; + if (n == 1 || n == 2) return cost[n]; + // Initialize dp table, used to store solutions to subproblems + List dp = List.filled(n + 1, 0); + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{min_cost_climbing_stairs_dp} + /* Minimum cost climbing stairs: Dynamic programming */ + fn min_cost_climbing_stairs_dp(cost: &[i32]) -> i32 { + let n = cost.len() - 1; + if n == 1 || n == 2 { + return cost[n]; + } + // Initialize dp table, used to store solutions to subproblems + let mut dp = vec![-1; n + 1]; + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + 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} + /* Minimum cost climbing stairs: Dynamic programming */ + int minCostClimbingStairsDP(int cost[], int costSize) { + int n = costSize - 1; + if (n == 1 || n == 2) + return cost[n]; + // Initialize dp table, used to store solutions to subproblems + int *dp = calloc(n + 1, sizeof(int)); + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1]; + dp[2] = cost[2]; + // State transition: gradually solve larger subproblems from smaller ones + for (int i = 3; i <= n; i++) { + dp[i] = myMin(dp[i - 1], dp[i - 2]) + cost[i]; + } + int res = dp[n]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="min_cost_climbing_stairs_dp.kt" - [class]{}-[func]{minCostClimbingStairsDP} + /* Minimum cost climbing stairs: Dynamic programming */ + fun minCostClimbingStairsDP(cost: IntArray): Int { + val n = cost.size - 1 + if (n == 1 || n == 2) return cost[n] + // Initialize dp table, used to store solutions to subproblems + val dp = IntArray(n + 1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = cost[1] + dp[2] = cost[2] + // State transition: gradually solve larger subproblems from smaller ones + for (i in 3..n) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] + } + return dp[n] + } ``` === "Ruby" ```ruby title="min_cost_climbing_stairs_dp.rb" - [class]{}-[func]{min_cost_climbing_stairs_dp} - ``` - -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - [class]{}-[func]{minCostClimbingStairsDP} + ### Minimum cost climbing stairs: DP ### + def min_cost_climbing_stairs_dp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + # Initialize dp table, used to store solutions to subproblems + dp = Array.new(n + 1, 0) + # Initial state: preset the solution to the smallest subproblem + dp[1], dp[2] = cost[1], cost[2] + # State transition: gradually solve larger subproblems from smaller ones + (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] } + dp[n] + end ``` Figure 14-7 shows the dynamic programming process for the above code. -![Dynamic programming process for minimum cost of climbing stairs](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } +![Dynamic programming process for climbing stairs with minimum cost](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } -

Figure 14-7   Dynamic programming process for minimum cost of climbing stairs

+

Figure 14-7   Dynamic programming process for climbing stairs with minimum cost

-This problem can also be space-optimized, compressing one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$: +This problem can also be space-optimized, compressing from one dimension to zero, reducing the space complexity from $O(n)$ to $O(1)$: === "Python" ```python title="min_cost_climbing_stairs_dp.py" def min_cost_climbing_stairs_dp_comp(cost: list[int]) -> int: - """Climbing stairs with minimum cost: Space-optimized dynamic programming""" + """Minimum cost climbing stairs: Space-optimized dynamic programming""" n = len(cost) - 1 if n == 1 or n == 2: return cost[n] @@ -191,7 +343,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red === "C++" ```cpp title="min_cost_climbing_stairs_dp.cpp" - /* Climbing stairs with minimum cost: Space-optimized dynamic programming */ + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ int minCostClimbingStairsDPComp(vector &cost) { int n = cost.size() - 1; if (n == 1 || n == 2) @@ -209,7 +361,7 @@ This problem can also be space-optimized, compressing one dimension to zero, red === "Java" ```java title="min_cost_climbing_stairs_dp.java" - /* Climbing stairs with minimum cost: Space-optimized dynamic programming */ + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ int minCostClimbingStairsDPComp(int[] cost) { int n = cost.length - 1; if (n == 1 || n == 2) @@ -227,97 +379,231 @@ This problem can also be space-optimized, compressing one dimension to zero, red === "C#" ```csharp title="min_cost_climbing_stairs_dp.cs" - [class]{min_cost_climbing_stairs_dp}-[func]{MinCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + func minCostClimbingStairsDPComp(cost []int) int { + n := len(cost) - 1 + if n == 1 || n == 2 { + return cost[n] + } + min := func(a, b int) int { + if a < b { + return a + } + return b + } + // Initial state: preset the solution to the smallest subproblem + a, b := cost[1], cost[2] + // State transition: gradually solve larger subproblems from smaller ones + for i := 3; i <= n; i++ { + tmp := b + b = min(a, tmp) + cost[i] + a = tmp + } + return b + } ``` === "Swift" ```swift title="min_cost_climbing_stairs_dp.swift" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + func minCostClimbingStairsDPComp(cost: [Int]) -> Int { + let n = cost.count - 1 + if n == 1 || n == 2 { + return cost[n] + } + var (a, b) = (cost[1], cost[2]) + for i in 3 ... n { + (a, b) = (b, min(a, b) + cost[i]) + } + return b + } ``` === "JS" ```javascript title="min_cost_climbing_stairs_dp.js" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{min_cost_climbing_stairs_dp_comp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + 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} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + int minCostClimbingStairsDPComp(int cost[], int costSize) { + int n = costSize - 1; + if (n == 1 || n == 2) + return cost[n]; + int a = cost[1], b = cost[2]; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = myMin(a, tmp) + cost[i]; + a = tmp; + } + return b; + } ``` === "Kotlin" ```kotlin title="min_cost_climbing_stairs_dp.kt" - [class]{}-[func]{minCostClimbingStairsDPComp} + /* Minimum cost climbing stairs: Space-optimized dynamic programming */ + fun minCostClimbingStairsDPComp(cost: IntArray): Int { + val n = cost.size - 1 + if (n == 1 || n == 2) return cost[n] + var a = cost[1] + var b = cost[2] + for (i in 3..n) { + val tmp = b + b = min(a, tmp) + cost[i] + a = tmp + } + return b + } ``` === "Ruby" ```ruby title="min_cost_climbing_stairs_dp.rb" - [class]{}-[func]{min_cost_climbing_stairs_dp_comp} + ### Minimum cost climbing stairs: DP ### + def min_cost_climbing_stairs_dp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + # Initialize dp table, used to store solutions to subproblems + dp = Array.new(n + 1, 0) + # Initial state: preset the solution to the smallest subproblem + dp[1], dp[2] = cost[1], cost[2] + # State transition: gradually solve larger subproblems from smaller ones + (3...(n + 1)).each { |i| dp[i] = [dp[i - 1], dp[i - 2]].min + cost[i] } + dp[n] + end + + # Minimum cost climbing stairs: Space-optimized dynamic programming + def min_cost_climbing_stairs_dp_comp(cost) + n = cost.length - 1 + return cost[n] if n == 1 || n == 2 + a, b = cost[1], cost[2] + (3...(n + 1)).each { |i| a, b = b, [a, b].min + cost[i] } + b + end ``` -=== "Zig" +## 14.2.2   No Aftereffects - ```zig title="min_cost_climbing_stairs_dp.zig" - [class]{}-[func]{minCostClimbingStairsDPComp} - ``` +No aftereffects is one of the important characteristics that enable dynamic programming to solve problems effectively. Its definition is: **given a certain state, its future development is only related to the current state and has nothing to do with all past states**. -## 14.2.2   Statelessness - -Statelessness is one of the important characteristics that make dynamic programming effective in solving problems. Its definition is: **Given a certain state, its future development is only related to the current state and unrelated to all past states experienced**. - -Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping 1 step and 2 steps respectively. When making these two choices, we do not need to consider the states before state $i$, as they do not affect the future of state $i$. +Taking the stair climbing problem as an example, given state $i$, it will develop into states $i+1$ and $i+2$, corresponding to jumping $1$ step and jumping $2$ steps, respectively. When making these two choices, we do not need to consider the states before state $i$, as they have no effect on the future of state $i$. However, if we add a constraint to the stair climbing problem, the situation changes. -!!! question "Stair climbing with constraints" +!!! question "Climbing stairs with constraint" - Given a staircase with $n$ steps, you can go up 1 or 2 steps each time, **but you cannot jump 1 step twice in a row**. How many ways are there to climb to the top? + Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, **but you cannot jump $1$ step in two consecutive rounds**. How many ways are there to climb to the top? -As shown in Figure 14-8, there are only 2 feasible options for climbing to the 3rd step, among which the option of jumping 1 step three times in a row does not meet the constraint condition and is therefore discarded. +As shown in Figure 14-8, there are only $2$ feasible ways to climb to the $3$rd step. The way of jumping $1$ step three consecutive times does not satisfy the constraint and is therefore discarded. -![Number of feasible options for climbing to the 3rd step with constraints](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } +![Number of ways to climb to the 3rd step with constraint](dp_problem_features.assets/climbing_stairs_constraint_example.png){ class="animation-figure" } -

Figure 14-8   Number of feasible options for climbing to the 3rd step with constraints

+

Figure 14-8   Number of ways to climb to the 3rd step with constraint

-In this problem, if the last round was a jump of 1 step, then the next round must be a jump of 2 steps. This means that **the next step choice cannot be independently determined by the current state (current stair step), but also depends on the previous state (last round's stair step)**. +In this problem, if the previous round was a jump of $1$ step, then the next round must jump $2$ steps. This means that **the next choice cannot be determined solely by the current state (current stair step number), but also depends on the previous state (the stair step number from the previous round)**. -It is not difficult to find that this problem no longer satisfies statelessness, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents this round's jump of 1 step, but it includes many "last round was a jump of 1 step" options, which, to meet the constraint, cannot be directly included in $dp[i]$. +It is not difficult to see that this problem no longer satisfies no aftereffects, and the state transition equation $dp[i] = dp[i-1] + dp[i-2]$ also fails, because $dp[i-1]$ represents jumping $1$ step in this round, but it includes many solutions where "the previous round was a jump of $1$ step", which cannot be directly counted in $dp[i]$ to satisfy the constraint. -For this, we need to expand the state definition: **State $[i, j]$ represents being on the $i$-th step and the last round was a jump of $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the last round was a jump of 1 step or 2 steps, and we can judge accordingly where the current state came from. +For this reason, we need to expand the state definition: **state $[i, j]$ represents being on the $i$-th step with the previous round having jumped $j$ steps**, where $j \in \{1, 2\}$. This state definition effectively distinguishes whether the previous round was a jump of $1$ step or $2$ steps, allowing us to determine where the current state came from. -- When the last round was a jump of 1 step, the round before last could only choose to jump 2 steps, that is, $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$. -- When the last round was a jump of 2 steps, the round before last could choose to jump 1 step or 2 steps, that is, $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$. +- When the previous round jumped $1$ step, the round before that could only choose to jump $2$ steps, i.e., $dp[i, 1]$ can only be transferred from $dp[i-1, 2]$. +- When the previous round jumped $2$ steps, the round before that could choose to jump $1$ step or $2$ steps, i.e., $dp[i, 2]$ can be transferred from $dp[i-2, 1]$ or $dp[i-2, 2]$. -As shown in Figure 14-9, $dp[i, j]$ represents the number of solutions for state $[i, j]$. At this point, the state transition equation is: +As shown in Figure 14-9, under this definition, $dp[i, j]$ represents the number of ways for state $[i, j]$. The state transition equation is then: $$ \begin{cases} @@ -326,22 +612,22 @@ dp[i, 2] = dp[i-2, 1] + dp[i-2, 2] \end{cases} $$ -![Recursive relationship considering constraints](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" } +![Recurrence relation considering constraints](dp_problem_features.assets/climbing_stairs_constraint_state_transfer.png){ class="animation-figure" } -

Figure 14-9   Recursive relationship considering constraints

+

Figure 14-9   Recurrence relation considering constraints

-In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two representing the total number of solutions for climbing to the $n$-th step: +Finally, return $dp[n, 1] + dp[n, 2]$, where the sum of the two represents the total number of ways to climb to the $n$-th step: === "Python" ```python title="climbing_stairs_constraint_dp.py" def climbing_stairs_constraint_dp(n: int) -> int: - """Constrained climbing stairs: Dynamic programming""" + """Climbing stairs with constraint: Dynamic programming""" if n == 1 or n == 2: return 1 - # Initialize dp table, used to store subproblem solutions + # Initialize dp table, used to store solutions to subproblems dp = [[0] * 3 for _ in range(n + 1)] - # Initial state: preset the smallest subproblem solution + # Initial state: preset the solution to the smallest subproblem dp[1][1], dp[1][2] = 1, 0 dp[2][1], dp[2][2] = 0, 1 # State transition: gradually solve larger subproblems from smaller ones @@ -354,14 +640,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen === "C++" ```cpp title="climbing_stairs_constraint_dp.cpp" - /* Constrained climbing stairs: Dynamic programming */ + /* Climbing stairs with constraint: Dynamic programming */ int climbingStairsConstraintDP(int n) { if (n == 1 || n == 2) { return 1; } - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems vector> dp(n + 1, vector(3, 0)); - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1][1] = 1; dp[1][2] = 0; dp[2][1] = 0; @@ -378,14 +664,14 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen === "Java" ```java title="climbing_stairs_constraint_dp.java" - /* Constrained climbing stairs: Dynamic programming */ + /* Climbing stairs with constraint: Dynamic programming */ int climbingStairsConstraintDP(int n) { if (n == 1 || n == 2) { return 1; } - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems int[][] dp = new int[n + 1][3]; - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1][1] = 1; dp[1][2] = 0; dp[2][1] = 0; @@ -402,75 +688,256 @@ In the end, returning $dp[n, 1] + dp[n, 2]$ will do, the sum of the two represen === "C#" ```csharp title="climbing_stairs_constraint_dp.cs" - [class]{climbing_stairs_constraint_dp}-[func]{ClimbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + int ClimbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // Initialize dp table, used to store solutions to subproblems + int[,] dp = new int[n + 1, 3]; + // Initial state: preset the solution to the smallest subproblem + dp[1, 1] = 1; + dp[1, 2] = 0; + dp[2, 1] = 0; + dp[2, 2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + func climbingStairsConstraintDP(n int) int { + if n == 1 || n == 2 { + return 1 + } + // Initialize dp table, used to store solutions to subproblems + dp := make([][3]int, n+1) + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + func climbingStairsConstraintDP(n: Int) -> Int { + if n == 1 || n == 2 { + return 1 + } + // Initialize dp table, used to store solutions to subproblems + var dp = Array(repeating: Array(repeating: 0, count: 3), count: n + 1) + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // State transition: gradually solve larger subproblems from smaller ones + for i in 3 ... n { + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + } + return dp[n][1] + dp[n][2] + } ``` === "JS" ```javascript title="climbing_stairs_constraint_dp.js" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + function climbingStairsConstraintDP(n) { + if (n === 1 || n === 2) { + return 1; + } + // Initialize dp table, used to store solutions to subproblems + const dp = Array.from(new Array(n + 1), () => new Array(3)); + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + function climbingStairsConstraintDP(n: number): number { + if (n === 1 || n === 2) { + return 1; + } + // Initialize dp table, used to store solutions to subproblems + const dp = Array.from({ length: n + 1 }, () => new Array(3)); + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // Initialize dp table, used to store solutions to subproblems + List> dp = List.generate(n + 1, (index) => List.filled(3, 0)); + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbing_stairs_constraint_dp} + /* Climbing stairs with constraint: Dynamic programming */ + fn climbing_stairs_constraint_dp(n: usize) -> i32 { + if n == 1 || n == 2 { + return 1; + }; + // Initialize dp table, used to store solutions to subproblems + let mut dp = vec![vec![-1; 3]; n + 1]; + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + 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} + /* Climbing stairs with constraint: Dynamic programming */ + int climbingStairsConstraintDP(int n) { + if (n == 1 || n == 2) { + return 1; + } + // Initialize dp table, used to store solutions to subproblems + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(3, sizeof(int)); + } + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1; + dp[1][2] = 0; + dp[2][1] = 0; + dp[2][2] = 1; + // State transition: gradually solve larger subproblems from smaller ones + for (int i = 3; i <= n; i++) { + dp[i][1] = dp[i - 1][2]; + dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; + } + int res = dp[n][1] + dp[n][2]; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_constraint_dp.kt" - [class]{}-[func]{climbingStairsConstraintDP} + /* Climbing stairs with constraint: Dynamic programming */ + fun climbingStairsConstraintDP(n: Int): Int { + if (n == 1 || n == 2) { + return 1 + } + // Initialize dp table, used to store solutions to subproblems + val dp = Array(n + 1) { IntArray(3) } + // Initial state: preset the solution to the smallest subproblem + dp[1][1] = 1 + dp[1][2] = 0 + dp[2][1] = 0 + dp[2][2] = 1 + // State transition: gradually solve larger subproblems from smaller ones + for (i in 3..n) { + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + } + return dp[n][1] + dp[n][2] + } ``` === "Ruby" ```ruby title="climbing_stairs_constraint_dp.rb" - [class]{}-[func]{climbing_stairs_constraint_dp} + ### Climbing stairs with constraint: DP ### + def climbing_stairs_constraint_dp(n) + return 1 if n == 1 || n == 2 + + # Initialize dp table, used to store solutions to subproblems + dp = Array.new(n + 1) { Array.new(3, 0) } + # Initial state: preset the solution to the smallest subproblem + dp[1][1], dp[1][2] = 1, 0 + dp[2][1], dp[2][2] = 0, 1 + # State transition: gradually solve larger subproblems from smaller ones + for i in 3...(n + 1) + dp[i][1] = dp[i - 1][2] + dp[i][2] = dp[i - 2][1] + dp[i - 2][2] + end + + dp[n][1] + dp[n][2] + end ``` -=== "Zig" +In the above case, since we only need to consider one more preceding state, we can still make the problem satisfy no aftereffects by expanding the state definition. However, some problems have very severe "aftereffects". - ```zig title="climbing_stairs_constraint_dp.zig" - [class]{}-[func]{climbingStairsConstraintDP} - ``` +!!! question "Climbing stairs with obstacle generation" -In the above cases, since we only need to consider the previous state, we can still meet the statelessness by expanding the state definition. However, some problems have very serious "state effects". + Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time. **It is stipulated that when climbing to the $i$-th step, the system will automatically place an obstacle on the $2i$-th step, and thereafter no round is allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the $2$nd and $3$rd steps, then afterwards you cannot jump to the $4$th and $6$th steps. How many ways are there to climb to the top? -!!! question "Stair climbing with obstacle generation" +In this problem, the next jump depends on all past states, because each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming is often difficult to solve. - Given a staircase with $n$ steps, you can go up 1 or 2 steps each time. **It is stipulated that when climbing to the $i$-th step, the system automatically places an obstacle on the $2i$-th step, and thereafter all rounds are not allowed to jump to the $2i$-th step**. For example, if the first two rounds jump to the 2nd and 3rd steps, then later you cannot jump to the 4th and 6th steps. How many ways are there to climb to the top? - -In this problem, the next jump depends on all past states, as each jump places obstacles on higher steps, affecting future jumps. For such problems, dynamic programming often struggles to solve. - -In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy statelessness. For these kinds of problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time. +In fact, many complex combinatorial optimization problems (such as the traveling salesman problem) do not satisfy no aftereffects. For such problems, we usually choose to use other methods, such as heuristic search, genetic algorithms, reinforcement learning, etc., to obtain usable local optimal solutions within a limited time. diff --git a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 34f855d9e..0e3823d4e 100644 --- a/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/en/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -2,70 +2,70 @@ comments: true --- -# 14.3   Dynamic programming problem-solving approach +# 14.3   Dynamic Programming Problem-Solving Approach -The last two sections introduced the main characteristics of dynamic programming problems. Next, let's explore two more practical issues together. +The previous two sections introduced the main characteristics of dynamic programming problems. Next, let us explore two more practical issues together. 1. How to determine whether a problem is a dynamic programming problem? -2. What are the complete steps to solve a dynamic programming problem? +2. What is the complete process for solving a dynamic programming problem, and where should we start? -## 14.3.1   Problem determination +## 14.3.1   Problem Determination -Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and exhibits no aftereffects, it is usually suitable for dynamic programming solutions. However, it is often difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and **first observe whether the problem is suitable for resolution using backtracking (exhaustive search)**. +Generally speaking, if a problem contains overlapping subproblems, optimal substructure, and satisfies no aftereffects, then it is usually suitable for solving with dynamic programming. However, it is difficult to directly extract these characteristics from the problem description. Therefore, we usually relax the conditions and **first observe whether the problem is suitable for solving with backtracking (exhaustive search)**. -**Problems suitable for backtracking usually fit the "decision tree model"**, which can be described using a tree structure, where each node represents a decision, and each path represents a sequence of decisions. +**Problems suitable for solving with backtracking usually satisfy the "decision tree model"**, which means the problem can be described using a tree structure, where each node represents a decision and each path represents a sequence of decisions. -In other words, if the problem contains explicit decision concepts, and the solution is produced through a series of decisions, then it fits the decision tree model and can usually be solved using backtracking. +In other words, if a problem contains an explicit concept of decisions, and the solution is generated through a series of decisions, then it satisfies the decision tree model and can usually be solved using backtracking. -On this basis, there are some "bonus points" for determining dynamic programming problems. +On this basis, dynamic programming problems also have some "bonus points" for determination. -- The problem contains descriptions of maximization (minimization) or finding the most (least) optimal solution. -- The problem's states can be represented using a list, multi-dimensional matrix, or tree, and a state has a recursive relationship with its surrounding states. +- The problem contains descriptions such as maximum (minimum) or most (least), indicating optimization. +- The problem's state can be represented using a list, multi-dimensional matrix, or tree, and a state has a recurrence relation with its surrounding states. Correspondingly, there are also some "penalty points". -- The goal of the problem is to find all possible solutions, not just the optimal solution. -- The problem description has obvious characteristics of permutations and combinations, requiring the return of specific multiple solutions. +- The goal of the problem is to find all possible solutions, rather than finding the optimal solution. +- The problem description has obvious permutation and combination characteristics, requiring the return of specific multiple solutions. -If a problem fits the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solution process. +If a problem satisfies the decision tree model and has relatively obvious "bonus points", we can assume it is a dynamic programming problem and verify it during the solving process. -## 14.3.2   Problem-solving steps +## 14.3.2   Problem-Solving Steps -The dynamic programming problem-solving process varies with the nature and difficulty of the problem but generally follows these steps: describe decisions, define states, establish a $dp$ table, derive state transition equations, and determine boundary conditions, etc. +The problem-solving process for dynamic programming varies depending on the nature and difficulty of the problem, but generally follows these steps: describe decisions, define states, establish the $dp$ table, derive state transition equations, determine boundary conditions, etc. -To illustrate the problem-solving steps more vividly, we use a classic problem, "Minimum Path Sum", as an example. +To illustrate the problem-solving steps more vividly, we use a classic problem "minimum path sum" as an example. !!! question - Given an $n \times m$ two-dimensional grid `grid`, each cell in the grid contains a non-negative integer representing the cost of that cell. The robot starts from the top-left cell and can only move down or right at each step until it reaches the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. + Given an $n \times m$ two-dimensional grid `grid`, where each cell in the grid contains a non-negative integer representing the cost of that cell. A robot starts from the top-left cell and can only move down or right at each step until reaching the bottom-right cell. Return the minimum path sum from the top-left to the bottom-right. -Figure 14-10 shows an example, where the given grid's minimum path sum is $13$. +Figure 14-10 shows an example where the minimum path sum for the given grid is $13$. -![Minimum Path Sum Example Data](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" } +![Minimum path sum example data](dp_solution_pipeline.assets/min_path_sum_example.png){ class="animation-figure" } -

Figure 14-10   Minimum Path Sum Example Data

+

Figure 14-10   Minimum path sum example data

-**First step: Think about each round of decisions, define the state, and thereby obtain the $dp$ table** +**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table** -Each round of decisions in this problem is to move one step down or right from the current cell. Suppose the row and column indices of the current cell are $[i, j]$, then after moving down or right, the indices become $[i+1, j]$ or $[i, j+1]$. Therefore, the state should include two variables: the row index and the column index, denoted as $[i, j]$. +The decision in each round of this problem is to move one step down or right from the current cell. Let the row and column indices of the current cell be $[i, j]$. After moving down or right, the indices become $[i+1, j]$ or $[i, j+1]$. Therefore, the state should include two variables, the row index and column index, denoted as $[i, j]$. -The state $[i, j]$ corresponds to the subproblem: the minimum path sum from the starting point $[0, 0]$ to $[i, j]$, denoted as $dp[i, j]$. +State $[i, j]$ corresponds to the subproblem: **the minimum path sum from the starting point $[0, 0]$ to $[i, j]$**, denoted as $dp[i, j]$. -Thus, we obtain the two-dimensional $dp$ matrix shown in Figure 14-11, whose size is the same as the input grid $grid$. +From this, we obtain the two-dimensional $dp$ matrix shown in Figure 14-11, whose size is the same as the input grid $grid$. -![State definition and DP table](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" } +![State definition and dp table](dp_solution_pipeline.assets/min_path_sum_solution_state_definition.png){ class="animation-figure" } -

Figure 14-11   State definition and DP table

+

Figure 14-11   State definition and dp table

!!! note - Dynamic programming and backtracking can be described as a sequence of decisions, while a state consists of all decision variables. It should include all variables that describe the progress of solving the problem, containing enough information to derive the next state. + The dynamic programming and backtracking processes can be described as a sequence of decisions, and the state consists of all decision variables. It should contain all variables describing the progress of problem-solving, and should contain sufficient information to derive the next state. Each state corresponds to a subproblem, and we define a $dp$ table to store the solutions to all subproblems. Each independent variable of the state is a dimension of the $dp$ table. Essentially, the $dp$ table is a mapping between states and solutions to subproblems. -**Second step: Identify the optimal substructure, then derive the state transition equation** +**Step 2: Identify the optimal substructure, and then derive the state transition equation** -For the state $[i, j]$, it can only be derived from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. +For state $[i, j]$, it can only be transferred from the cell above $[i-1, j]$ or the cell to the left $[i, j-1]$. Therefore, the optimal substructure is: the minimum path sum to reach $[i, j]$ is determined by the smaller of the minimum path sums of $[i, j-1]$ and $[i-1, j]$. Based on the above analysis, the state transition equation shown in Figure 14-12 can be derived: @@ -79,15 +79,15 @@ $$ !!! note - Based on the defined $dp$ table, think about the relationship between the original problem and the subproblems, and find out how to construct the optimal solution to the original problem from the optimal solutions to the subproblems, i.e., the optimal substructure. + Based on the defined $dp$ table, think about the relationship between the original problem and subproblems, and find the method to construct the optimal solution to the original problem from the optimal solutions to the subproblems, which is the optimal substructure. - Once we have identified the optimal substructure, we can use it to build the state transition equation. + Once we identify the optimal substructure, we can use it to construct the state transition equation. -**Third step: Determine boundary conditions and state transition order** +**Step 3: Determine boundary conditions and state transition order** -In this problem, the states in the first row can only come from the states to their left, and the states in the first column can only come from the states above them, so the first row $i = 0$ and the first column $j = 0$ are the boundary conditions. +In this problem, states in the first row can only come from the state to their left, and states in the first column can only come from the state above them. Therefore, the first row $i = 0$ and first column $j = 0$ are boundary conditions. -As shown in Figure 14-13, since each cell is derived from the cell to its left and the cell above it, we use loops to traverse the matrix, the outer loop iterating over the rows and the inner loop iterating over the columns. +As shown in Figure 14-13, since each cell is transferred from the cell to its left and the cell above it, we use loops to traverse the matrix, with the outer loop traversing rows and the inner loop traversing columns. ![Boundary conditions and state transition order](dp_solution_pipeline.assets/min_path_sum_solution_initial_state.png){ class="animation-figure" } @@ -95,58 +95,58 @@ As shown in Figure 14-13, since each cell is derived from the cell to its left a !!! note - Boundary conditions are used in dynamic programming to initialize the $dp$ table, and in search to prune. - - The core of the state transition order is to ensure that when calculating the solution to the current problem, all the smaller subproblems it depends on have already been correctly calculated. + Boundary conditions in dynamic programming are used to initialize the $dp$ table, and in search are used for pruning. -Based on the above analysis, we can directly write the dynamic programming code. However, the decomposition of subproblems is a top-down approach, so implementing it in the order of "brute-force search → memoized search → dynamic programming" is more in line with habitual thinking. + The core of state transition order is to ensure that when computing the solution to the current problem, all the smaller subproblems it depends on have already been computed correctly. -### 1.   Method 1: Brute-force search +Based on the above analysis, we can directly write the dynamic programming code. However, subproblem decomposition is a top-down approach, so implementing in the order "brute force search $\rightarrow$ memoization $\rightarrow$ dynamic programming" is more aligned with thinking habits. -Start searching from the state $[i, j]$, constantly decomposing it into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. +### 1.   Method 1: Brute Force Search -- **Recursive parameter**: state $[i, j]$. -- **Return value**: the minimum path sum from $[0, 0]$ to $[i, j]$ $dp[i, j]$. -- **Termination condition**: when $i = 0$ and $j = 0$, return the cost $grid[0, 0]$. -- **Pruning**: when $i < 0$ or $j < 0$ index out of bounds, return the cost $+\infty$, representing infeasibility. +Starting from state $[i, j]$, continuously decompose into smaller states $[i-1, j]$ and $[i, j-1]$. The recursive function includes the following elements. -Implementation code as follows: +- **Recursive parameters**: state $[i, j]$. +- **Return value**: minimum path sum from $[0, 0]$ to $[i, j]$, which is $dp[i, j]$. +- **Termination condition**: when $i = 0$ and $j = 0$, return cost $grid[0, 0]$. +- **Pruning**: when $i < 0$ or $j < 0$, the index is out of bounds, return cost $+\infty$, representing infeasibility. + +The implementation code is as follows: === "Python" ```python title="min_path_sum.py" def min_path_sum_dfs(grid: list[list[int]], i: int, j: int) -> int: - """Minimum path sum: Brute force search""" + """Minimum path sum: Brute-force search""" # If it's the top-left cell, terminate the search if i == 0 and j == 0: return grid[0][0] - # If the row or column index is out of bounds, return a +∞ cost + # If row or column index is out of bounds, return +∞ cost if i < 0 or j < 0: return inf - # Calculate the minimum path cost from the top-left to (i-1, j) and (i, j-1) + # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) up = min_path_sum_dfs(grid, i - 1, j) left = min_path_sum_dfs(grid, i, j - 1) - # Return the minimum path cost from the top-left to (i, j) + # Return the minimum path cost from top-left to (i, j) return min(left, up) + grid[i][j] ``` === "C++" ```cpp title="min_path_sum.cpp" - /* Minimum path sum: Brute force search */ + /* Minimum path sum: Brute-force search */ int minPathSumDFS(vector> &grid, int i, int j) { // If it's the top-left cell, terminate the search if (i == 0 && j == 0) { return grid[0][0]; } - // If the row or column index is out of bounds, return a +∞ cost + // If row or column index is out of bounds, return +∞ cost if (i < 0 || j < 0) { return INT_MAX; } - // Calculate the minimum path cost from the top-left to (i-1, j) and (i, j-1) + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) int up = minPathSumDFS(grid, i - 1, j); int left = minPathSumDFS(grid, i, j - 1); - // Return the minimum path cost from the top-left to (i, j) + // Return the minimum path cost from top-left to (i, j) return min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; } ``` @@ -154,20 +154,20 @@ Implementation code as follows: === "Java" ```java title="min_path_sum.java" - /* Minimum path sum: Brute force search */ + /* Minimum path sum: Brute-force search */ int minPathSumDFS(int[][] grid, int i, int j) { // If it's the top-left cell, terminate the search if (i == 0 && j == 0) { return grid[0][0]; } - // If the row or column index is out of bounds, return a +∞ cost + // If row or column index is out of bounds, return +∞ cost if (i < 0 || j < 0) { return Integer.MAX_VALUE; } - // Calculate the minimum path cost from the top-left to (i-1, j) and (i, j-1) + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) int up = minPathSumDFS(grid, i - 1, j); int left = minPathSumDFS(grid, i, j - 1); - // Return the minimum path cost from the top-left to (i, j) + // Return the minimum path cost from top-left to (i, j) return Math.min(left, up) + grid[i][j]; } ``` @@ -175,82 +175,227 @@ Implementation code as follows: === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDFS} + /* Minimum path sum: Brute-force search */ + int MinPathSumDFS(int[][] grid, int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return int.MaxValue; + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + int up = MinPathSumDFS(grid, i - 1, j); + int left = MinPathSumDFS(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + return Math.Min(left, up) + grid[i][j]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + func minPathSumDFS(grid [][]int, i, j int) int { + // If it's the top-left cell, terminate the search + if i == 0 && j == 0 { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return math.MaxInt + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + up := minPathSumDFS(grid, i-1, j) + left := minPathSumDFS(grid, i, j-1) + // Return the minimum path cost from top-left to (i, j) + return int(math.Min(float64(left), float64(up))) + grid[i][j] + } ``` === "Swift" ```swift title="min_path_sum.swift" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + func minPathSumDFS(grid: [[Int]], i: Int, j: Int) -> Int { + // If it's the top-left cell, terminate the search + if i == 0, j == 0 { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return .max + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + let up = minPathSumDFS(grid: grid, i: i - 1, j: j) + let left = minPathSumDFS(grid: grid, i: i, j: j - 1) + // Return the minimum path cost from top-left to (i, j) + return min(left, up) + grid[i][j] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + function minPathSumDFS(grid, i, j) { + // If it's the top-left cell, terminate the search + if (i === 0 && j === 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Infinity; + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + return Math.min(left, up) + grid[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + function minPathSumDFS( + grid: Array>, + i: number, + j: number + ): number { + // If it's the top-left cell, terminate the search + if (i === 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Infinity; + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + const up = minPathSumDFS(grid, i - 1, j); + const left = minPathSumDFS(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + return Math.min(left, up) + grid[i][j]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + int minPathSumDFS(List> grid, int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + // In Dart, int type is fixed-range integer, no value representing "infinity" + return BigInt.from(2).pow(31).toInt(); + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + return min(left, up) + grid[i][j]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dfs} + /* Minimum path sum: Brute-force search */ + fn min_path_sum_dfs(grid: &Vec>, i: i32, j: i32) -> i32 { + // If it's the top-left cell, terminate the search + if i == 0 && j == 0 { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return i32::MAX; + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + let up = min_path_sum_dfs(grid, i - 1, j); + let left = min_path_sum_dfs(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + std::cmp::min(left, up) + grid[i as usize][j as usize] + } ``` === "C" ```c title="min_path_sum.c" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + int minPathSumDFS(int grid[MAX_SIZE][MAX_SIZE], int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return INT_MAX; + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + int up = minPathSumDFS(grid, i - 1, j); + int left = minPathSumDFS(grid, i, j - 1); + // Return the minimum path cost from top-left to (i, j) + return myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDFS} + /* Minimum path sum: Brute-force search */ + fun minPathSumDFS(grid: Array, i: Int, j: Int): Int { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + val up = minPathSumDFS(grid, i - 1, j) + val left = minPathSumDFS(grid, i, j - 1) + // Return the minimum path cost from top-left to (i, j) + return min(left, up) + grid[i][j] + } ``` === "Ruby" ```ruby title="min_path_sum.rb" - [class]{}-[func]{min_path_sum_dfs} + ### Minimum path sum: brute force search ### + def min_path_sum_dfs(grid, i, j) + # If it's the top-left cell, terminate the search + return grid[i][j] if i == 0 && j == 0 + # If row or column index is out of bounds, return +∞ cost + return Float::INFINITY if i < 0 || j < 0 + # Calculate the minimum path cost from top-left to (i-1, j) and (i, j-1) + up = min_path_sum_dfs(grid, i - 1, j) + left = min_path_sum_dfs(grid, i, j - 1) + # Return the minimum path cost from top-left to (i, j) + [left, up].min + grid[i][j] + end ``` -=== "Zig" +Figure 14-14 shows the recursion tree rooted at $dp[2, 1]$, which includes some overlapping subproblems whose number will increase sharply as the size of grid `grid` grows. - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDFS} - ``` +Essentially, the reason for overlapping subproblems is: **there are multiple paths from the top-left corner to reach a certain cell**. -Figure 14-14 shows the recursive tree rooted at $dp[2, 1]$, which includes some overlapping subproblems, the number of which increases sharply as the size of the grid `grid` increases. +![Brute force search recursion tree](dp_solution_pipeline.assets/min_path_sum_dfs.png){ class="animation-figure" } -Essentially, the reason for overlapping subproblems is: **there are multiple paths to reach a certain cell from the top-left corner**. +

Figure 14-14   Brute force search recursion tree

-![Brute-force search recursive tree](dp_solution_pipeline.assets/min_path_sum_dfs.png){ class="animation-figure" } +Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is $m + n - 2$, giving a worst-case time complexity of $O(2^{m + n})$, where $n$ and $m$ are the number of rows and columns of the grid, respectively. Note that this calculation does not account for situations near the grid boundaries, where only one choice remains when reaching the grid boundary, so the actual number of paths will be somewhat less. -

Figure 14-14   Brute-force search recursive tree

+### 2.   Method 2: Memoization -Each state has two choices, down and right, so the total number of steps from the top-left corner to the bottom-right corner is $m + n - 2$, so the worst-case time complexity is $O(2^{m + n})$. Please note that this calculation method does not consider the situation near the grid edge, where there is only one choice left when reaching the network edge, so the actual number of paths will be less. - -### 2.   Method 2: Memoized search - -We introduce a memo list `mem` of the same size as the grid `grid`, used to record the solutions to various subproblems, and prune overlapping subproblems: +We introduce a memo list `mem` of the same size as grid `grid` to record the solutions to subproblems and prune overlapping subproblems: === "Python" @@ -258,20 +403,20 @@ We introduce a memo list `mem` of the same size as the grid `grid`, used to reco def min_path_sum_dfs_mem( grid: list[list[int]], mem: list[list[int]], i: int, j: int ) -> int: - """Minimum path sum: Memoized search""" + """Minimum path sum: Memoization search""" # If it's the top-left cell, terminate the search if i == 0 and j == 0: return grid[0][0] - # If the row or column index is out of bounds, return a +∞ cost + # If row or column index is out of bounds, return +∞ cost if i < 0 or j < 0: return inf - # If there is a record, return it + # If there's a record, return it directly if mem[i][j] != -1: return mem[i][j] - # The minimum path cost from the left and top cells + # Minimum path cost for left and upper cells up = min_path_sum_dfs_mem(grid, mem, i - 1, j) left = min_path_sum_dfs_mem(grid, mem, i, j - 1) - # Record and return the minimum path cost from the top-left to (i, j) + # Record and return the minimum path cost from top-left to (i, j) mem[i][j] = min(left, up) + grid[i][j] return mem[i][j] ``` @@ -279,24 +424,24 @@ We introduce a memo list `mem` of the same size as the grid `grid`, used to reco === "C++" ```cpp title="min_path_sum.cpp" - /* Minimum path sum: Memoized search */ + /* Minimum path sum: Memoization search */ int minPathSumDFSMem(vector> &grid, vector> &mem, int i, int j) { // If it's the top-left cell, terminate the search if (i == 0 && j == 0) { return grid[0][0]; } - // If the row or column index is out of bounds, return a +∞ cost + // If row or column index is out of bounds, return +∞ cost if (i < 0 || j < 0) { return INT_MAX; } - // If there is a record, return it + // If there's a record, return it directly if (mem[i][j] != -1) { return mem[i][j]; } - // The minimum path cost from the left and top cells + // Minimum path cost for left and upper cells int up = minPathSumDFSMem(grid, mem, i - 1, j); int left = minPathSumDFSMem(grid, mem, i, j - 1); - // Record and return the minimum path cost from the top-left to (i, j) + // Record and return the minimum path cost from top-left to (i, j) mem[i][j] = min(left, up) != INT_MAX ? min(left, up) + grid[i][j] : INT_MAX; return mem[i][j]; } @@ -305,24 +450,24 @@ We introduce a memo list `mem` of the same size as the grid `grid`, used to reco === "Java" ```java title="min_path_sum.java" - /* Minimum path sum: Memoized search */ + /* Minimum path sum: Memoization search */ int minPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { // If it's the top-left cell, terminate the search if (i == 0 && j == 0) { return grid[0][0]; } - // If the row or column index is out of bounds, return a +∞ cost + // If row or column index is out of bounds, return +∞ cost if (i < 0 || j < 0) { return Integer.MAX_VALUE; } - // If there is a record, return it + // If there's a record, return it directly if (mem[i][j] != -1) { return mem[i][j]; } - // The minimum path cost from the left and top cells + // Minimum path cost for left and upper cells int up = minPathSumDFSMem(grid, mem, i - 1, j); int left = minPathSumDFSMem(grid, mem, i, j - 1); - // Record and return the minimum path cost from the top-left to (i, j) + // Record and return the minimum path cost from top-left to (i, j) mem[i][j] = Math.min(left, up) + grid[i][j]; return mem[i][j]; } @@ -331,78 +476,276 @@ We introduce a memo list `mem` of the same size as the grid `grid`, used to reco === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDFSMem} + /* Minimum path sum: Memoization search */ + int MinPathSumDFSMem(int[][] grid, int[][] mem, int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return int.MaxValue; + } + // If there's a record, return it directly + if (mem[i][j] != -1) { + return mem[i][j]; + } + // Minimum path cost for left and upper cells + int up = MinPathSumDFSMem(grid, mem, i - 1, j); + int left = MinPathSumDFSMem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = Math.Min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Go" ```go title="min_path_sum.go" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + func minPathSumDFSMem(grid, mem [][]int, i, j int) int { + // If it's the top-left cell, terminate the search + if i == 0 && j == 0 { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return math.MaxInt + } + // If there's a record, return it directly + if mem[i][j] != -1 { + return mem[i][j] + } + // Minimum path cost for left and upper cells + up := minPathSumDFSMem(grid, mem, i-1, j) + left := minPathSumDFSMem(grid, mem, i, j-1) + // Record and return the minimum path cost from top-left to (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" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + func minPathSumDFSMem(grid: [[Int]], mem: inout [[Int]], i: Int, j: Int) -> Int { + // If it's the top-left cell, terminate the search + if i == 0, j == 0 { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return .max + } + // If there's a record, return it directly + if mem[i][j] != -1 { + return mem[i][j] + } + // Minimum path cost for left and upper cells + let up = minPathSumDFSMem(grid: grid, mem: &mem, i: i - 1, j: j) + let left = minPathSumDFSMem(grid: grid, mem: &mem, i: i, j: j - 1) + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + function minPathSumDFSMem(grid, mem, i, j) { + // If it's the top-left cell, terminate the search + if (i === 0 && j === 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Infinity; + } + // If there's a record, return it directly + if (mem[i][j] !== -1) { + return mem[i][j]; + } + // Minimum path cost for left and upper cells + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "TS" ```typescript title="min_path_sum.ts" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + function minPathSumDFSMem( + grid: Array>, + mem: Array>, + i: number, + j: number + ): number { + // If it's the top-left cell, terminate the search + if (i === 0 && j === 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Infinity; + } + // If there's a record, return it directly + if (mem[i][j] != -1) { + return mem[i][j]; + } + // Minimum path cost for left and upper cells + const up = minPathSumDFSMem(grid, mem, i - 1, j); + const left = minPathSumDFSMem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = Math.min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Dart" ```dart title="min_path_sum.dart" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + int minPathSumDFSMem(List> grid, List> mem, int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + // In Dart, int type is fixed-range integer, no value representing "infinity" + return BigInt.from(2).pow(31).toInt(); + } + // If there's a record, return it directly + if (mem[i][j] != -1) { + return mem[i][j]; + } + // Minimum path cost for left and upper cells + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = min(left, up) + grid[i][j]; + return mem[i][j]; + } ``` === "Rust" ```rust title="min_path_sum.rs" - [class]{}-[func]{min_path_sum_dfs_mem} + /* Minimum path sum: Memoization search */ + fn min_path_sum_dfs_mem(grid: &Vec>, mem: &mut Vec>, i: i32, j: i32) -> i32 { + // If it's the top-left cell, terminate the search + if i == 0 && j == 0 { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if i < 0 || j < 0 { + return i32::MAX; + } + // If there's a record, return it directly + if mem[i as usize][j as usize] != -1 { + return mem[i as usize][j as usize]; + } + // Minimum path cost for left and upper cells + let up = min_path_sum_dfs_mem(grid, mem, i - 1, j); + let left = min_path_sum_dfs_mem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (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} + /* Minimum path sum: Memoization search */ + int minPathSumDFSMem(int grid[MAX_SIZE][MAX_SIZE], int mem[MAX_SIZE][MAX_SIZE], int i, int j) { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0]; + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return INT_MAX; + } + // If there's a record, return it directly + if (mem[i][j] != -1) { + return mem[i][j]; + } + // Minimum path cost for left and upper cells + int up = minPathSumDFSMem(grid, mem, i - 1, j); + int left = minPathSumDFSMem(grid, mem, i, j - 1); + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = myMin(left, up) != INT_MAX ? myMin(left, up) + grid[i][j] : INT_MAX; + return mem[i][j]; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDFSMem} + /* Minimum path sum: Memoization search */ + fun minPathSumDFSMem( + grid: Array, + mem: Array, + i: Int, + j: Int + ): Int { + // If it's the top-left cell, terminate the search + if (i == 0 && j == 0) { + return grid[0][0] + } + // If row or column index is out of bounds, return +∞ cost + if (i < 0 || j < 0) { + return Int.MAX_VALUE + } + // If there's a record, return it directly + if (mem[i][j] != -1) { + return mem[i][j] + } + // Minimum path cost for left and upper cells + val up = minPathSumDFSMem(grid, mem, i - 1, j) + val left = minPathSumDFSMem(grid, mem, i, j - 1) + // Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = min(left, up) + grid[i][j] + return mem[i][j] + } ``` === "Ruby" ```ruby title="min_path_sum.rb" - [class]{}-[func]{min_path_sum_dfs_mem} + ### Minimum path sum: memoization search ### + def min_path_sum_dfs_mem(grid, mem, i, j) + # If it's the top-left cell, terminate the search + return grid[0][0] if i == 0 && j == 0 + # If row or column index is out of bounds, return +∞ cost + return Float::INFINITY if i < 0 || j < 0 + # If there's a record, return it directly + return mem[i][j] if mem[i][j] != -1 + # Minimum path cost for left and upper cells + up = min_path_sum_dfs_mem(grid, mem, i - 1, j) + left = min_path_sum_dfs_mem(grid, mem, i, j - 1) + # Record and return the minimum path cost from top-left to (i, j) + mem[i][j] = [left, up].min + grid[i][j] + end ``` -=== "Zig" +As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be computed once, so the time complexity depends on the total number of states, which is the grid size $O(nm)$. - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDFSMem} - ``` +![Memoization recursion tree](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" } -As shown in Figure 14-15, after introducing memoization, all subproblem solutions only need to be calculated once, so the time complexity depends on the total number of states, i.e., the grid size $O(nm)$. +

Figure 14-15   Memoization recursion tree

-![Memoized search recursive tree](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" } +### 3.   Method 3: Dynamic Programming -

Figure 14-15   Memoized search recursive tree

- -### 3.   Method 3: Dynamic programming - -Implement the dynamic programming solution iteratively, code as shown below: +Implement the dynamic programming solution based on iteration, as shown in the code below: === "Python" @@ -419,7 +762,7 @@ Implement the dynamic programming solution iteratively, code as shown below: # State transition: first column for i in range(1, n): dp[i][0] = dp[i - 1][0] + grid[i][0] - # State transition: the rest of the rows and columns + # State transition: rest of the rows and columns 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] @@ -443,7 +786,7 @@ Implement the dynamic programming solution iteratively, code as shown below: for (int i = 1; i < n; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns 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]; @@ -470,7 +813,7 @@ Implement the dynamic programming solution iteratively, code as shown below: for (int i = 1; i < n; i++) { dp[i][0] = dp[i - 1][0] + grid[i][0]; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns 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]; @@ -483,75 +826,293 @@ Implement the dynamic programming solution iteratively, code as shown below: === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDP} + /* Minimum path sum: Dynamic programming */ + int MinPathSumDP(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // Initialize dp table + int[,] dp = new int[n, m]; + dp[0, 0] = grid[0][0]; + // State transition: first row + for (int j = 1; j < m; j++) { + dp[0, j] = dp[0, j - 1] + grid[0][j]; + } + // State transition: first column + for (int i = 1; i < n; i++) { + dp[i, 0] = dp[i - 1, 0] + grid[i][0]; + } + // State transition: rest of the rows and columns + 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" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + func minPathSumDP(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // Initialize dp table + dp := make([][]int, n) + for i := 0; i < n; i++ { + dp[i] = make([]int, m) + } + dp[0][0] = grid[0][0] + // State transition: first row + for j := 1; j < m; j++ { + dp[0][j] = dp[0][j-1] + grid[0][j] + } + // State transition: first column + for i := 1; i < n; i++ { + dp[i][0] = dp[i-1][0] + grid[i][0] + } + // State transition: rest of the rows and columns + 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" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + func minPathSumDP(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // Initialize dp table + var dp = Array(repeating: Array(repeating: 0, count: m), count: n) + dp[0][0] = grid[0][0] + // State transition: first row + for j in 1 ..< m { + dp[0][j] = dp[0][j - 1] + grid[0][j] + } + // State transition: first column + for i in 1 ..< n { + dp[i][0] = dp[i - 1][0] + grid[i][0] + } + // State transition: rest of the rows and columns + for i in 1 ..< n { + for j in 1 ..< m { + dp[i][j] = min(dp[i][j - 1], dp[i - 1][j]) + grid[i][j] + } + } + return dp[n - 1][m - 1] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + function minPathSumDP(grid) { + const n = grid.length, + m = grid[0].length; + // Initialize dp table + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // State transition: first row + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // State transition: first column + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // State transition: rest of the rows and columns + 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" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + function minPathSumDP(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // Initialize dp table + const dp = Array.from({ length: n }, () => + Array.from({ length: m }, () => 0) + ); + dp[0][0] = grid[0][0]; + // State transition: first row + for (let j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // State transition: first column + for (let i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // State transition: rest of the rows and columns + 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" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + int minPathSumDP(List> grid) { + int n = grid.length, m = grid[0].length; + // Initialize dp table + List> dp = List.generate(n, (i) => List.filled(m, 0)); + dp[0][0] = grid[0][0]; + // State transition: first row + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // State transition: first column + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // State transition: rest of the rows and columns + 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" - [class]{}-[func]{min_path_sum_dp} + /* Minimum path sum: Dynamic programming */ + fn min_path_sum_dp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // Initialize dp table + let mut dp = vec![vec![0; m]; n]; + dp[0][0] = grid[0][0]; + // State transition: first row + for j in 1..m { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // State transition: first column + for i in 1..n { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // State transition: rest of the rows and columns + 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} + /* Minimum path sum: Dynamic programming */ + int minPathSumDP(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // Initialize dp table + int **dp = malloc(n * sizeof(int *)); + for (int i = 0; i < n; i++) { + dp[i] = calloc(m, sizeof(int)); + } + dp[0][0] = grid[0][0]; + // State transition: first row + for (int j = 1; j < m; j++) { + dp[0][j] = dp[0][j - 1] + grid[0][j]; + } + // State transition: first column + for (int i = 1; i < n; i++) { + dp[i][0] = dp[i - 1][0] + grid[i][0]; + } + // State transition: rest of the rows and columns + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + dp[i][j] = myMin(dp[i][j - 1], dp[i - 1][j]) + grid[i][j]; + } + } + int res = dp[n - 1][m - 1]; + // Free memory + for (int i = 0; i < n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDP} + /* Minimum path sum: Dynamic programming */ + fun minPathSumDP(grid: Array): Int { + val n = grid.size + val m = grid[0].size + // Initialize dp table + val dp = Array(n) { IntArray(m) } + dp[0][0] = grid[0][0] + // State transition: first row + for (j in 1.." - ![Dynamic programming process of minimum path sum](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" } + ![Dynamic programming process for minimum path sum](dp_solution_pipeline.assets/min_path_sum_dp_step1.png){ class="animation-figure" } === "<2>" ![min_path_sum_dp_step2](dp_solution_pipeline.assets/min_path_sum_dp_step2.png){ class="animation-figure" } @@ -586,13 +1147,13 @@ The array `dp` is of size $n \times m$, **therefore the space complexity is $O(n === "<12>" ![min_path_sum_dp_step12](dp_solution_pipeline.assets/min_path_sum_dp_step12.png){ class="animation-figure" } -

Figure 14-16   Dynamic programming process of minimum path sum

+

Figure 14-16   Dynamic programming process for minimum path sum

-### 4.   Space optimization +### 4.   Space Optimization -Since each cell is only related to the cell to its left and above, we can use a single-row array to implement the $dp$ table. +Since each cell is only related to the cell to its left and the cell above it, we can use a single-row array to implement the $dp$ table. -Please note, since the array `dp` can only represent the state of one row, we cannot initialize the first column state in advance, but update it as we traverse each row: +Note that since the array `dp` can only represent the state of one row, we cannot initialize the first column state in advance, but rather update it when traversing each row: === "Python" @@ -606,11 +1167,11 @@ Please note, since the array `dp` can only represent the state of one row, we ca dp[0] = grid[0][0] for j in range(1, m): dp[j] = dp[j - 1] + grid[0][j] - # State transition: the rest of the rows + # State transition: rest of the rows for i in range(1, n): # State transition: first column dp[0] = dp[0] + grid[i][0] - # State transition: the rest of the columns + # State transition: rest of the columns for j in range(1, m): dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] return dp[m - 1] @@ -629,11 +1190,11 @@ Please note, since the array `dp` can only represent the state of one row, we ca for (int j = 1; j < m; j++) { dp[j] = dp[j - 1] + grid[0][j]; } - // State transition: the rest of the rows + // State transition: rest of the rows for (int i = 1; i < n; i++) { // State transition: first column dp[0] = dp[0] + grid[i][0]; - // State transition: the rest of the columns + // State transition: rest of the columns for (int j = 1; j < m; j++) { dp[j] = min(dp[j - 1], dp[j]) + grid[i][j]; } @@ -655,11 +1216,11 @@ Please note, since the array `dp` can only represent the state of one row, we ca for (int j = 1; j < m; j++) { dp[j] = dp[j - 1] + grid[0][j]; } - // State transition: the rest of the rows + // State transition: rest of the rows for (int i = 1; i < n; i++) { // State transition: first column dp[0] = dp[0] + grid[i][0]; - // State transition: the rest of the columns + // State transition: rest of the columns for (int j = 1; j < m; j++) { dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j]; } @@ -671,65 +1232,260 @@ Please note, since the array `dp` can only represent the state of one row, we ca === "C#" ```csharp title="min_path_sum.cs" - [class]{min_path_sum}-[func]{MinPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + int MinPathSumDPComp(int[][] grid) { + int n = grid.Length, m = grid[0].Length; + // Initialize dp table + int[] dp = new int[m]; + dp[0] = grid[0][0]; + // State transition: first row + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // State transition: rest of the rows + for (int i = 1; i < n; i++) { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + 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" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + func minPathSumDPComp(grid [][]int) int { + n, m := len(grid), len(grid[0]) + // Initialize dp table + dp := make([]int, m) + // State transition: first row + dp[0] = grid[0][0] + for j := 1; j < m; j++ { + dp[j] = dp[j-1] + grid[0][j] + } + // State transition: rest of the rows and columns + for i := 1; i < n; i++ { + // State transition: first column + dp[0] = dp[0] + grid[i][0] + // State transition: rest of the columns + 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" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + func minPathSumDPComp(grid: [[Int]]) -> Int { + let n = grid.count + let m = grid[0].count + // Initialize dp table + var dp = Array(repeating: 0, count: m) + // State transition: first row + dp[0] = grid[0][0] + for j in 1 ..< m { + dp[j] = dp[j - 1] + grid[0][j] + } + // State transition: rest of the rows + for i in 1 ..< n { + // State transition: first column + dp[0] = dp[0] + grid[i][0] + // State transition: rest of the columns + for j in 1 ..< m { + dp[j] = min(dp[j - 1], dp[j]) + grid[i][j] + } + } + return dp[m - 1] + } ``` === "JS" ```javascript title="min_path_sum.js" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + function minPathSumDPComp(grid) { + const n = grid.length, + m = grid[0].length; + // Initialize dp table + const dp = new Array(m); + // State transition: first row + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // State transition: rest of the rows + for (let i = 1; i < n; i++) { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + 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" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + function minPathSumDPComp(grid: Array>): number { + const n = grid.length, + m = grid[0].length; + // Initialize dp table + const dp = new Array(m); + // State transition: first row + dp[0] = grid[0][0]; + for (let j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // State transition: rest of the rows + for (let i = 1; i < n; i++) { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + 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" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + int minPathSumDPComp(List> grid) { + int n = grid.length, m = grid[0].length; + // Initialize dp table + 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]; + } + // State transition: rest of the rows + for (int i = 1; i < n; i++) { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + 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" - [class]{}-[func]{min_path_sum_dp_comp} + /* Minimum path sum: Space-optimized dynamic programming */ + fn min_path_sum_dp_comp(grid: &Vec>) -> i32 { + let (n, m) = (grid.len(), grid[0].len()); + // Initialize dp table + let mut dp = vec![0; m]; + // State transition: first row + dp[0] = grid[0][0]; + for j in 1..m { + dp[j] = dp[j - 1] + grid[0][j]; + } + // State transition: rest of the rows + for i in 1..n { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + 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} + /* Minimum path sum: Space-optimized dynamic programming */ + int minPathSumDPComp(int grid[MAX_SIZE][MAX_SIZE], int n, int m) { + // Initialize dp table + int *dp = calloc(m, sizeof(int)); + // State transition: first row + dp[0] = grid[0][0]; + for (int j = 1; j < m; j++) { + dp[j] = dp[j - 1] + grid[0][j]; + } + // State transition: rest of the rows + for (int i = 1; i < n; i++) { + // State transition: first column + dp[0] = dp[0] + grid[i][0]; + // State transition: rest of the columns + for (int j = 1; j < m; j++) { + dp[j] = myMin(dp[j - 1], dp[j]) + grid[i][j]; + } + } + int res = dp[m - 1]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="min_path_sum.kt" - [class]{}-[func]{minPathSumDPComp} + /* Minimum path sum: Space-optimized dynamic programming */ + fun minPathSumDPComp(grid: Array): Int { + val n = grid.size + val m = grid[0].size + // Initialize dp table + val dp = IntArray(m) + // State transition: first row + dp[0] = grid[0][0] + for (j in 1.. Figure 14-27   Example data of edit distance

+

Figure 14-27   Example data for edit distance

-**The edit distance problem can naturally be explained with a decision tree model**. Strings correspond to tree nodes, and a round of decision (an edit operation) corresponds to an edge of the tree. +**The edit distance problem can be naturally explained using the decision tree model**. Strings correspond to tree nodes, and a round of decision (one edit operation) corresponds to an edge of the tree. -As shown in Figure 14-28, with unrestricted operations, each node can derive many edges, each corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`. +As shown in Figure 14-28, without restricting operations, each node can branch into many edges, with each edge corresponding to one operation, meaning there are many possible paths to transform `hello` into `algo`. -From the perspective of the decision tree, the goal of this problem is to find the shortest path between the node `hello` and the node `algo`. +From the perspective of the decision tree, the goal of this problem is to find the shortest path between node `hello` and node `algo`. -![Edit distance problem represented based on decision tree model](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" } +![Representing edit distance problem based on decision tree model](edit_distance_problem.assets/edit_distance_decision_tree.png){ class="animation-figure" } -

Figure 14-28   Edit distance problem represented based on decision tree model

+

Figure 14-28   Representing edit distance problem based on decision tree model

-### 1.   Dynamic programming approach +### 1.   Dynamic Programming Approach -**Step one: Think about each round of decision, define the state, thus obtaining the $dp$ table** +**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table** Each round of decision involves performing one edit operation on string $s$. -We aim to gradually reduce the problem size during the edit process, which enables us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$, respectively. We first consider the tail characters of both strings $s[n-1]$ and $t[m-1]$. +We want the problem scale to gradually decrease during the editing process, which allows us to construct subproblems. Let the lengths of strings $s$ and $t$ be $n$ and $m$ respectively. We first consider the tail characters of the two strings, $s[n-1]$ and $t[m-1]$. - If $s[n-1]$ and $t[m-1]$ are the same, we can skip them and directly consider $s[n-2]$ and $t[m-2]$. -- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, replace) so that the tail characters of the two strings match, allowing us to skip them and consider a smaller-scale problem. +- If $s[n-1]$ and $t[m-1]$ are different, we need to perform one edit on $s$ (insert, delete, or replace) to make the tail characters of the two strings the same, allowing us to skip them and consider a smaller-scale problem. -Thus, each round of decision (edit operation) in string $s$ changes the remaining characters in $s$ and $t$ to be matched. Therefore, the state is the $i$-th and $j$-th characters currently considered in $s$ and $t$, denoted as $[i, j]$. +In other words, each round of decision (edit operation) we make on string $s$ will change the remaining characters to be matched in $s$ and $t$. Therefore, the state is the $i$-th and $j$-th characters currently being considered in $s$ and $t$, denoted as $[i, j]$. -State $[i, j]$ corresponds to the subproblem: **The minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**. +State $[i, j]$ corresponds to the subproblem: **the minimum number of edits required to change the first $i$ characters of $s$ into the first $j$ characters of $t$**. From this, we obtain a two-dimensional $dp$ table of size $(i+1) \times (j+1)$. -**Step two: Identify the optimal substructure and then derive the state transition equation** +**Step 2: Identify the optimal substructure, and then derive the state transition equation** -Consider the subproblem $dp[i, j]$, whose corresponding tail characters of the two strings are $s[i-1]$ and $t[j-1]$, which can be divided into three scenarios as shown in Figure 14-29. +Consider subproblem $dp[i, j]$, where the tail characters of the corresponding two strings are $s[i-1]$ and $t[j-1]$, which can be divided into the three cases shown in Figure 14-29 based on different edit operations. -1. Add $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$. +1. Insert $t[j-1]$ after $s[i-1]$, then the remaining subproblem is $dp[i, j-1]$. 2. Delete $s[i-1]$, then the remaining subproblem is $dp[i-1, j]$. 3. Replace $s[i-1]$ with $t[j-1]$, then the remaining subproblem is $dp[i-1, j-1]$. -![State transition of edit distance](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" } +![State transition for edit distance](edit_distance_problem.assets/edit_distance_state_transfer.png){ class="animation-figure" } -

Figure 14-29   State transition of edit distance

+

Figure 14-29   State transition for edit distance

-Based on the analysis above, we can determine the optimal substructure: The minimum number of edits for $dp[i, j]$ is the minimum among $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$. The corresponding state transition equation is: +Based on the above analysis, the optimal substructure can be obtained: the minimum number of edits for $dp[i, j]$ equals the minimum among the minimum edit steps of $dp[i, j-1]$, $dp[i-1, j]$, and $dp[i-1, j-1]$, plus the edit step $1$ for this time. The corresponding state transition equation is: $$ dp[i, j] = \min(dp[i, j-1], dp[i-1, j], dp[i-1, j-1]) + 1 $$ -Please note, **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is: +Please note that **when $s[i-1]$ and $t[j-1]$ are the same, no edit is required for the current character**, in which case the state transition equation is: $$ dp[i, j] = dp[i-1, j-1] $$ -**Step three: Determine the boundary conditions and the order of state transitions** +**Step 3: Determine boundary conditions and state transition order** -When both strings are empty, the number of edits is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edits equals the length of $t$, that is, the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is, the minimum number of edits equals the length of $s$, that is, the first column $dp[i, 0] = i$. +When both strings are empty, the number of edit steps is $0$, i.e., $dp[0, 0] = 0$. When $s$ is empty but $t$ is not, the minimum number of edit steps equals the length of $t$, i.e., the first row $dp[0, j] = j$. When $s$ is not empty but $t$ is empty, the minimum number of edit steps equals the length of $s$, i.e., the first column $dp[i, 0] = i$. -Observing the state transition equation, solving $dp[i, j]$ depends on the solutions to the left, above, and upper left, so a double loop can be used to traverse the entire $dp$ table in the correct order. +Observing the state transition equation, the solution $dp[i, j]$ depends on solutions to the left, above, and upper-left, so the entire $dp$ table can be traversed in order through two nested loops. -### 2.   Code implementation +### 2.   Code Implementation === "Python" @@ -89,14 +89,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut dp[i][0] = i for j in range(1, m + 1): dp[0][j] = j - # State transition: the rest of the rows and columns + # State transition: rest of the rows and columns for i in range(1, n + 1): for j in range(1, m + 1): if s[i - 1] == t[j - 1]: - # If the two characters are equal, skip these two characters + # If two characters are equal, skip both characters dp[i][j] = dp[i - 1][j - 1] else: - # The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + # Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1 return dp[n][m] ``` @@ -115,14 +115,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut for (int j = 1; j <= m; j++) { dp[0][j] = j; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (s[i - 1] == t[j - 1]) { - // If the two characters are equal, skip these two characters + // If two characters are equal, skip both characters dp[i][j] = dp[i - 1][j - 1]; } else { - // The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; } } @@ -145,14 +145,14 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut for (int j = 1; j <= m; j++) { dp[0][j] = j; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (s.charAt(i - 1) == t.charAt(j - 1)) { - // If the two characters are equal, skip these two characters + // If two characters are equal, skip both characters dp[i][j] = dp[i - 1][j - 1]; } else { - // The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[i][j] = Math.min(Math.min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; } } @@ -164,73 +164,323 @@ Observing the state transition equation, solving $dp[i, j]$ depends on the solut === "C#" ```csharp title="edit_distance.cs" - [class]{edit_distance}-[func]{EditDistanceDP} + /* Edit distance: Dynamic programming */ + int EditDistanceDP(string s, string t) { + int n = s.Length, m = t.Length; + int[,] dp = new int[n + 1, m + 1]; + // State transition: first row and first column + for (int i = 1; i <= n; i++) { + dp[i, 0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0, j] = j; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[i, j] = dp[i - 1, j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 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" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + 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) + } + // State transition: first row and first column + for i := 1; i <= n; i++ { + dp[i][0] = i + } + for j := 1; j <= m; j++ { + dp[0][j] = j + } + // State transition: rest of the rows and columns + for i := 1; i <= n; i++ { + for j := 1; j <= m; j++ { + if s[i-1] == t[j-1] { + // If two characters are equal, skip both characters + dp[i][j] = dp[i-1][j-1] + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 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" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + 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) + // State transition: first row and first column + for i in 1 ... n { + dp[i][0] = i + } + for j in 1 ... m { + dp[0][j] = j + } + // State transition: rest of the rows and columns + for i in 1 ... n { + for j in 1 ... m { + if s.utf8CString[i - 1] == t.utf8CString[j - 1] { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1] + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 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" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + function editDistanceDP(s, t) { + const n = s.length, + m = t.length; + const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0)); + // State transition: first row and first column + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // State transition: rest of the rows and columns + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[i][j] = + Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + 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) + ); + // State transition: first row and first column + for (let i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= m; j++) { + dp[0][j] = j; + } + // State transition: rest of the rows and columns + for (let i = 1; i <= n; i++) { + for (let j = 1; j <= m; j++) { + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[i][j] = + Math.min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1; + } + } + } + return dp[n][m]; + } ``` === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + int editDistanceDP(String s, String t) { + int n = s.length, m = t.length; + List> dp = List.generate(n + 1, (_) => List.filled(m + 1, 0)); + // State transition: first row and first column + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 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" - [class]{}-[func]{edit_distance_dp} + /* Edit distance: Dynamic programming */ + 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]; + // State transition: first row and first column + for i in 1..=n { + dp[i][0] = i as i32; + } + for j in 1..m { + dp[0][j] = j as i32; + } + // State transition: rest of the rows and columns + for i in 1..=n { + for j in 1..=m { + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 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} + /* Edit distance: Dynamic programming */ + int editDistanceDP(char *s, char *t, int n, int m) { + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(m + 1, sizeof(int)); + } + // State transition: first row and first column + for (int i = 1; i <= n; i++) { + dp[i][0] = i; + } + for (int j = 1; j <= m; j++) { + dp[0][j] = j; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1]; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[i][j] = myMin(myMin(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; + } + } + } + int res = dp[n][m]; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="edit_distance.kt" - [class]{}-[func]{editDistanceDP} + /* Edit distance: Dynamic programming */ + fun editDistanceDP(s: String, t: String): Int { + val n = s.length + val m = t.length + val dp = Array(n + 1) { IntArray(m + 1) } + // State transition: first row and first column + for (i in 1..n) { + dp[i][0] = i + } + for (j in 1..m) { + dp[0][j] = j + } + // State transition: rest of the rows and columns + for (i in 1..n) { + for (j in 1..m) { + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1] + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[i][j] = min(min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1 + } + } + } + return dp[n][m] + } ``` === "Ruby" ```ruby title="edit_distance.rb" - [class]{}-[func]{edit_distance_dp} + ### Edit distance: dynamic programming ### + def edit_distance_dp(s, t) + n, m = s.length, t.length + dp = Array.new(n + 1) { Array.new(m + 1, 0) } + # State transition: first row and first column + (1...(n + 1)).each { |i| dp[i][0] = i } + (1...(m + 1)).each { |j| dp[0][j] = j } + # State transition: rest of the rows and columns + for i in 1...(n + 1) + for j in 1...(m +1) + if s[i - 1] == t[j - 1] + # If two characters are equal, skip both characters + dp[i][j] = dp[i - 1][j - 1] + else + # Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[i][j] = [dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]].min + 1 + end + end + end + dp[n][m] + end ``` -=== "Zig" - - ```zig title="edit_distance.zig" - [class]{}-[func]{editDistanceDP} - ``` - -As shown in Figure 14-30, the process of state transition in the edit distance problem is very similar to that in the knapsack problem, which can be seen as filling a two-dimensional grid. +As shown in Figure 14-30, the state transition process for the edit distance problem is very similar to the knapsack problem and can both be viewed as the process of filling a two-dimensional grid. === "<1>" - ![Dynamic programming process of edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } + ![Dynamic programming process for edit distance](edit_distance_problem.assets/edit_distance_dp_step1.png){ class="animation-figure" } === "<2>" ![edit_distance_dp_step2](edit_distance_problem.assets/edit_distance_dp_step2.png){ class="animation-figure" } @@ -274,13 +524,13 @@ As shown in Figure 14-30, the process of state transition in the edit distance p === "<15>" ![edit_distance_dp_step15](edit_distance_problem.assets/edit_distance_dp_step15.png){ class="animation-figure" } -

Figure 14-30   Dynamic programming process of edit distance

+

Figure 14-30   Dynamic programming process for edit distance

-### 3.   Space optimization +### 3.   Space Optimization -Since $dp[i, j]$ is derived from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper left $dp[i-1, j-1]$, and direct traversal will lose the upper left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, therefore, both traversal orders are not feasible. +Since $dp[i, j]$ is transferred from the solutions above $dp[i-1, j]$, to the left $dp[i, j-1]$, and to the upper-left $dp[i-1, j-1]$, forward traversal will lose the upper-left solution $dp[i-1, j-1]$, and reverse traversal cannot build $dp[i, j-1]$ in advance, so neither traversal order is feasible. -For this reason, we can use a variable `leftup` to temporarily store the solution from the upper left $dp[i-1, j-1]$, thus only needing to consider the solutions to the left and above. This situation is similar to the unbounded knapsack problem, allowing for direct traversal. The code is as follows: +For this reason, we can use a variable `leftup` to temporarily store the upper-left solution $dp[i-1, j-1]$, so we only need to consider the solutions to the left and above. This situation is the same as the unbounded knapsack problem, allowing for forward traversal. The code is as follows: === "Python" @@ -292,21 +542,21 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio # State transition: first row for j in range(1, m + 1): dp[j] = j - # State transition: the rest of the rows + # State transition: rest of the rows for i in range(1, n + 1): # State transition: first column leftup = dp[0] # Temporarily store dp[i-1, j-1] dp[0] += 1 - # State transition: the rest of the columns + # State transition: rest of the columns for j in range(1, m + 1): temp = dp[j] if s[i - 1] == t[j - 1]: - # If the two characters are equal, skip these two characters + # If two characters are equal, skip both characters dp[j] = leftup else: - # The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + # Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[j] = min(dp[j - 1], dp[j], leftup) + 1 - leftup = temp # Update for the next round of dp[i-1, j-1] + leftup = temp # Update for next round's dp[i-1, j-1] return dp[m] ``` @@ -321,22 +571,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio for (int j = 1; j <= m; j++) { dp[j] = j; } - // State transition: the rest of the rows + // State transition: rest of the rows for (int i = 1; i <= n; i++) { // State transition: first column int leftup = dp[0]; // Temporarily store dp[i-1, j-1] dp[0] = i; - // State transition: the rest of the columns + // State transition: rest of the columns for (int j = 1; j <= m; j++) { int temp = dp[j]; if (s[i - 1] == t[j - 1]) { - // If the two characters are equal, skip these two characters + // If two characters are equal, skip both characters dp[j] = leftup; } else { - // The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; } - leftup = temp; // Update for the next round of dp[i-1, j-1] + leftup = temp; // Update for next round's dp[i-1, j-1] } } return dp[m]; @@ -354,22 +604,22 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio for (int j = 1; j <= m; j++) { dp[j] = j; } - // State transition: the rest of the rows + // State transition: rest of the rows for (int i = 1; i <= n; i++) { // State transition: first column int leftup = dp[0]; // Temporarily store dp[i-1, j-1] dp[0] = i; - // State transition: the rest of the columns + // State transition: rest of the columns for (int j = 1; j <= m; j++) { int temp = dp[j]; if (s.charAt(i - 1) == t.charAt(j - 1)) { - // If the two characters are equal, skip these two characters + // If two characters are equal, skip both characters dp[j] = leftup; } else { - // The minimum number of edits = the minimum number of edits from three operations (insert, remove, replace) + 1 + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 dp[j] = Math.min(Math.min(dp[j - 1], dp[j]), leftup) + 1; } - leftup = temp; // Update for the next round of dp[i-1, j-1] + leftup = temp; // Update for next round's dp[i-1, j-1] } } return dp[m]; @@ -379,65 +629,334 @@ For this reason, we can use a variable `leftup` to temporarily store the solutio === "C#" ```csharp title="edit_distance.cs" - [class]{edit_distance}-[func]{EditDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + int EditDistanceDPComp(string s, string t) { + int n = s.Length, m = t.Length; + int[] dp = new int[m + 1]; + // State transition: first row + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // State transition: rest of the rows + for (int i = 1; i <= n; i++) { + // State transition: first column + int leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i; + // State transition: rest of the columns + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = Math.Min(Math.Min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "Go" ```go title="edit_distance.go" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + func editDistanceDPComp(s string, t string) int { + n := len(s) + m := len(t) + dp := make([]int, m+1) + // State transition: first row + for j := 1; j <= m; j++ { + dp[j] = j + } + // State transition: rest of the rows + for i := 1; i <= n; i++ { + // State transition: first column + leftUp := dp[0] // Temporarily store dp[i-1, j-1] + dp[0] = i + // State transition: rest of the columns + for j := 1; j <= m; j++ { + temp := dp[j] + if s[i-1] == t[j-1] { + // If two characters are equal, skip both characters + dp[j] = leftUp + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = MinInt(MinInt(dp[j-1], dp[j]), leftUp) + 1 + } + leftUp = temp // Update for next round's dp[i-1, j-1] + } + } + return dp[m] + } ``` === "Swift" ```swift title="edit_distance.swift" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + 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) + // State transition: first row + for j in 1 ... m { + dp[j] = j + } + // State transition: rest of the rows + for i in 1 ... n { + // State transition: first column + var leftup = dp[0] // Temporarily store dp[i-1, j-1] + dp[0] = i + // State transition: rest of the columns + for j in 1 ... m { + let temp = dp[j] + if s.utf8CString[i - 1] == t.utf8CString[j - 1] { + // If two characters are equal, skip both characters + dp[j] = leftup + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1 + } + leftup = temp // Update for next round's dp[i-1, j-1] + } + } + return dp[m] + } ``` === "JS" ```javascript title="edit_distance.js" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + function editDistanceDPComp(s, t) { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // State transition: first row + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // State transition: rest of the rows + for (let i = 1; i <= n; i++) { + // State transition: first column + let leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i; + // State transition: rest of the columns + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "TS" ```typescript title="edit_distance.ts" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + function editDistanceDPComp(s: string, t: string): number { + const n = s.length, + m = t.length; + const dp = new Array(m + 1).fill(0); + // State transition: first row + for (let j = 1; j <= m; j++) { + dp[j] = j; + } + // State transition: rest of the rows + for (let i = 1; i <= n; i++) { + // State transition: first column + let leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i; + // State transition: rest of the columns + for (let j = 1; j <= m; j++) { + const temp = dp[j]; + if (s.charAt(i - 1) === t.charAt(j - 1)) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = Math.min(dp[j - 1], dp[j], leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "Dart" ```dart title="edit_distance.dart" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + int editDistanceDPComp(String s, String t) { + int n = s.length, m = t.length; + List dp = List.filled(m + 1, 0); + // State transition: first row + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // State transition: rest of the rows + for (int i = 1; i <= n; i++) { + // State transition: first column + int leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i; + // State transition: rest of the columns + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + return dp[m]; + } ``` === "Rust" ```rust title="edit_distance.rs" - [class]{}-[func]{edit_distance_dp_comp} + /* Edit distance: Space-optimized dynamic programming */ + fn edit_distance_dp_comp(s: &str, t: &str) -> i32 { + let (n, m) = (s.len(), t.len()); + let mut dp = vec![0; m + 1]; + // State transition: first row + for j in 1..m { + dp[j] = j as i32; + } + // State transition: rest of the rows + for i in 1..=n { + // State transition: first column + let mut leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i as i32; + // State transition: rest of the columns + for j in 1..=m { + let temp = dp[j]; + if s.chars().nth(i - 1) == t.chars().nth(j - 1) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = std::cmp::min(std::cmp::min(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + dp[m] + } ``` === "C" ```c title="edit_distance.c" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + int editDistanceDPComp(char *s, char *t, int n, int m) { + int *dp = calloc(m + 1, sizeof(int)); + // State transition: first row + for (int j = 1; j <= m; j++) { + dp[j] = j; + } + // State transition: rest of the rows + for (int i = 1; i <= n; i++) { + // State transition: first column + int leftup = dp[0]; // Temporarily store dp[i-1, j-1] + dp[0] = i; + // State transition: rest of the columns + for (int j = 1; j <= m; j++) { + int temp = dp[j]; + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[j] = leftup; + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = myMin(myMin(dp[j - 1], dp[j]), leftup) + 1; + } + leftup = temp; // Update for next round's dp[i-1, j-1] + } + } + int res = dp[m]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="edit_distance.kt" - [class]{}-[func]{editDistanceDPComp} + /* Edit distance: Space-optimized dynamic programming */ + fun editDistanceDPComp(s: String, t: String): Int { + val n = s.length + val m = t.length + val dp = IntArray(m + 1) + // State transition: first row + for (j in 1..m) { + dp[j] = j + } + // State transition: rest of the rows + for (i in 1..n) { + // State transition: first column + var leftup = dp[0] // Temporarily store dp[i-1, j-1] + dp[0] = i + // State transition: rest of the columns + for (j in 1..m) { + val temp = dp[j] + if (s[i - 1] == t[j - 1]) { + // If two characters are equal, skip both characters + dp[j] = leftup + } else { + // Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = min(min(dp[j - 1], dp[j]), leftup) + 1 + } + leftup = temp // Update for next round's dp[i-1, j-1] + } + } + return dp[m] + } ``` === "Ruby" ```ruby title="edit_distance.rb" - [class]{}-[func]{edit_distance_dp_comp} - ``` - -=== "Zig" - - ```zig title="edit_distance.zig" - [class]{}-[func]{editDistanceDPComp} + ### Edit distance: space-optimized DP ### + def edit_distance_dp_comp(s, t) + n, m = s.length, t.length + dp = Array.new(m + 1, 0) + # State transition: first row + (1...(m + 1)).each { |j| dp[j] = j } + # State transition: rest of the rows + for i in 1...(n + 1) + # State transition: first column + leftup = dp.first # Temporarily store dp[i-1, j-1] + dp[0] += 1 + # State transition: rest of the columns + for j in 1...(m + 1) + temp = dp[j] + if s[i - 1] == t[j - 1] + # If two characters are equal, skip both characters + dp[j] = leftup + else + # Minimum edit steps = minimum edit steps of insert, delete, replace + 1 + dp[j] = [dp[j - 1], dp[j], leftup].min + 1 + end + leftup = temp # Update for next round's dp[i-1, j-1] + end + end + dp[m] + end ``` diff --git a/en/docs/chapter_dynamic_programming/index.md b/en/docs/chapter_dynamic_programming/index.md index 5303c09b5..cf051aea8 100644 --- a/en/docs/chapter_dynamic_programming/index.md +++ b/en/docs/chapter_dynamic_programming/index.md @@ -3,22 +3,22 @@ comments: true icon: material/table-pivot --- -# Chapter 14.   Dynamic programming +# Chapter 14.   Dynamic Programming ![Dynamic programming](../assets/covers/chapter_dynamic_programming.jpg){ class="cover-image" } !!! abstract - Streams merge into rivers, and rivers merge into the sea. - - Dynamic programming weaves smaller problems’ solutions into larger ones, guiding us step by step toward the far shore—where the ultimate answer awaits. + Streams converge into rivers, rivers converge into the sea. + + Dynamic programming gathers solutions to small problems into answers to large problems, step by step guiding us to the shore of problem-solving. ## Chapter contents -- [14.1   Introduction to dynamic programming](intro_to_dynamic_programming.md) -- [14.2   Characteristics of DP problems](dp_problem_features.md) -- [14.3   DP problem-solving approach¶](dp_solution_pipeline.md) -- [14.4   0-1 Knapsack problem](knapsack_problem.md) -- [14.5   Unbounded knapsack problem](unbounded_knapsack_problem.md) -- [14.6   Edit distance problem](edit_distance_problem.md) +- [14.1   Introduction to Dynamic Programming](intro_to_dynamic_programming.md) +- [14.2   Characteristics of Dynamic Programming Problems](dp_problem_features.md) +- [14.3   Dynamic Programming Problem-Solving Approach](dp_solution_pipeline.md) +- [14.4   0-1 Knapsack Problem](knapsack_problem.md) +- [14.5   Unbounded Knapsack Problem](unbounded_knapsack_problem.md) +- [14.6   Edit Distance Problem](edit_distance_problem.md) - [14.7   Summary](summary.md) diff --git a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 1acf006e5..9d4d1900c 100644 --- a/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/en/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -2,46 +2,46 @@ comments: true --- -# 14.1   Introduction to dynamic programming +# 14.1   Introduction to Dynamic Programming -Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems, and stores the solutions of these subproblems to avoid redundant computations, thereby significantly improving time efficiency. +Dynamic programming is an important algorithmic paradigm that decomposes a problem into a series of smaller subproblems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving time efficiency. -In this section, we start with a classic problem, first presenting its brute force backtracking solution, identifying the overlapping subproblems, and then gradually deriving a more efficient dynamic programming solution. +In this section, we start with a classic example, first presenting its brute force backtracking solution, observing the overlapping subproblems within it, and then gradually deriving a more efficient dynamic programming solution. !!! question "Climbing stairs" Given a staircase with $n$ steps, where you can climb $1$ or $2$ steps at a time, how many different ways are there to reach the top? -As shown in Figure 14-1, there are $3$ ways to reach the top of a $3$-step staircase. +As shown in Figure 14-1, for a $3$-step staircase, there are $3$ different ways to reach the top. ![Number of ways to reach the 3rd step](intro_to_dynamic_programming.assets/climbing_stairs_example.png){ class="animation-figure" }

Figure 14-1   Number of ways to reach the 3rd step

-This problem aims to calculate the number of ways by **using backtracking to exhaust all possibilities**. Specifically, it considers the problem of climbing stairs as a multi-round choice process: starting from the ground, choosing to move up either $1$ or $2$ steps each round, incrementing the count of ways upon reaching the top of the stairs, and pruning the process when it exceeds the top. The code is as follows: +The goal of this problem is to find the number of ways, **we can consider using backtracking to enumerate all possibilities**. Specifically, imagine climbing stairs as a multi-round selection process: starting from the ground, choosing to go up $1$ or $2$ steps in each round, incrementing the count by $1$ whenever the top of the stairs is reached, and pruning when exceeding the top. The code is as follows: === "Python" ```python title="climbing_stairs_backtrack.py" def backtrack(choices: list[int], state: int, n: int, res: list[int]) -> int: """Backtracking""" - # When climbing to the nth step, add 1 to the number of solutions + # When climbing to the n-th stair, add 1 to the solution count if state == n: res[0] += 1 # Traverse all choices for choice in choices: - # Pruning: do not allow climbing beyond the nth step + # Pruning: not allowed to go beyond the n-th stair if state + choice > n: continue - # Attempt: make a choice, update the state + # Attempt: make a choice, update state backtrack(choices, state + choice, n, res) - # Retract + # Backtrack def climbing_stairs_backtrack(n: int) -> int: """Climbing stairs: Backtracking""" - choices = [1, 2] # Can choose to climb up 1 step or 2 steps - state = 0 # Start climbing from the 0th step - res = [0] # Use res[0] to record the number of solutions + choices = [1, 2] # Can choose to climb up 1 or 2 stairs + state = 0 # Start climbing from the 0-th stair + res = [0] # Use res[0] to record the solution count backtrack(choices, state, n, res) return res[0] ``` @@ -51,25 +51,25 @@ This problem aims to calculate the number of ways by **using backtracking to exh ```cpp title="climbing_stairs_backtrack.cpp" /* Backtracking */ void backtrack(vector &choices, int state, int n, vector &res) { - // When climbing to the nth step, add 1 to the number of solutions + // When climbing to the n-th stair, add 1 to the solution count if (state == n) res[0]++; // Traverse all choices for (auto &choice : choices) { - // Pruning: do not allow climbing beyond the nth step + // Pruning: not allowed to go beyond the n-th stair if (state + choice > n) continue; - // Attempt: make a choice, update the state + // Attempt: make choice, update state backtrack(choices, state + choice, n, res); - // Retract + // Backtrack } } /* Climbing stairs: Backtracking */ int climbingStairsBacktrack(int n) { - vector choices = {1, 2}; // Can choose to climb up 1 step or 2 steps - int state = 0; // Start climbing from the 0th step - vector res = {0}; // Use res[0] to record the number of solutions + vector choices = {1, 2}; // Can choose to climb up 1 or 2 stairs + int state = 0; // Start climbing from the 0-th stair + vector res = {0}; // Use res[0] to record the solution count backtrack(choices, state, n, res); return res[0]; } @@ -80,26 +80,26 @@ This problem aims to calculate the number of ways by **using backtracking to exh ```java title="climbing_stairs_backtrack.java" /* Backtracking */ void backtrack(List choices, int state, int n, List res) { - // When climbing to the nth step, add 1 to the number of solutions + // When climbing to the n-th stair, add 1 to the solution count if (state == n) res.set(0, res.get(0) + 1); // Traverse all choices for (Integer choice : choices) { - // Pruning: do not allow climbing beyond the nth step + // Pruning: not allowed to go beyond the n-th stair if (state + choice > n) continue; - // Attempt: make a choice, update the state + // Attempt: make choice, update state backtrack(choices, state + choice, n, res); - // Retract + // Backtrack } } /* Climbing stairs: Backtracking */ int climbingStairsBacktrack(int n) { - List choices = Arrays.asList(1, 2); // Can choose to climb up 1 step or 2 steps - int state = 0; // Start climbing from the 0th step + List choices = Arrays.asList(1, 2); // Can choose to climb up 1 or 2 stairs + int state = 0; // Start climbing from the 0-th stair List res = new ArrayList<>(); - res.add(0); // Use res[0] to record the number of solutions + res.add(0); // Use res[0] to record the solution count backtrack(choices, state, n, res); return res.get(0); } @@ -108,116 +108,343 @@ This problem aims to calculate the number of ways by **using backtracking to exh === "C#" ```csharp title="climbing_stairs_backtrack.cs" - [class]{climbing_stairs_backtrack}-[func]{Backtrack} + /* Backtracking */ + void Backtrack(List choices, int state, int n, List res) { + // When climbing to the n-th stair, add 1 to the solution count + if (state == n) + res[0]++; + // Traverse all choices + foreach (int choice in choices) { + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) + continue; + // Attempt: make choice, update state + Backtrack(choices, state + choice, n, res); + // Backtrack + } + } - [class]{climbing_stairs_backtrack}-[func]{ClimbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + int ClimbingStairsBacktrack(int n) { + List choices = [1, 2]; // Can choose to climb up 1 or 2 stairs + int state = 0; // Start climbing from the 0-th stair + List res = [0]; // Use res[0] to record the solution count + Backtrack(choices, state, n, res); + return res[0]; + } ``` === "Go" ```go title="climbing_stairs_backtrack.go" - [class]{}-[func]{backtrack} + /* Backtracking */ + func backtrack(choices []int, state, n int, res []int) { + // When climbing to the n-th stair, add 1 to the solution count + if state == n { + res[0] = res[0] + 1 + } + // Traverse all choices + for _, choice := range choices { + // Pruning: not allowed to go beyond the n-th stair + if state+choice > n { + continue + } + // Attempt: make choice, update state + backtrack(choices, state+choice, n, res) + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + func climbingStairsBacktrack(n int) int { + // Can choose to climb up 1 or 2 stairs + choices := []int{1, 2} + // Start climbing from the 0-th stair + state := 0 + res := make([]int, 1) + // Use res[0] to record the solution count + res[0] = 0 + backtrack(choices, state, n, res) + return res[0] + } ``` === "Swift" ```swift title="climbing_stairs_backtrack.swift" - [class]{}-[func]{backtrack} + /* Backtracking */ + func backtrack(choices: [Int], state: Int, n: Int, res: inout [Int]) { + // When climbing to the n-th stair, add 1 to the solution count + if state == n { + res[0] += 1 + } + // Traverse all choices + for choice in choices { + // Pruning: not allowed to go beyond the n-th stair + if state + choice > n { + continue + } + // Attempt: make choice, update state + backtrack(choices: choices, state: state + choice, n: n, res: &res) + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + func climbingStairsBacktrack(n: Int) -> Int { + let choices = [1, 2] // Can choose to climb up 1 or 2 stairs + let state = 0 // Start climbing from the 0-th stair + var res: [Int] = [] + res.append(0) // Use res[0] to record the solution count + backtrack(choices: choices, state: state, n: n, res: &res) + return res[0] + } ``` === "JS" ```javascript title="climbing_stairs_backtrack.js" - [class]{}-[func]{backtrack} + /* Backtracking */ + function backtrack(choices, state, n, res) { + // When climbing to the n-th stair, add 1 to the solution count + if (state === n) res.set(0, res.get(0) + 1); + // Traverse all choices + for (const choice of choices) { + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) continue; + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res); + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + function climbingStairsBacktrack(n) { + const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs + const state = 0; // Start climbing from the 0-th stair + const res = new Map(); + res.set(0, 0); // Use res[0] to record the solution count + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "TS" ```typescript title="climbing_stairs_backtrack.ts" - [class]{}-[func]{backtrack} + /* Backtracking */ + function backtrack( + choices: number[], + state: number, + n: number, + res: Map<0, any> + ): void { + // When climbing to the n-th stair, add 1 to the solution count + if (state === n) res.set(0, res.get(0) + 1); + // Traverse all choices + for (const choice of choices) { + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) continue; + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res); + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + function climbingStairsBacktrack(n: number): number { + const choices = [1, 2]; // Can choose to climb up 1 or 2 stairs + const state = 0; // Start climbing from the 0-th stair + const res = new Map(); + res.set(0, 0); // Use res[0] to record the solution count + backtrack(choices, state, n, res); + return res.get(0); + } ``` === "Dart" ```dart title="climbing_stairs_backtrack.dart" - [class]{}-[func]{backtrack} + /* Backtracking */ + void backtrack(List choices, int state, int n, List res) { + // When climbing to the n-th stair, add 1 to the solution count + if (state == n) { + res[0]++; + } + // Traverse all choices + for (int choice in choices) { + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) continue; + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res); + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + int climbingStairsBacktrack(int n) { + List choices = [1, 2]; // Can choose to climb up 1 or 2 stairs + int state = 0; // Start climbing from the 0-th stair + List res = []; + res.add(0); // Use res[0] to record the solution count + backtrack(choices, state, n, res); + return res[0]; + } ``` === "Rust" ```rust title="climbing_stairs_backtrack.rs" - [class]{}-[func]{backtrack} + /* Backtracking */ + fn backtrack(choices: &[i32], state: i32, n: i32, res: &mut [i32]) { + // When climbing to the n-th stair, add 1 to the solution count + if state == n { + res[0] = res[0] + 1; + } + // Traverse all choices + for &choice in choices { + // Pruning: not allowed to go beyond the n-th stair + if state + choice > n { + continue; + } + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res); + // Backtrack + } + } - [class]{}-[func]{climbing_stairs_backtrack} + /* Climbing stairs: Backtracking */ + fn climbing_stairs_backtrack(n: usize) -> i32 { + let choices = vec![1, 2]; // Can choose to climb up 1 or 2 stairs + let state = 0; // Start climbing from the 0-th stair + let mut res = Vec::new(); + res.push(0); // Use res[0] to record the solution count + backtrack(&choices, state, n as i32, &mut res); + res[0] + } ``` === "C" ```c title="climbing_stairs_backtrack.c" - [class]{}-[func]{backtrack} + /* Backtracking */ + void backtrack(int *choices, int state, int n, int *res, int len) { + // When climbing to the n-th stair, add 1 to the solution count + if (state == n) + res[0]++; + // Traverse all choices + for (int i = 0; i < len; i++) { + int choice = choices[i]; + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) + continue; + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res, len); + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + int climbingStairsBacktrack(int n) { + int choices[2] = {1, 2}; // Can choose to climb up 1 or 2 stairs + int state = 0; // Start climbing from the 0-th stair + int *res = (int *)malloc(sizeof(int)); + *res = 0; // Use res[0] to record the solution count + int len = sizeof(choices) / sizeof(int); + backtrack(choices, state, n, res, len); + int result = *res; + free(res); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_backtrack.kt" - [class]{}-[func]{backtrack} + /* Backtracking */ + fun backtrack( + choices: MutableList, + state: Int, + n: Int, + res: MutableList + ) { + // When climbing to the n-th stair, add 1 to the solution count + if (state == n) + res[0] = res[0] + 1 + // Traverse all choices + for (choice in choices) { + // Pruning: not allowed to go beyond the n-th stair + if (state + choice > n) continue + // Attempt: make choice, update state + backtrack(choices, state + choice, n, res) + // Backtrack + } + } - [class]{}-[func]{climbingStairsBacktrack} + /* Climbing stairs: Backtracking */ + fun climbingStairsBacktrack(n: Int): Int { + val choices = mutableListOf(1, 2) // Can choose to climb up 1 or 2 stairs + val state = 0 // Start climbing from the 0-th stair + val res = mutableListOf() + res.add(0) // Use res[0] to record the solution count + backtrack(choices, state, n, res) + return res[0] + } ``` === "Ruby" ```ruby title="climbing_stairs_backtrack.rb" - [class]{}-[func]{backtrack} + ### Backtracking ### + def backtrack(choices, state, n, res) + # When climbing to the n-th stair, add 1 to the solution count + res[0] += 1 if state == n + # Traverse all choices + for choice in choices + # Pruning: not allowed to go beyond the n-th stair + next if state + choice > n - [class]{}-[func]{climbing_stairs_backtrack} + # Attempt: make choice, update state + backtrack(choices, state + choice, n, res) + end + # Backtrack + end + + ### Climbing stairs: backtracking ### + def climbing_stairs_backtrack(n) + choices = [1, 2] # Can choose to climb up 1 or 2 stairs + state = 0 # Start climbing from the 0-th stair + res = [0] # Use res[0] to record the solution count + backtrack(choices, state, n, res) + res.first + end ``` -=== "Zig" +## 14.1.1   Method 1: Brute Force Search - ```zig title="climbing_stairs_backtrack.zig" - [class]{}-[func]{backtrack} +Backtracking algorithms typically do not explicitly decompose problems, but rather treat solving the problem as a series of decision steps, searching for all possible solutions through trial and pruning. - [class]{}-[func]{climbingStairsBacktrack} - ``` - -## 14.1.1   Method 1: Brute force search - -Backtracking algorithms do not explicitly decompose the problem into subproblems. Instead, they treat the problem as a sequence of decision steps, exploring all possibilities through trial and pruning. - -We can analyze this problem using a decomposition approach. Let $dp[i]$ represent the number of ways to reach the $i^{th}$ step. In this case, $dp[i]$ is the original problem, and its subproblems are: +We can try to analyze this problem from the perspective of problem decomposition. Let the number of ways to climb to the $i$-th step be $dp[i]$, then $dp[i]$ is the original problem, and its subproblems include: $$ dp[i-1], dp[i-2], \dots, dp[2], dp[1] $$ -Since each move can only advance $1$ or $2$ steps, when we stand on the $i^{th}$ step, the previous step must have been either on the $i-1^{th}$ or the $i-2^{th}$ step. In other words, we can only reach the $i^{th}$ from the $i-1^{th}$ or $i-2^{th}$ step. +Since we can only go up $1$ or $2$ steps in each round, when we stand on the $i$-th step, we could only have been on the $i-1$-th or $i-2$-th step in the previous round. In other words, we can only reach the $i$-th step from the $i-1$-th or $i-2$-th step. -This leads to an important conclusion: **the number of ways to reach the $i-1^{th}$ step plus the number of ways to reach the $i-2^{th}$ step equals the number of ways to reach the $i^{th}$ step**. The formula is as follows: +This leads to an important conclusion: **the number of ways to climb to the $i-1$-th step plus the number of ways to climb to the $i-2$-th step equals the number of ways to climb to the $i$-th step**. The formula is as follows: $$ dp[i] = dp[i-1] + dp[i-2] $$ -This means that in the stair climbing problem, there is a recursive relationship between the subproblems, **the solution to the original problem can be constructed from the solutions to the subproblems**. Figure 14-2 shows this recursive relationship. +This means that in the stair climbing problem, there exists a recurrence relation among the subproblems, **the solution to the original problem can be constructed from the solutions to the subproblems**. Figure 14-2 illustrates this recurrence relation. -![Recursive relationship of solution counts](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } +![Recurrence relation for the number of ways](intro_to_dynamic_programming.assets/climbing_stairs_state_transfer.png){ class="animation-figure" } -

Figure 14-2   Recursive relationship of solution counts

+

Figure 14-2   Recurrence relation for the number of ways

-We can obtain the brute force search solution according to the recursive formula. Starting with $dp[n]$, **we recursively break a larger problem into the sum of two smaller subproblems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ where the solutions are known, with $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the first and second steps, respectively. +We can obtain a brute force search solution based on the recurrence formula. Starting from $dp[n]$, **recursively decompose a larger problem into the sum of two smaller problems**, until reaching the smallest subproblems $dp[1]$ and $dp[2]$ and returning. Among them, the solutions to the smallest subproblems are known, namely $dp[1] = 1$ and $dp[2] = 2$, representing $1$ and $2$ ways to climb to the $1$st and $2$nd steps, respectively. Observe the following code, which, like standard backtracking code, belongs to depth-first search but is more concise: @@ -279,107 +506,206 @@ Observe the following code, which, like standard backtracking code, belongs to d === "C#" ```csharp title="climbing_stairs_dfs.cs" - [class]{climbing_stairs_dfs}-[func]{DFS} + /* Search */ + int DFS(int i) { + // Known dp[1] and dp[2], return them + 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; + } - [class]{climbing_stairs_dfs}-[func]{ClimbingStairsDFS} + /* Climbing stairs: Search */ + int ClimbingStairsDFS(int n) { + return DFS(n); + } ``` === "Go" ```go title="climbing_stairs_dfs.go" - [class]{}-[func]{dfs} + /* Search */ + func dfs(i int) int { + // Known dp[1] and dp[2], return them + if i == 1 || i == 2 { + return i + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfs(i-1) + dfs(i-2) + return count + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + func climbingStairsDFS(n int) int { + return dfs(n) + } ``` === "Swift" ```swift title="climbing_stairs_dfs.swift" - [class]{}-[func]{dfs} + /* Search */ + func dfs(i: Int) -> Int { + // Known dp[1] and dp[2], return them + 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 + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + func climbingStairsDFS(n: Int) -> Int { + dfs(i: n) + } ``` === "JS" ```javascript title="climbing_stairs_dfs.js" - [class]{}-[func]{dfs} + /* Search */ + function dfs(i) { + // Known dp[1] and dp[2], return them + 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; + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + function climbingStairsDFS(n) { + return dfs(n); + } ``` === "TS" ```typescript title="climbing_stairs_dfs.ts" - [class]{}-[func]{dfs} + /* Search */ + function dfs(i: number): number { + // Known dp[1] and dp[2], return them + 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; + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + function climbingStairsDFS(n: number): number { + return dfs(n); + } ``` === "Dart" ```dart title="climbing_stairs_dfs.dart" - [class]{}-[func]{dfs} + /* Search */ + int dfs(int i) { + // Known dp[1] and dp[2], return them + 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; + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + int climbingStairsDFS(int n) { + return dfs(n); + } ``` === "Rust" ```rust title="climbing_stairs_dfs.rs" - [class]{}-[func]{dfs} + /* Search */ + fn dfs(i: usize) -> i32 { + // Known dp[1] and dp[2], return them + 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 + } - [class]{}-[func]{climbing_stairs_dfs} + /* Climbing stairs: Search */ + fn climbing_stairs_dfs(n: usize) -> i32 { + dfs(n) + } ``` === "C" ```c title="climbing_stairs_dfs.c" - [class]{}-[func]{dfs} + /* Search */ + int dfs(int i) { + // Known dp[1] and dp[2], return them + 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; + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + int climbingStairsDFS(int n) { + return dfs(n); + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dfs.kt" - [class]{}-[func]{dfs} + /* Search */ + fun dfs(i: Int): Int { + // Known dp[1] and dp[2], return them + if (i == 1 || i == 2) return i + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1) + dfs(i - 2) + return count + } - [class]{}-[func]{climbingStairsDFS} + /* Climbing stairs: Search */ + fun climbingStairsDFS(n: Int): Int { + return dfs(n) + } ``` === "Ruby" ```ruby title="climbing_stairs_dfs.rb" - [class]{}-[func]{dfs} + ### Search ### + def dfs(i) + # Known dp[1] and dp[2], return them + return i if i == 1 || i == 2 + # dp[i] = dp[i-1] + dp[i-2] + dfs(i - 1) + dfs(i - 2) + end - [class]{}-[func]{climbing_stairs_dfs} + ### Climbing stairs: search ### + def climbing_stairs_dfs(n) + dfs(n) + end ``` -=== "Zig" +Figure 14-3 shows the recursion tree formed by brute force search. For the problem $dp[n]$, the depth of its recursion tree is $n$, with a time complexity of $O(2^n)$. Exponential order represents explosive growth; if we input a relatively large $n$, we will fall into a long wait. - ```zig title="climbing_stairs_dfs.zig" - [class]{}-[func]{dfs} +![Recursion tree for climbing stairs](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } - [class]{}-[func]{climbingStairsDFS} - ``` +

Figure 14-3   Recursion tree for climbing stairs

-Figure 14-3 shows the recursive tree formed by brute force search. For the problem $dp[n]$, the depth of its recursive tree is $n$, with a time complexity of $O(2^n)$. This exponential growth causes the program to run much more slowly when $n$ is large, leading to long wait times. +Observing the above figure, **the exponential time complexity is caused by "overlapping subproblems"**. For example, $dp[9]$ is decomposed into $dp[8]$ and $dp[7]$, and $dp[8]$ is decomposed into $dp[7]$ and $dp[6]$, both of which contain the subproblem $dp[7]$. -![Recursive tree for climbing stairs](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } +And so on, subproblems contain smaller overlapping subproblems, ad infinitum. The vast majority of computational resources are wasted on these overlapping subproblems. -

Figure 14-3   Recursive tree for climbing stairs

+## 14.1.2   Method 2: Memoization -Observing Figure 14-3, **the exponential time complexity is caused by 'overlapping subproblems'**. For example, $dp[9]$ is broken down into $dp[8]$ and $dp[7]$, and $dp[8]$ is further broken into $dp[7]$ and $dp[6]$, both containing the subproblem $dp[7]$. +To improve algorithm efficiency, **we want all overlapping subproblems to be computed only once**. For this purpose, we declare an array `mem` to record the solution to each subproblem and prune overlapping subproblems during the search process. -Thus, subproblems include even smaller overlapping subproblems, endlessly. A vast majority of computational resources are wasted on these overlapping subproblems. - -## 14.1.2   Method 2: Memoized search - -To enhance algorithm efficiency, **we hope that all overlapping subproblems are calculated only once**. For this purpose, we declare an array `mem` to record the solution of each subproblem, and prune overlapping subproblems during the search process. - -1. When $dp[i]$ is calculated for the first time, we record it in `mem[i]` for later use. -2. When $dp[i]$ needs to be calculated again, we can directly retrieve the result from `mem[i]`, thus avoiding redundant calculations of that subproblem. +1. When computing $dp[i]$ for the first time, we record it in `mem[i]` for later use. +2. When we need to compute $dp[i]$ again, we can directly retrieve the result from `mem[i]`, thereby avoiding redundant computation of that subproblem. The code is as follows: @@ -387,11 +713,11 @@ The code is as follows: ```python title="climbing_stairs_dfs_mem.py" def dfs(i: int, mem: list[int]) -> int: - """Memoized search""" + """Memoization search""" # Known dp[1] and dp[2], return them if i == 1 or i == 2: return i - # If there is a record for dp[i], return it + # If record dp[i] exists, return it directly if mem[i] != -1: return mem[i] # dp[i] = dp[i-1] + dp[i-2] @@ -401,8 +727,8 @@ The code is as follows: return count def climbing_stairs_dfs_mem(n: int) -> int: - """Climbing stairs: Memoized search""" - # mem[i] records the total number of solutions for climbing to the ith step, -1 means no record + """Climbing stairs: Memoization search""" + # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record mem = [-1] * (n + 1) return dfs(n, mem) ``` @@ -410,12 +736,12 @@ The code is as follows: === "C++" ```cpp title="climbing_stairs_dfs_mem.cpp" - /* Memoized search */ + /* Memoization search */ int dfs(int i, vector &mem) { // Known dp[1] and dp[2], return them if (i == 1 || i == 2) return i; - // If there is a record for dp[i], return it + // If record dp[i] exists, return it directly if (mem[i] != -1) return mem[i]; // dp[i] = dp[i-1] + dp[i-2] @@ -425,9 +751,9 @@ The code is as follows: return count; } - /* Climbing stairs: Memoized search */ + /* Climbing stairs: Memoization search */ int climbingStairsDFSMem(int n) { - // mem[i] records the total number of solutions for climbing to the ith step, -1 means no record + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record vector mem(n + 1, -1); return dfs(n, mem); } @@ -436,12 +762,12 @@ The code is as follows: === "Java" ```java title="climbing_stairs_dfs_mem.java" - /* Memoized search */ + /* Memoization search */ int dfs(int i, int[] mem) { // Known dp[1] and dp[2], return them if (i == 1 || i == 2) return i; - // If there is a record for dp[i], return it + // If record dp[i] exists, return it directly if (mem[i] != -1) return mem[i]; // dp[i] = dp[i-1] + dp[i-2] @@ -451,9 +777,9 @@ The code is as follows: return count; } - /* Climbing stairs: Memoized search */ + /* Climbing stairs: Memoization search */ int climbingStairsDFSMem(int n) { - // mem[i] records the total number of solutions for climbing to the ith step, -1 means no record + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record int[] mem = new int[n + 1]; Arrays.fill(mem, -1); return dfs(n, mem); @@ -463,104 +789,282 @@ The code is as follows: === "C#" ```csharp title="climbing_stairs_dfs_mem.cs" - [class]{climbing_stairs_dfs_mem}-[func]{DFS} + /* Memoization search */ + int DFS(int i, int[] mem) { + // Known dp[1] and dp[2], return them + if (i == 1 || i == 2) + return i; + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + return count; + } - [class]{climbing_stairs_dfs_mem}-[func]{ClimbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + int ClimbingStairsDFSMem(int n) { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + int[] mem = new int[n + 1]; + Array.Fill(mem, -1); + return DFS(n, mem); + } ``` === "Go" ```go title="climbing_stairs_dfs_mem.go" - [class]{}-[func]{dfsMem} + /* Memoization search */ + func dfsMem(i int, mem []int) int { + // Known dp[1] and dp[2], return them + if i == 1 || i == 2 { + return i + } + // If record dp[i] exists, return it directly + if mem[i] != -1 { + return mem[i] + } + // dp[i] = dp[i-1] + dp[i-2] + count := dfsMem(i-1, mem) + dfsMem(i-2, mem) + // Record dp[i] + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + func climbingStairsDFSMem(n int) int { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + mem := make([]int, n+1) + for i := range mem { + mem[i] = -1 + } + return dfsMem(n, mem) + } ``` === "Swift" ```swift title="climbing_stairs_dfs_mem.swift" - [class]{}-[func]{dfs} + /* Memoization search */ + func dfs(i: Int, mem: inout [Int]) -> Int { + // Known dp[1] and dp[2], return them + if i == 1 || i == 2 { + return i + } + // If record dp[i] exists, return it directly + 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) + // Record dp[i] + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + func climbingStairsDFSMem(n: Int) -> Int { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + var mem = Array(repeating: -1, count: n + 1) + return dfs(i: n, mem: &mem) + } ``` === "JS" ```javascript title="climbing_stairs_dfs_mem.js" - [class]{}-[func]{dfs} + /* Memoization search */ + function dfs(i, mem) { + // Known dp[1] and dp[2], return them + if (i === 1 || i === 2) return i; + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + function climbingStairsDFSMem(n) { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "TS" ```typescript title="climbing_stairs_dfs_mem.ts" - [class]{}-[func]{dfs} + /* Memoization search */ + function dfs(i: number, mem: number[]): number { + // Known dp[1] and dp[2], return them + if (i === 1 || i === 2) return i; + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + function climbingStairsDFSMem(n: number): number { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + const mem = new Array(n + 1).fill(-1); + return dfs(n, mem); + } ``` === "Dart" ```dart title="climbing_stairs_dfs_mem.dart" - [class]{}-[func]{dfs} + /* Memoization search */ + int dfs(int i, List mem) { + // Known dp[1] and dp[2], return them + if (i == 1 || i == 2) return i; + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + int climbingStairsDFSMem(int n) { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + List mem = List.filled(n + 1, -1); + return dfs(n, mem); + } ``` === "Rust" ```rust title="climbing_stairs_dfs_mem.rs" - [class]{}-[func]{dfs} + /* Memoization search */ + fn dfs(i: usize, mem: &mut [i32]) -> i32 { + // Known dp[1] and dp[2], return them + if i == 1 || i == 2 { + return i as i32; + } + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + count + } - [class]{}-[func]{climbing_stairs_dfs_mem} + /* Climbing stairs: Memoization search */ + fn climbing_stairs_dfs_mem(n: usize) -> i32 { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + let mut mem = vec![-1; n + 1]; + dfs(n, &mut mem) + } ``` === "C" ```c title="climbing_stairs_dfs_mem.c" - [class]{}-[func]{dfs} + /* Memoization search */ + int dfs(int i, int *mem) { + // Known dp[1] and dp[2], return them + if (i == 1 || i == 2) + return i; + // If record dp[i] exists, return it directly + 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); + // Record dp[i] + mem[i] = count; + return count; + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + int climbingStairsDFSMem(int n) { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + int *mem = (int *)malloc((n + 1) * sizeof(int)); + for (int i = 0; i <= n; i++) { + mem[i] = -1; + } + int result = dfs(n, mem); + free(mem); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dfs_mem.kt" - [class]{}-[func]{dfs} + /* Memoization search */ + fun dfs(i: Int, mem: IntArray): Int { + // Known dp[1] and dp[2], return them + if (i == 1 || i == 2) return i + // If record dp[i] exists, return it directly + if (mem[i] != -1) return mem[i] + // dp[i] = dp[i-1] + dp[i-2] + val count = dfs(i - 1, mem) + dfs(i - 2, mem) + // Record dp[i] + mem[i] = count + return count + } - [class]{}-[func]{climbingStairsDFSMem} + /* Climbing stairs: Memoization search */ + fun climbingStairsDFSMem(n: Int): Int { + // mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + val mem = IntArray(n + 1) + mem.fill(-1) + return dfs(n, mem) + } ``` === "Ruby" ```ruby title="climbing_stairs_dfs_mem.rb" - [class]{}-[func]{dfs} + ### Memoization search ### + def dfs(i, mem) + # Known dp[1] and dp[2], return them + return i if i == 1 || i == 2 + # If record dp[i] exists, return it directly + return mem[i] if mem[i] != -1 - [class]{}-[func]{climbing_stairs_dfs_mem} + # dp[i] = dp[i-1] + dp[i-2] + count = dfs(i - 1, mem) + dfs(i - 2, mem) + # Record dp[i] + mem[i] = count + end + + ### Climbing stairs: memoization search ### + def climbing_stairs_dfs_mem(n) + # mem[i] records the total number of solutions to climb to the i-th stair, -1 means no record + mem = Array.new(n + 1, -1) + dfs(n, mem) + end ``` -=== "Zig" +Observe Figure 14-4, **after memoization, all overlapping subproblems only need to be computed once, optimizing the time complexity to $O(n)$**, which is a tremendous leap. - ```zig title="climbing_stairs_dfs_mem.zig" - [class]{}-[func]{dfs} +![Recursion tree with memoization](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } - [class]{}-[func]{climbingStairsDFSMem} - ``` +

Figure 14-4   Recursion tree with memoization

-Observe Figure 14-4, **after memoization, all overlapping subproblems need to be calculated only once, optimizing the time complexity to $O(n)$**, which is a significant leap. +## 14.1.3   Method 3: Dynamic Programming -![Recursive tree with memoized search](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } +**Memoization is a "top-down" method**: we start from the original problem (root node), recursively decompose larger subproblems into smaller ones, until reaching the smallest known subproblems (leaf nodes). Afterward, by backtracking, we collect the solutions to the subproblems layer by layer to construct the solution to the original problem. -

Figure 14-4   Recursive tree with memoized search

+In contrast, **dynamic programming is a "bottom-up" method**: starting from the solutions to the smallest subproblems, iteratively constructing solutions to larger subproblems until obtaining the solution to the original problem. -## 14.1.3   Method 3: Dynamic programming - -**Memoized search is a 'top-down' method**: we start with the original problem (root node), recursively break larger subproblems into smaller ones until the solutions to the smallest known subproblems (leaf nodes) are reached. Subsequently, by backtracking, we collect the solutions of the subproblems, constructing the solution to the original problem. - -On the contrary, **dynamic programming is a 'bottom-up' method**: starting with the solutions to the smallest subproblems, it iteratively constructs the solutions to larger subproblems until the original problem is solved. - -Since dynamic programming does not involve backtracking, it only requires iteration using loops and does not need recursion. In the following code, we initialize an array `dp` to store the solutions to subproblems, serving the same recording function as the array `mem` in memoized search: +Since dynamic programming does not include a backtracking process, it only requires loop iteration for implementation and does not need recursion. In the following code, we initialize an array `dp` to store the solutions to subproblems, which serves the same recording function as the array `mem` in memoization: === "Python" @@ -569,9 +1073,9 @@ Since dynamic programming does not involve backtracking, it only requires iterat """Climbing stairs: Dynamic programming""" if n == 1 or n == 2: return n - # Initialize dp table, used to store subproblem solutions + # Initialize dp table, used to store solutions to subproblems dp = [0] * (n + 1) - # Initial state: preset the smallest subproblem solution + # Initial state: preset the solution to the smallest subproblem dp[1], dp[2] = 1, 2 # State transition: gradually solve larger subproblems from smaller ones for i in range(3, n + 1): @@ -586,9 +1090,9 @@ Since dynamic programming does not involve backtracking, it only requires iterat int climbingStairsDP(int n) { if (n == 1 || n == 2) return n; - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems vector dp(n + 1); - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1] = 1; dp[2] = 2; // State transition: gradually solve larger subproblems from smaller ones @@ -606,9 +1110,9 @@ Since dynamic programming does not involve backtracking, it only requires iterat int climbingStairsDP(int n) { if (n == 1 || n == 2) return n; - // Initialize dp table, used to store subproblem solutions + // Initialize dp table, used to store solutions to subproblems int[] dp = new int[n + 1]; - // Initial state: preset the smallest subproblem solution + // Initial state: preset the solution to the smallest subproblem dp[1] = 1; dp[2] = 2; // State transition: gradually solve larger subproblems from smaller ones @@ -622,67 +1126,201 @@ Since dynamic programming does not involve backtracking, it only requires iterat === "C#" ```csharp title="climbing_stairs_dp.cs" - [class]{climbing_stairs_dp}-[func]{ClimbingStairsDP} + /* Climbing stairs: Dynamic programming */ + int ClimbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // Initialize dp table, used to store solutions to subproblems + int[] dp = new int[n + 1]; + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + func climbingStairsDP(n int) int { + if n == 1 || n == 2 { + return n + } + // Initialize dp table, used to store solutions to subproblems + dp := make([]int, n+1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1 + dp[2] = 2 + // State transition: gradually solve larger subproblems from smaller ones + for i := 3; i <= n; i++ { + dp[i] = dp[i-1] + dp[i-2] + } + return dp[n] + } ``` === "Swift" ```swift title="climbing_stairs_dp.swift" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + func climbingStairsDP(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + // Initialize dp table, used to store solutions to subproblems + var dp = Array(repeating: 0, count: n + 1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1 + dp[2] = 2 + // State transition: gradually solve larger subproblems from smaller ones + for i in 3 ... n { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } ``` === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + function climbingStairsDP(n) { + if (n === 1 || n === 2) return n; + // Initialize dp table, used to store solutions to subproblems + const dp = new Array(n + 1).fill(-1); + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + function climbingStairsDP(n: number): number { + if (n === 1 || n === 2) return n; + // Initialize dp table, used to store solutions to subproblems + const dp = new Array(n + 1).fill(-1); + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) return n; + // Initialize dp table, used to store solutions to subproblems + List dp = List.filled(n + 1, 0); + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + 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" - [class]{}-[func]{climbing_stairs_dp} + /* Climbing stairs: Dynamic programming */ + fn climbing_stairs_dp(n: usize) -> i32 { + // Known dp[1] and dp[2], return them + if n == 1 || n == 2 { + return n as i32; + } + // Initialize dp table, used to store solutions to subproblems + let mut dp = vec![-1; n + 1]; + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + for i in 3..=n { + dp[i] = dp[i - 1] + dp[i - 2]; + } + dp[n] + } ``` === "C" ```c title="climbing_stairs_dp.c" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + int climbingStairsDP(int n) { + if (n == 1 || n == 2) + return n; + // Initialize dp table, used to store solutions to subproblems + int *dp = (int *)malloc((n + 1) * sizeof(int)); + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1; + dp[2] = 2; + // State transition: gradually solve larger subproblems from smaller ones + for (int i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + int result = dp[n]; + free(dp); + return result; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dp.kt" - [class]{}-[func]{climbingStairsDP} + /* Climbing stairs: Dynamic programming */ + fun climbingStairsDP(n: Int): Int { + if (n == 1 || n == 2) return n + // Initialize dp table, used to store solutions to subproblems + val dp = IntArray(n + 1) + // Initial state: preset the solution to the smallest subproblem + dp[1] = 1 + dp[2] = 2 + // State transition: gradually solve larger subproblems from smaller ones + for (i in 3..n) { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n] + } ``` === "Ruby" ```ruby title="climbing_stairs_dp.rb" - [class]{}-[func]{climbing_stairs_dp} - ``` + ### Climbing stairs: dynamic programming ### + def climbing_stairs_dp(n) + return n if n == 1 || n == 2 -=== "Zig" + # Initialize dp table, used to store solutions to subproblems + dp = Array.new(n + 1, 0) + # Initial state: preset the solution to the smallest subproblem + dp[1], dp[2] = 1, 2 + # State transition: gradually solve larger subproblems from smaller ones + (3...(n + 1)).each { |i| dp[i] = dp[i - 1] + dp[i - 2] } - ```zig title="climbing_stairs_dp.zig" - [class]{}-[func]{climbingStairsDP} + dp[n] + end ``` Figure 14-5 simulates the execution process of the above code. @@ -691,17 +1329,17 @@ Figure 14-5 simulates the execution process of the above code.

Figure 14-5   Dynamic programming process for climbing stairs

-Like the backtracking algorithm, dynamic programming also uses the concept of "states" to represent specific stages in problem solving, each state corresponding to a subproblem and its local optimal solution. For example, the state of the climbing stairs problem is defined as the current step number $i$. +Like backtracking algorithms, dynamic programming also uses the "state" concept to represent specific stages of problem solving, with each state corresponding to a subproblem and its corresponding local optimal solution. For example, the state in the stair climbing problem is defined as the current stair step number $i$. Based on the above content, we can summarize the commonly used terminology in dynamic programming. -- The array `dp` is referred to as the DP table, with $dp[i]$ representing the solution to the subproblem corresponding to state $i$. -- The states corresponding to the smallest subproblems (steps $1$ and $2$) are called initial states. -- The recursive formula $dp[i] = dp[i-1] + dp[i-2]$ is called the state transition equation. +- The array `dp` is called the dp table, where $dp[i]$ represents the solution to the subproblem corresponding to state $i$. +- The states corresponding to the smallest subproblems (the $1$st and $2$nd steps) are called initial states. +- The recurrence formula $dp[i] = dp[i-1] + dp[i-2]$ is called the state transition equation. -## 14.1.4   Space optimization +## 14.1.4   Space Optimization -Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, but can simply use two variables to progress iteratively. The code is as follows: +Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i-1]$ and $dp[i-2]$, we do not need to use an array `dp` to store the solutions to all subproblems**, but can simply use two variables to roll forward. The code is as follows: === "Python" @@ -753,69 +1391,170 @@ Observant readers may have noticed that **since $dp[i]$ is only related to $dp[i === "C#" ```csharp title="climbing_stairs_dp.cs" - [class]{climbing_stairs_dp}-[func]{ClimbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + func climbingStairsDPComp(n int) int { + if n == 1 || n == 2 { + return n + } + a, b := 1, 2 + // State transition: gradually solve larger subproblems from smaller ones + for i := 3; i <= n; i++ { + a, b = b, a+b + } + return b + } ``` === "Swift" ```swift title="climbing_stairs_dp.swift" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + func climbingStairsDPComp(n: Int) -> Int { + if n == 1 || n == 2 { + return n + } + var a = 1 + var b = 2 + for _ in 3 ... n { + (a, b) = (b, a + b) + } + return b + } ``` === "JS" ```javascript title="climbing_stairs_dp.js" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{climbing_stairs_dp_comp} + /* Climbing stairs: Space-optimized dynamic programming */ + 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" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + int climbingStairsDPComp(int n) { + if (n == 1 || n == 2) + return n; + int a = 1, b = 2; + for (int i = 3; i <= n; i++) { + int tmp = b; + b = a + b; + a = tmp; + } + return b; + } ``` === "Kotlin" ```kotlin title="climbing_stairs_dp.kt" - [class]{}-[func]{climbingStairsDPComp} + /* Climbing stairs: Space-optimized dynamic programming */ + fun climbingStairsDPComp(n: Int): Int { + if (n == 1 || n == 2) return n + var a = 1 + var b = 2 + for (i in 3..n) { + val temp = b + b += a + a = temp + } + return b + } ``` === "Ruby" ```ruby title="climbing_stairs_dp.rb" - [class]{}-[func]{climbing_stairs_dp_comp} + ### Climbing stairs: space-optimized DP ### + def climbing_stairs_dp_comp(n) + return n if n == 1 || n == 2 + + a, b = 1, 2 + (3...(n + 1)).each { a, b = b, a + b } + + b + end ``` -=== "Zig" +Observing the above code, since the space occupied by the array `dp` is saved, the space complexity is reduced from $O(n)$ to $O(1)$. - ```zig title="climbing_stairs_dp.zig" - [class]{}-[func]{climbingStairsDPComp} - ``` - -Observing the above code, since the space occupied by the array `dp` is eliminated, the space complexity is reduced from $O(n)$ to $O(1)$. - -In many dynamic programming problems, the current state depends only on a limited number of previous states, allowing us to retain only the necessary states and save memory space by "dimension reduction". **This space optimization technique is known as 'rolling variable' or 'rolling array'**. +In dynamic programming problems, the current state often depends only on a limited number of preceding states, allowing us to retain only the necessary states and save memory space through "dimension reduction". **This space optimization technique is called "rolling variable" or "rolling array"**. diff --git a/en/docs/chapter_dynamic_programming/knapsack_problem.md b/en/docs/chapter_dynamic_programming/knapsack_problem.md index ca7f6afca..9a378de94 100644 --- a/en/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/knapsack_problem.md @@ -2,101 +2,101 @@ comments: true --- -# 14.4   0-1 Knapsack problem +# 14.4   0-1 Knapsack Problem -The knapsack problem is an excellent introductory problem for dynamic programming and is the most common type of problem in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem, etc. +The knapsack problem is an excellent introductory problem for dynamic programming and is one of the most common problem forms in dynamic programming. It has many variants, such as the 0-1 knapsack problem, the unbounded knapsack problem, and the multiple knapsack problem. In this section, we will first solve the most common 0-1 knapsack problem. !!! question - Given $n$ items, the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with a capacity of $cap$. Each item can be chosen only once. What is the maximum value of items that can be placed in the knapsack under the capacity limit? + Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can only be selected once. What is the maximum value that can be placed in the knapsack within the capacity limit? -Observe Figure 14-17, since the item number $i$ starts counting from 1, and the array index starts from 0, thus the weight of item $i$ corresponds to $wgt[i-1]$ and the value corresponds to $val[i-1]$. +Observe Figure 14-17. Since item number $i$ starts counting from $1$ and array indices start from $0$, item $i$ corresponds to weight $wgt[i-1]$ and value $val[i-1]$. -![Example data of the 0-1 knapsack](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" } +![Example data for 0-1 knapsack](knapsack_problem.assets/knapsack_example.png){ class="animation-figure" } -

Figure 14-17   Example data of the 0-1 knapsack

+

Figure 14-17   Example data for 0-1 knapsack

-We can consider the 0-1 knapsack problem as a process consisting of $n$ rounds of decisions, where for each item there are two decisions: not to put it in or to put it in, thus the problem fits the decision tree model. +We can view the 0-1 knapsack problem as a process consisting of $n$ rounds of decisions, where for each item there are two decisions: not putting it in and putting it in, thus the problem satisfies the decision tree model. -The objective of this problem is to "maximize the value of the items that can be put in the knapsack under the limited capacity," thus it is more likely a dynamic programming problem. +The goal of this problem is to find "the maximum value that can be placed in the knapsack within the capacity limit", so it is more likely to be a dynamic programming problem. -**First step: Think about each round of decisions, define states, thereby obtaining the $dp$ table** +**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table** -For each item, if not put into the knapsack, the capacity remains unchanged; if put in, the capacity is reduced. From this, the state definition can be obtained: the current item number $i$ and knapsack capacity $c$, denoted as $[i, c]$. +For each item, if not placed in the knapsack, the knapsack capacity remains unchanged; if placed in, the knapsack capacity decreases. From this, we can derive the state definition: current item number $i$ and knapsack capacity $c$, denoted as $[i, c]$. -State $[i, c]$ corresponds to the sub-problem: **the maximum value of the first $i$ items in a knapsack of capacity $c$**, denoted as $dp[i, c]$. +State $[i, c]$ corresponds to the subproblem: **the maximum value among the first $i$ items in a knapsack of capacity $c$**, denoted as $dp[i, c]$. -The solution we are looking for is $dp[n, cap]$, so we need a two-dimensional $dp$ table of size $(n+1) \times (cap+1)$. +What we need to find is $dp[n, cap]$, so we need a two-dimensional $dp$ table of size $(n+1) \times (cap+1)$. -**Second step: Identify the optimal substructure, then derive the state transition equation** +**Step 2: Identify the optimal substructure, and then derive the state transition equation** -After making the decision for item $i$, what remains is the sub-problem of decisions for the first $i-1$ items, which can be divided into two cases. +After making the decision for item $i$, what remains is the subproblem of the first $i-1$ items, which can be divided into the following two cases. -- **Not putting item $i$**: The knapsack capacity remains unchanged, state changes to $[i-1, c]$. -- **Putting item $i$**: The knapsack capacity decreases by $wgt[i-1]$, and the value increases by $val[i-1]$, state changes to $[i-1, c-wgt[i-1]]$. +- **Not putting item $i$**: The knapsack capacity remains unchanged, and the state changes to $[i-1, c]$. +- **Putting item $i$**: The knapsack capacity decreases by $wgt[i-1]$, the value increases by $val[i-1]$, and the state changes to $[i-1, c-wgt[i-1]]$. -The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ is equal to the larger value of the two schemes of not putting item $i$ and putting item $i$**. From this, the state transition equation can be derived: +The above analysis reveals the optimal substructure of this problem: **the maximum value $dp[i, c]$ equals the larger value between not putting item $i$ and putting item $i$**. From this, the state transition equation can be derived: $$ dp[i, c] = \max(dp[i-1, c], dp[i-1, c - wgt[i-1]] + val[i-1]) $$ -It is important to note that if the current item's weight $wgt[i - 1]$ exceeds the remaining knapsack capacity $c$, then the only option is not to put it in the knapsack. +Note that if the weight of the current item $wgt[i - 1]$ exceeds the remaining knapsack capacity $c$, then the only option is not to put it in the knapsack. -**Third step: Determine the boundary conditions and the order of state transitions** +**Step 3: Determine boundary conditions and state transition order** When there are no items or the knapsack capacity is $0$, the maximum value is $0$, i.e., the first column $dp[i, 0]$ and the first row $dp[0, c]$ are both equal to $0$. -The current state $[i, c]$ transitions from the state directly above $[i-1, c]$ and the state to the upper left $[i-1, c-wgt[i-1]]$, thus, the entire $dp$ table is traversed in order through two layers of loops. +The current state $[i, c]$ is transferred from the state above $[i-1, c]$ and the state in the upper-left $[i-1, c-wgt[i-1]]$, so the entire $dp$ table is traversed in order through two nested loops. -Following the above analysis, we will next implement the solutions in the order of brute force search, memoized search, and dynamic programming. +Based on the above analysis, we will next implement the brute force search, memoization, and dynamic programming solutions in order. -### 1.   Method one: Brute force search +### 1.   Method 1: Brute Force Search The search code includes the following elements. -- **Recursive parameters**: State $[i, c]$. -- **Return value**: Solution to the sub-problem $dp[i, c]$. -- **Termination condition**: When the item number is out of bounds $i = 0$ or the remaining capacity of the knapsack is $0$, terminate the recursion and return the value $0$. -- **Pruning**: If the current item's weight exceeds the remaining capacity of the knapsack, the only option is not to put it in the knapsack. +- **Recursive parameters**: state $[i, c]$. +- **Return value**: solution to the subproblem $dp[i, c]$. +- **Termination condition**: when the item number is out of bounds $i = 0$ or the remaining knapsack capacity is $0$, terminate recursion and return value $0$. +- **Pruning**: if the weight of the current item exceeds the remaining knapsack capacity, only the option of not putting it in is available. === "Python" ```python title="knapsack.py" def knapsack_dfs(wgt: list[int], val: list[int], i: int, c: int) -> int: - """0-1 Knapsack: Brute force search""" - # If all items have been chosen or the knapsack has no remaining capacity, return value 0 + """0-1 knapsack: Brute-force search""" + # If all items have been selected or knapsack has no remaining capacity, return value 0 if i == 0 or c == 0: return 0 - # If exceeding the knapsack capacity, can only choose not to put it in the knapsack + # If exceeds knapsack capacity, can only choose not to put it in if wgt[i - 1] > c: return knapsack_dfs(wgt, val, i - 1, c) # Calculate the maximum value of not putting in and putting in item i no = knapsack_dfs(wgt, val, i - 1, c) yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] - # Return the greater value of the two options + # Return the larger value of the two options return max(no, yes) ``` === "C++" ```cpp title="knapsack.cpp" - /* 0-1 Knapsack: Brute force search */ + /* 0-1 knapsack: Brute-force search */ int knapsackDFS(vector &wgt, vector &val, int i, int c) { - // If all items have been chosen or the knapsack has no remaining capacity, return value 0 + // If all items have been selected or knapsack has no remaining capacity, return value 0 if (i == 0 || c == 0) { return 0; } - // If exceeding the knapsack capacity, can only choose not to put it in the knapsack + // If exceeds knapsack capacity, can only choose not to put it in if (wgt[i - 1] > c) { return knapsackDFS(wgt, val, i - 1, c); } // Calculate the maximum value of not putting in and putting in item i int no = knapsackDFS(wgt, val, i - 1, c); int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; - // Return the greater value of the two options + // Return the larger value of the two options return max(no, yes); } ``` @@ -104,20 +104,20 @@ The search code includes the following elements. === "Java" ```java title="knapsack.java" - /* 0-1 Knapsack: Brute force search */ + /* 0-1 knapsack: Brute-force search */ int knapsackDFS(int[] wgt, int[] val, int i, int c) { - // If all items have been chosen or the knapsack has no remaining capacity, return value 0 + // If all items have been selected or knapsack has no remaining capacity, return value 0 if (i == 0 || c == 0) { return 0; } - // If exceeding the knapsack capacity, can only choose not to put it in the knapsack + // If exceeds knapsack capacity, can only choose not to put it in if (wgt[i - 1] > c) { return knapsackDFS(wgt, val, i - 1, c); } // Calculate the maximum value of not putting in and putting in item i int no = knapsackDFS(wgt, val, i - 1, c); int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; - // Return the greater value of the two options + // Return the larger value of the two options return Math.max(no, yes); } ``` @@ -125,82 +125,232 @@ The search code includes the following elements. === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDFS} + /* 0-1 knapsack: Brute-force search */ + int KnapsackDFS(int[] weight, int[] val, int i, int c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (weight[i - 1] > c) { + return KnapsackDFS(weight, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + int no = KnapsackDFS(weight, val, i - 1, c); + int yes = KnapsackDFS(weight, val, i - 1, c - weight[i - 1]) + val[i - 1]; + // Return the larger value of the two options + return Math.Max(no, yes); + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + func knapsackDFS(wgt, val []int, i, c int) int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0 + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i-1] > c { + return knapsackDFS(wgt, val, i-1, c) + } + // Calculate the maximum value of not putting in and putting in item i + no := knapsackDFS(wgt, val, i-1, c) + yes := knapsackDFS(wgt, val, i-1, c-wgt[i-1]) + val[i-1] + // Return the larger value of the two options + return int(math.Max(float64(no), float64(yes))) + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + func knapsackDFS(wgt: [Int], val: [Int], i: Int, c: Int) -> Int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0 + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i - 1] > c { + return knapsackDFS(wgt: wgt, val: val, i: i - 1, c: c) + } + // Calculate the maximum value of not putting in and putting in item 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 the larger value of the two options + return max(no, yes) + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + function knapsackDFS(wgt, val, i, c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i === 0 || c === 0) { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // Return the larger value of the two options + return Math.max(no, yes); + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + function knapsackDFS( + wgt: Array, + val: Array, + i: number, + c: number + ): number { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i === 0 || c === 0) { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + const no = knapsackDFS(wgt, val, i - 1, c); + const yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // Return the larger value of the two options + return Math.max(no, yes); + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + int knapsackDFS(List wgt, List val, int i, int c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // Return the larger value of the two options + return max(no, yes); + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dfs} + /* 0-1 knapsack: Brute-force search */ + fn knapsack_dfs(wgt: &[i32], val: &[i32], i: usize, c: usize) -> i32 { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i - 1] > c as i32 { + return knapsack_dfs(wgt, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Return the larger value of the two options + std::cmp::max(no, yes) + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + int knapsackDFS(int wgt[], int val[], int i, int c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, val, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + int no = knapsackDFS(wgt, val, i - 1, c); + int yes = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]; + // Return the larger value of the two options + return myMax(no, yes); + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDFS} + /* 0-1 knapsack: Brute-force search */ + fun knapsackDFS( + wgt: IntArray, + _val: IntArray, + i: Int, + c: Int + ): Int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0 + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFS(wgt, _val, i - 1, c) + } + // Calculate the maximum value of not putting in and putting in item i + val no = knapsackDFS(wgt, _val, i - 1, c) + val yes = knapsackDFS(wgt, _val, i - 1, c - wgt[i - 1]) + _val[i - 1] + // Return the larger value of the two options + return max(no, yes) + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dfs} - ``` - -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDFS} + ### 0-1 knapsack: brute force search ### + def knapsack_dfs(wgt, val, i, c) + # If all items have been selected or knapsack has no remaining capacity, return value 0 + return 0 if i == 0 || c == 0 + # If exceeds knapsack capacity, can only choose not to put it in + return knapsack_dfs(wgt, val, i - 1, c) if wgt[i - 1] > c + # Calculate the maximum value of not putting in and putting in item i + no = knapsack_dfs(wgt, val, i - 1, c) + yes = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] + # Return the larger value of the two options + [no, yes].max + end ``` As shown in Figure 14-18, since each item generates two search branches of not selecting and selecting, the time complexity is $O(2^n)$. -Observing the recursive tree, it is easy to see that there are overlapping sub-problems, such as $dp[1, 10]$, etc. When there are many items and the knapsack capacity is large, especially when there are many items of the same weight, the number of overlapping sub-problems will increase significantly. +Observing the recursion tree, it is easy to see overlapping subproblems, such as $dp[1, 10]$. When there are many items, large knapsack capacity, and especially many items with the same weight, the number of overlapping subproblems will increase significantly. -![The brute force search recursive tree of the 0-1 knapsack problem](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" } +![Brute force search recursion tree for 0-1 knapsack problem](knapsack_problem.assets/knapsack_dfs.png){ class="animation-figure" } -

Figure 14-18   The brute force search recursive tree of the 0-1 knapsack problem

+

Figure 14-18   Brute force search recursion tree for 0-1 knapsack problem

-### 2.   Method two: Memoized search +### 2.   Method 2: Memoization -To ensure that overlapping sub-problems are only calculated once, we use a memoization list `mem` to record the solutions to sub-problems, where `mem[i][c]` corresponds to $dp[i, c]$. +To ensure that overlapping subproblems are only computed once, we use a memo list `mem` to record the solutions to subproblems, where `mem[i][c]` corresponds to $dp[i, c]$. -After introducing memoization, **the time complexity depends on the number of sub-problems**, which is $O(n \times cap)$. The implementation code is as follows: +After introducing memoization, **the time complexity depends on the number of subproblems**, which is $O(n \times cap)$. The implementation code is as follows: === "Python" @@ -208,20 +358,20 @@ After introducing memoization, **the time complexity depends on the number of su def knapsack_dfs_mem( wgt: list[int], val: list[int], mem: list[list[int]], i: int, c: int ) -> int: - """0-1 Knapsack: Memoized search""" - # If all items have been chosen or the knapsack has no remaining capacity, return value 0 + """0-1 knapsack: Memoization search""" + # If all items have been selected or knapsack has no remaining capacity, return value 0 if i == 0 or c == 0: return 0 - # If there is a record, return it + # If there's a record, return it directly if mem[i][c] != -1: return mem[i][c] - # If exceeding the knapsack capacity, can only choose not to put it in the knapsack + # If exceeds knapsack capacity, can only choose not to put it in if wgt[i - 1] > c: return knapsack_dfs_mem(wgt, val, mem, i - 1, c) # Calculate the maximum value of not putting in and putting in item 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] - # Record and return the greater value of the two options + # Record and return the larger value of the two options mem[i][c] = max(no, yes) return mem[i][c] ``` @@ -229,24 +379,24 @@ After introducing memoization, **the time complexity depends on the number of su === "C++" ```cpp title="knapsack.cpp" - /* 0-1 Knapsack: Memoized search */ + /* 0-1 knapsack: Memoization search */ int knapsackDFSMem(vector &wgt, vector &val, vector> &mem, int i, int c) { - // If all items have been chosen or the knapsack has no remaining capacity, return value 0 + // If all items have been selected or knapsack has no remaining capacity, return value 0 if (i == 0 || c == 0) { return 0; } - // If there is a record, return it + // If there's a record, return it directly if (mem[i][c] != -1) { return mem[i][c]; } - // If exceeding the knapsack capacity, can only choose not to put it in the knapsack + // If exceeds knapsack capacity, can only choose not to put it in if (wgt[i - 1] > c) { return knapsackDFSMem(wgt, val, mem, i - 1, c); } // Calculate the maximum value of not putting in and putting in item 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]; - // Record and return the greater value of the two options + // Record and return the larger value of the two options mem[i][c] = max(no, yes); return mem[i][c]; } @@ -255,24 +405,24 @@ After introducing memoization, **the time complexity depends on the number of su === "Java" ```java title="knapsack.java" - /* 0-1 Knapsack: Memoized search */ + /* 0-1 knapsack: Memoization search */ int knapsackDFSMem(int[] wgt, int[] val, int[][] mem, int i, int c) { - // If all items have been chosen or the knapsack has no remaining capacity, return value 0 + // If all items have been selected or knapsack has no remaining capacity, return value 0 if (i == 0 || c == 0) { return 0; } - // If there is a record, return it + // If there's a record, return it directly if (mem[i][c] != -1) { return mem[i][c]; } - // If exceeding the knapsack capacity, can only choose not to put it in the knapsack + // If exceeds knapsack capacity, can only choose not to put it in if (wgt[i - 1] > c) { return knapsackDFSMem(wgt, val, mem, i - 1, c); } // Calculate the maximum value of not putting in and putting in item 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]; - // Record and return the greater value of the two options + // Record and return the larger value of the two options mem[i][c] = Math.max(no, yes); return mem[i][c]; } @@ -281,84 +431,291 @@ After introducing memoization, **the time complexity depends on the number of su === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + int KnapsackDFSMem(int[] weight, int[] val, int[][] mem, int i, int c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If there's a record, return it directly + if (mem[i][c] != -1) { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (weight[i - 1] > c) { + return KnapsackDFSMem(weight, val, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Record and return the larger value of the two options + mem[i][c] = Math.Max(no, yes); + return mem[i][c]; + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + func knapsackDFSMem(wgt, val []int, mem [][]int, i, c int) int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0 + } + // If there's a record, return it directly + if mem[i][c] != -1 { + return mem[i][c] + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i-1] > c { + return knapsackDFSMem(wgt, val, mem, i-1, c) + } + // Calculate the maximum value of not putting in and putting in item i + no := knapsackDFSMem(wgt, val, mem, i-1, c) + yes := knapsackDFSMem(wgt, val, mem, i-1, c-wgt[i-1]) + val[i-1] + // Return the larger value of the two options + mem[i][c] = int(math.Max(float64(no), float64(yes))) + return mem[i][c] + } ``` === "Swift" ```swift title="knapsack.swift" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + func knapsackDFSMem(wgt: [Int], val: [Int], mem: inout [[Int]], i: Int, c: Int) -> Int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0 + } + // If there's a record, return it directly + if mem[i][c] != -1 { + return mem[i][c] + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i - 1] > c { + return knapsackDFSMem(wgt: wgt, val: val, mem: &mem, i: i - 1, c: c) + } + // Calculate the maximum value of not putting in and putting in item 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] + // Record and return the larger value of the two options + mem[i][c] = max(no, yes) + return mem[i][c] + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + function knapsackDFSMem(wgt, val, mem, i, c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i === 0 || c === 0) { + return 0; + } + // If there's a record, return it directly + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Record and return the larger value of the two options + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + function knapsackDFSMem( + wgt: Array, + val: Array, + mem: Array>, + i: number, + c: number + ): number { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i === 0 || c === 0) { + return 0; + } + // If there's a record, return it directly + if (mem[i][c] !== -1) { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Record and return the larger value of the two options + mem[i][c] = Math.max(no, yes); + return mem[i][c]; + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + int knapsackDFSMem( + List wgt, + List val, + List> mem, + int i, + int c, + ) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If there's a record, return it directly + if (mem[i][c] != -1) { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Record and return the larger value of the two options + mem[i][c] = max(no, yes); + return mem[i][c]; + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dfs_mem} + /* 0-1 knapsack: Memoization search */ + fn knapsack_dfs_mem(wgt: &[i32], val: &[i32], mem: &mut Vec>, i: usize, c: usize) -> i32 { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if i == 0 || c == 0 { + return 0; + } + // If there's a record, return it directly + if mem[i][c] != -1 { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if wgt[i - 1] > c as i32 { + return knapsack_dfs_mem(wgt, val, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item 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]; + // Record and return the larger value of the two options + mem[i][c] = std::cmp::max(no, yes); + mem[i][c] + } ``` === "C" ```c title="knapsack.c" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + int knapsackDFSMem(int wgt[], int val[], int memCols, int **mem, int i, int c) { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0; + } + // If there's a record, return it directly + if (mem[i][c] != -1) { + return mem[i][c]; + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + } + // Calculate the maximum value of not putting in and putting in item i + int no = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c); + int yes = knapsackDFSMem(wgt, val, memCols, mem, i - 1, c - wgt[i - 1]) + val[i - 1]; + // Record and return the larger value of the two options + mem[i][c] = myMax(no, yes); + return mem[i][c]; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDFSMem} + /* 0-1 knapsack: Memoization search */ + fun knapsackDFSMem( + wgt: IntArray, + _val: IntArray, + mem: Array, + i: Int, + c: Int + ): Int { + // If all items have been selected or knapsack has no remaining capacity, return value 0 + if (i == 0 || c == 0) { + return 0 + } + // If there's a record, return it directly + if (mem[i][c] != -1) { + return mem[i][c] + } + // If exceeds knapsack capacity, can only choose not to put it in + if (wgt[i - 1] > c) { + return knapsackDFSMem(wgt, _val, mem, i - 1, c) + } + // Calculate the maximum value of not putting in and putting in item i + val no = knapsackDFSMem(wgt, _val, mem, i - 1, c) + val yes = knapsackDFSMem(wgt, _val, mem, i - 1, c - wgt[i - 1]) + _val[i - 1] + // Record and return the larger value of the two options + mem[i][c] = max(no, yes) + return mem[i][c] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dfs_mem} + ### 0-1 knapsack: memoization search ### + def knapsack_dfs_mem(wgt, val, mem, i, c) + # If all items have been selected or knapsack has no remaining capacity, return value 0 + return 0 if i == 0 || c == 0 + # If there's a record, return it directly + return mem[i][c] if mem[i][c] != -1 + # If exceeds knapsack capacity, can only choose not to put it in + return knapsack_dfs_mem(wgt, val, mem, i - 1, c) if wgt[i - 1] > c + # Calculate the maximum value of not putting in and putting in item 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] + # Record and return the larger value of the two options + mem[i][c] = [no, yes].max + end ``` -=== "Zig" +Figure 14-19 shows the search branches pruned in memoization. - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDFSMem} - ``` +![Memoization recursion tree for 0-1 knapsack problem](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } -Figure 14-19 shows the search branches that are pruned in memoized search. +

Figure 14-19   Memoization recursion tree for 0-1 knapsack problem

-![The memoized search recursive tree of the 0-1 knapsack problem](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } +### 3.   Method 3: Dynamic Programming -

Figure 14-19   The memoized search recursive tree of the 0-1 knapsack problem

- -### 3.   Method three: Dynamic programming - -Dynamic programming essentially involves filling the $dp$ table during the state transition, the code is shown in Figure 14-20: +Dynamic programming is essentially the process of filling the $dp$ table during state transitions. The code is as follows: === "Python" ```python title="knapsack.py" def knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: - """0-1 Knapsack: Dynamic programming""" + """0-1 knapsack: Dynamic programming""" n = len(wgt) # Initialize dp table dp = [[0] * (cap + 1) for _ in range(n + 1)] @@ -366,10 +723,10 @@ Dynamic programming essentially involves filling the $dp$ table during the state for i in range(1, n + 1): for c in range(1, cap + 1): if wgt[i - 1] > c: - # If exceeding the knapsack capacity, do not choose item i + # If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c] else: - # The greater value between not choosing and choosing item i + # The larger value between not selecting and selecting item i dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) return dp[n][cap] ``` @@ -377,7 +734,7 @@ Dynamic programming essentially involves filling the $dp$ table during the state === "C++" ```cpp title="knapsack.cpp" - /* 0-1 Knapsack: Dynamic programming */ + /* 0-1 knapsack: Dynamic programming */ int knapsackDP(vector &wgt, vector &val, int cap) { int n = wgt.size(); // Initialize dp table @@ -386,10 +743,10 @@ Dynamic programming essentially involves filling the $dp$ table during the state for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); } } @@ -401,7 +758,7 @@ Dynamic programming essentially involves filling the $dp$ table during the state === "Java" ```java title="knapsack.java" - /* 0-1 Knapsack: Dynamic programming */ + /* 0-1 knapsack: Dynamic programming */ int knapsackDP(int[] wgt, int[] val, int cap) { int n = wgt.length; // Initialize dp table @@ -410,10 +767,10 @@ Dynamic programming essentially involves filling the $dp$ table during the state for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[i][c] = Math.max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); } } @@ -425,73 +782,275 @@ Dynamic programming essentially involves filling the $dp$ table during the state === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDP} + /* 0-1 knapsack: Dynamic programming */ + int KnapsackDP(int[] weight, int[] val, int cap) { + int n = weight.Length; + // Initialize dp table + int[,] dp = new int[n + 1, cap + 1]; + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (weight[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i, c] = dp[i - 1, c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + func knapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // Initialize dp table + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // State transition + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i-1][c] + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + func knapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // Initialize dp table + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // State transition + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + function knapsackDP(wgt, val, cap) { + const n = wgt.length; + // Initialize dp table + const dp = Array(n + 1) + .fill(0) + .map(() => Array(cap + 1).fill(0)); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + function knapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + int knapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // Initialize dp table + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsack_dp} + /* 0-1 knapsack: Dynamic programming */ + fn knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // Initialize dp table + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // State transition + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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} + /* 0-1 knapsack: Dynamic programming */ + int knapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // Initialize dp table + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item i + dp[i][c] = myMax(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDP} + /* 0-1 knapsack: Dynamic programming */ + fun knapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // Initialize dp table + val dp = Array(n + 1) { IntArray(cap + 1) } + // State transition + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + } else { + // The larger value between not selecting and selecting item i + dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dp} + ### 0-1 knapsack: dynamic programming ### + def knapsack_dp(wgt, val, cap) + n = wgt.length + # Initialize dp table + dp = Array.new(n + 1) { Array.new(cap + 1, 0) } + # State transition + for i in 1...(n + 1) + for c in 1...(cap + 1) + if wgt[i - 1] > c + # If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + else + # The larger value between not selecting and selecting item i + dp[i][c] = [dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[n][cap] + end ``` -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDP} - ``` - -As shown in Figure 14-20, both the time complexity and space complexity are determined by the size of the array `dp`, i.e., $O(n \times cap)$. +As shown in Figure 14-20, both time complexity and space complexity are determined by the size of the array `dp`, which is $O(n \times cap)$. === "<1>" - ![The dynamic programming process of the 0-1 knapsack problem](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" } + ![Dynamic programming process for 0-1 knapsack problem](knapsack_problem.assets/knapsack_dp_step1.png){ class="animation-figure" } === "<2>" ![knapsack_dp_step2](knapsack_problem.assets/knapsack_dp_step2.png){ class="animation-figure" } @@ -532,21 +1091,21 @@ As shown in Figure 14-20, both the time complexity and space complexity are dete === "<14>" ![knapsack_dp_step14](knapsack_problem.assets/knapsack_dp_step14.png){ class="animation-figure" } -

Figure 14-20   The dynamic programming process of the 0-1 knapsack problem

+

Figure 14-20   Dynamic programming process for 0-1 knapsack problem

-### 4.   Space optimization +### 4.   Space Optimization -Since each state is only related to the state in the row above it, we can use two arrays to roll forward, reducing the space complexity from $O(n^2)$ to $O(n)$. +Since each state is only related to the state in the row above it, we can use two arrays rolling forward to reduce the space complexity from $O(n^2)$ to $O(n)$. -Further thinking, can we use just one array to achieve space optimization? It can be observed that each state is transferred from the cell directly above or from the upper left cell. If there is only one array, when starting to traverse the $i$-th row, that array still stores the state of row $i-1$. +Further thinking, can we achieve space optimization using just one array? Observing, we can see that each state is transferred from the cell directly above or the cell in the upper-left. If there is only one array, when we start traversing row $i$, that array still stores the state of row $i-1$. -- If using normal order traversal, then when traversing to $dp[i, j]$, the values from the upper left $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ may have already been overwritten, thus the correct state transition result cannot be obtained. -- If using reverse order traversal, there will be no overwriting problem, and the state transition can be conducted correctly. +- If using forward traversal, then when traversing to $dp[i, j]$, the values in the upper-left $dp[i-1, 1]$ ~ $dp[i-1, j-1]$ may have already been overwritten, thus preventing correct state transition. +- If using reverse traversal, there will be no overwriting issue, and state transition can proceed correctly. -The figures below show the transition process from row $i = 1$ to row $i = 2$ in a single array. Please think about the differences between normal order traversal and reverse order traversal. +Figure 14-21 shows the transition process from row $i = 1$ to row $i = 2$ using a single array. Please consider the difference between forward and reverse traversal. === "<1>" - ![The space-optimized dynamic programming process of the 0-1 knapsack](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } + ![Space-optimized dynamic programming process for 0-1 knapsack](knapsack_problem.assets/knapsack_dp_comp_step1.png){ class="animation-figure" } === "<2>" ![knapsack_dp_comp_step2](knapsack_problem.assets/knapsack_dp_comp_step2.png){ class="animation-figure" } @@ -563,15 +1122,15 @@ The figures below show the transition process from row $i = 1$ to row $i = 2$ in === "<6>" ![knapsack_dp_comp_step6](knapsack_problem.assets/knapsack_dp_comp_step6.png){ class="animation-figure" } -

Figure 14-21   The space-optimized dynamic programming process of the 0-1 knapsack

+

Figure 14-21   Space-optimized dynamic programming process for 0-1 knapsack

-In the code implementation, we only need to delete the first dimension $i$ of the array `dp` and change the inner loop to reverse traversal: +In the code implementation, we simply need to delete the first dimension $i$ of the array `dp` and change the inner loop to reverse traversal: === "Python" ```python title="knapsack.py" def knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """0-1 Knapsack: Space-optimized dynamic programming""" + """0-1 knapsack: Space-optimized dynamic programming""" n = len(wgt) # Initialize dp table dp = [0] * (cap + 1) @@ -580,10 +1139,10 @@ In the code implementation, we only need to delete the first dimension $i$ of th # Traverse in reverse order for c in range(cap, 0, -1): if wgt[i - 1] > c: - # If exceeding the knapsack capacity, do not choose item i + # If exceeds knapsack capacity, don't select item i dp[c] = dp[c] else: - # The greater value between not choosing and choosing item i + # The larger value between not selecting and selecting item i dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) return dp[cap] ``` @@ -591,7 +1150,7 @@ In the code implementation, we only need to delete the first dimension $i$ of th === "C++" ```cpp title="knapsack.cpp" - /* 0-1 Knapsack: Space-optimized dynamic programming */ + /* 0-1 knapsack: Space-optimized dynamic programming */ int knapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); // Initialize dp table @@ -601,7 +1160,7 @@ In the code implementation, we only need to delete the first dimension $i$ of th // Traverse in reverse order for (int c = cap; c >= 1; c--) { if (wgt[i - 1] <= c) { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -613,7 +1172,7 @@ In the code implementation, we only need to delete the first dimension $i$ of th === "Java" ```java title="knapsack.java" - /* 0-1 Knapsack: Space-optimized dynamic programming */ + /* 0-1 knapsack: Space-optimized dynamic programming */ int knapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; // Initialize dp table @@ -623,7 +1182,7 @@ In the code implementation, we only need to delete the first dimension $i$ of th // Traverse in reverse order for (int c = cap; c >= 1; c--) { if (wgt[i - 1] <= c) { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -635,65 +1194,232 @@ In the code implementation, we only need to delete the first dimension $i$ of th === "C#" ```csharp title="knapsack.cs" - [class]{knapsack}-[func]{KnapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + int KnapsackDPComp(int[] weight, int[] val, int cap) { + int n = weight.Length; + // Initialize dp table + int[] dp = new int[cap + 1]; + // State transition + for (int i = 1; i <= n; i++) { + // Traverse in reverse order + for (int c = cap; c > 0; c--) { + if (weight[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = Math.Max(dp[c], dp[c - weight[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Go" ```go title="knapsack.go" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + func knapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // Initialize dp table + dp := make([]int, cap+1) + // State transition + for i := 1; i <= n; i++ { + // Traverse in reverse order + for c := cap; c >= 1; c-- { + if wgt[i-1] <= c { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + func knapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // Initialize dp table + var dp = Array(repeating: 0, count: cap + 1) + // State transition + for i in 1 ... n { + // Traverse in reverse order + for c in (1 ... cap).reversed() { + if wgt[i - 1] <= c { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } ``` === "JS" ```javascript title="knapsack.js" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + function knapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // Initialize dp table + const dp = Array(cap + 1).fill(0); + // State transition + for (let i = 1; i <= n; i++) { + // Traverse in reverse order + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // The larger value between not selecting and selecting item i + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="knapsack.ts" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + function knapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // Initialize dp table + const dp = Array(cap + 1).fill(0); + // State transition + for (let i = 1; i <= n; i++) { + // Traverse in reverse order + for (let c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // The larger value between not selecting and selecting item i + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Dart" ```dart title="knapsack.dart" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + int knapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // Initialize dp table + List dp = List.filled(cap + 1, 0); + // State transition + for (int i = 1; i <= n; i++) { + // Traverse in reverse order + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" ```rust title="knapsack.rs" - [class]{}-[func]{knapsack_dp_comp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + fn knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // Initialize dp table + let mut dp = vec![0; cap + 1]; + // State transition + for i in 1..=n { + // Traverse in reverse order + for c in (1..=cap).rev() { + if wgt[i - 1] <= c as i32 { + // The larger value between not selecting and selecting item 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} + /* 0-1 knapsack: Space-optimized dynamic programming */ + int knapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // Initialize dp table + int *dp = calloc(cap + 1, sizeof(int)); + // State transition + for (int i = 1; i <= n; i++) { + // Traverse in reverse order + for (int c = cap; c >= 1; c--) { + if (wgt[i - 1] <= c) { + // The larger value between not selecting and selecting item i + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="knapsack.kt" - [class]{}-[func]{knapsackDPComp} + /* 0-1 knapsack: Space-optimized dynamic programming */ + fun knapsackDPComp(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // Initialize dp table + val dp = IntArray(cap + 1) + // State transition + for (i in 1..n) { + // Traverse in reverse order + for (c in cap downTo 1) { + if (wgt[i - 1] <= c) { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[cap] + } ``` === "Ruby" ```ruby title="knapsack.rb" - [class]{}-[func]{knapsack_dp_comp} - ``` - -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDPComp} + ### 0-1 knapsack: space-optimized DP ### + def knapsack_dp_comp(wgt, val, cap) + n = wgt.length + # Initialize dp table + dp = Array.new(cap + 1, 0) + # State transition + for i in 1...(n + 1) + # Traverse in reverse order + for c in cap.downto(1) + if wgt[i - 1] > c + # If exceeds knapsack capacity, don't select item i + dp[c] = dp[c] + else + # The larger value between not selecting and selecting item i + dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[cap] + end ``` diff --git a/en/docs/chapter_dynamic_programming/summary.md b/en/docs/chapter_dynamic_programming/summary.md index 83c38d4aa..f7bded759 100644 --- a/en/docs/chapter_dynamic_programming/summary.md +++ b/en/docs/chapter_dynamic_programming/summary.md @@ -4,24 +4,26 @@ comments: true # 14.7   Summary -- Dynamic programming decomposes problems and improves computational efficiency by avoiding redundant computations through storing solutions of subproblems. -- Without considering time, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree has many overlapping subproblems, resulting in very low efficiency. By introducing a memorization list, it's possible to store solutions of all computed subproblems, ensuring that overlapping subproblems are only computed once. -- Memorization search is a top-down recursive solution, whereas dynamic programming corresponds to a bottom-up iterative approach, akin to "filling out a table." Since the current state only depends on certain local states, we can eliminate one dimension of the dp table to reduce space complexity. -- Decomposition of subproblems is a universal algorithmic approach, differing in characteristics among divide and conquer, dynamic programming, and backtracking. -- Dynamic programming problems have three main characteristics: overlapping subproblems, optimal substructure, and no aftereffects. -- If the optimal solution of the original problem can be constructed from the optimal solutions of its subproblems, it has an optimal substructure. -- No aftereffects mean that the future development of a state depends only on the current state and not on all past states experienced. Many combinatorial optimization problems do not have this property and cannot be quickly solved using dynamic programming. +### 1.   Key Review + +- Dynamic programming decomposes problems and avoids redundant computation by storing the solutions to subproblems, thereby significantly improving computational efficiency. +- Without considering time constraints, all dynamic programming problems can be solved using backtracking (brute force search), but the recursion tree contains a large number of overlapping subproblems, resulting in extremely low efficiency. By introducing a memo list, we can store the solutions to all computed subproblems, ensuring that overlapping subproblems are only computed once. +- Memoization is a top-down recursive solution, while the corresponding dynamic programming is a bottom-up iterative solution, similar to "filling in a table". Since the current state only depends on certain local states, we can eliminate one dimension of the $dp$ table to reduce space complexity. +- Subproblem decomposition is a general algorithmic approach, with different properties in divide and conquer, dynamic programming, and backtracking. +- Dynamic programming problems have three major characteristics: overlapping subproblems, optimal substructure, and no aftereffects. +- If the optimal solution to the original problem can be constructed from the optimal solutions to the subproblems, then it has optimal substructure. +- No aftereffects means that for a given state, its future development is only related to that state and has nothing to do with all past states. Many combinatorial optimization problems do not have no aftereffects and cannot be quickly solved using dynamic programming. **Knapsack problem** -- The knapsack problem is one of the most typical dynamic programming problems, with variants including the 0-1 knapsack, unbounded knapsack, and multiple knapsacks. -- The state definition of the 0-1 knapsack is the maximum value in a knapsack of capacity $c$ with the first $i$ items. Based on decisions not to include or to include an item in the knapsack, optimal substructures can be identified and state transition equations constructed. In space optimization, since each state depends on the state directly above and to the upper left, the list should be traversed in reverse order to avoid overwriting the upper left state. -- In the unbounded knapsack problem, there is no limit on the number of each kind of item that can be chosen, thus the state transition for including items differs from the 0-1 knapsack. Since the state depends on the state directly above and to the left, space optimization should involve forward traversal. -- The coin change problem is a variant of the unbounded knapsack problem, shifting from seeking the “maximum” value to seeking the “minimum” number of coins, thus the state transition equation should change $\max()$ to $\min()$. From pursuing “not exceeding” the capacity of the knapsack to seeking exactly the target amount, thus use $amt + 1$ to represent the invalid solution of “unable to make up the target amount.” -- Coin Change Problem II shifts from seeking the “minimum number of coins” to seeking the “number of coin combinations,” changing the state transition equation accordingly from $\min()$ to summation operator. +- The knapsack problem is one of the most typical dynamic programming problems, with variants such as the 0-1 knapsack, unbounded knapsack, and multiple knapsack. +- The state definition for the 0-1 knapsack is the maximum value among the first $i$ items in a knapsack of capacity $c$. Based on the two decisions of not putting an item in the knapsack and putting it in, the optimal substructure can be identified and the state transition equation constructed. In space optimization, since each state depends on the state directly above and to the upper-left, the list needs to be traversed in reverse order to avoid overwriting the upper-left state. +- The unbounded knapsack problem has no limit on the selection quantity of each type of item, so the state transition for choosing to put in an item differs from the 0-1 knapsack problem. Since the state depends on the state directly above and directly to the left, space optimization should use forward traversal. +- The coin change problem is a variant of the unbounded knapsack problem. It changes from seeking the "maximum" value to seeking the "minimum" number of coins, so $\max()$ in the state transition equation should be changed to $\min()$. It changes from seeking "not exceeding" the knapsack capacity to seeking "exactly" making up the target amount, so $amt + 1$ is used to represent the invalid solution of "unable to make up the target amount". +- Coin change problem II changes from seeking the "minimum number of coins" to seeking the "number of coin combinations", so the state transition equation correspondingly changes from $\min()$ to a summation operator. **Edit distance problem** -- Edit distance (Levenshtein distance) measures the similarity between two strings, defined as the minimum number of editing steps needed to change one string into another, with editing operations including adding, deleting, or replacing. -- The state definition for the edit distance problem is the minimum number of editing steps needed to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: add, delete, replace, each with their corresponding residual subproblems. From this, optimal substructures can be identified, and state transition equations built. When $s[i] = t[j]$, no editing of the current character is necessary. -- In edit distance, the state depends on the state directly above, to the left, and to the upper left. Therefore, after space optimization, neither forward nor reverse traversal can correctly perform state transitions. To address this, we use a variable to temporarily store the upper left state, making it equivalent to the situation in the unbounded knapsack problem, allowing for forward traversal after space optimization. +- Edit distance (Levenshtein distance) is used to measure the similarity between two strings, defined as the minimum number of edit steps from one string to another, with edit operations including insert, delete, and replace. +- The state definition for the edit distance problem is the minimum number of edit steps required to change the first $i$ characters of $s$ into the first $j$ characters of $t$. When $s[i] \ne t[j]$, there are three decisions: insert, delete, replace, each with corresponding remaining subproblems. From this, the optimal substructure can be identified and the state transition equation constructed. When $s[i] = t[j]$, no edit is required for the current character. +- In edit distance, the state depends on the state directly above, directly to the left, and to the upper-left, so after space optimization, neither forward nor reverse traversal can correctly perform state transitions. For this reason, we use a variable to temporarily store the upper-left state, thus transforming to a situation equivalent to the unbounded knapsack problem, allowing for forward traversal after space optimization. diff --git a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index d3f49df4e..559237fbd 100644 --- a/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/en/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -2,47 +2,47 @@ comments: true --- -# 14.5   Unbounded knapsack problem +# 14.5   Unbounded Knapsack Problem In this section, we first solve another common knapsack problem: the unbounded knapsack, and then explore a special case of it: the coin change problem. -## 14.5.1   Unbounded knapsack problem +## 14.5.1   Unbounded Knapsack Problem !!! question - Given $n$ items, where the weight of the $i^{th}$ item is $wgt[i-1]$ and its value is $val[i-1]$, and a backpack with a capacity of $cap$. **Each item can be selected multiple times**. What is the maximum value of the items that can be put into the backpack without exceeding its capacity? See the example below. + Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. **Each item can be selected multiple times**. What is the maximum value that can be placed in the knapsack within the capacity limit? An example is shown in Figure 14-22. -![Example data for the unbounded knapsack problem](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png){ class="animation-figure" } +![Example data for unbounded knapsack problem](unbounded_knapsack_problem.assets/unbounded_knapsack_example.png){ class="animation-figure" } -

Figure 14-22   Example data for the unbounded knapsack problem

+

Figure 14-22   Example data for unbounded knapsack problem

-### 1.   Dynamic programming approach +### 1.   Dynamic Programming Approach -The unbounded knapsack problem is very similar to the 0-1 knapsack problem, **the only difference being that there is no limit on the number of times an item can be chosen**. +The unbounded knapsack problem is very similar to the 0-1 knapsack problem, **differing only in that there is no limit on the number of times an item can be selected**. -- In the 0-1 knapsack problem, there is only one of each item, so after placing item $i$ into the backpack, you can only choose from the previous $i-1$ items. -- In the unbounded knapsack problem, the quantity of each item is unlimited, so after placing item $i$ in the backpack, **you can still choose from the previous $i$ items**. +- In the 0-1 knapsack problem, there is only one of each type of item, so after placing item $i$ in the knapsack, we can only choose from the first $i-1$ items. +- In the unbounded knapsack problem, the quantity of each type of item is unlimited, so after placing item $i$ in the knapsack, **we can still choose from the first $i$ items**. -Under the rules of the unbounded knapsack problem, the state $[i, c]$ can change in two ways. +Under the rules of the unbounded knapsack problem, the changes in state $[i, c]$ are divided into two cases. -- **Not putting item $i$ in**: As with the 0-1 knapsack problem, transition to $[i-1, c]$. -- **Putting item $i$ in**: Unlike the 0-1 knapsack problem, transition to $[i, c-wgt[i-1]]$. +- **Not putting item $i$**: Same as the 0-1 knapsack problem, transfer to $[i-1, c]$. +- **Putting item $i$**: Different from the 0-1 knapsack problem, transfer to $[i, c-wgt[i-1]]$. -The state transition equation thus becomes: +Thus, the state transition equation becomes: $$ dp[i, c] = \max(dp[i-1, c], dp[i, c - wgt[i-1]] + val[i-1]) $$ -### 2.   Code implementation +### 2.   Code Implementation -Comparing the code for the two problems, the state transition changes from $i-1$ to $i$, the rest is completely identical: +Comparing the code for the two problems, there is one change in state transition from $i-1$ to $i$, with everything else identical: === "Python" ```python title="unbounded_knapsack.py" def unbounded_knapsack_dp(wgt: list[int], val: list[int], cap: int) -> int: - """Complete knapsack: Dynamic programming""" + """Unbounded knapsack: Dynamic programming""" n = len(wgt) # Initialize dp table dp = [[0] * (cap + 1) for _ in range(n + 1)] @@ -50,10 +50,10 @@ Comparing the code for the two problems, the state transition changes from $i-1$ for i in range(1, n + 1): for c in range(1, cap + 1): if wgt[i - 1] > c: - # If exceeding the knapsack capacity, do not choose item i + # If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c] else: - # The greater value between not choosing and choosing item i + # The larger value between not selecting and selecting item i dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]) return dp[n][cap] ``` @@ -61,7 +61,7 @@ Comparing the code for the two problems, the state transition changes from $i-1$ === "C++" ```cpp title="unbounded_knapsack.cpp" - /* Complete knapsack: Dynamic programming */ + /* Unbounded knapsack: Dynamic programming */ int unboundedKnapsackDP(vector &wgt, vector &val, int cap) { int n = wgt.size(); // Initialize dp table @@ -70,10 +70,10 @@ Comparing the code for the two problems, the state transition changes from $i-1$ for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); } } @@ -85,7 +85,7 @@ Comparing the code for the two problems, the state transition changes from $i-1$ === "Java" ```java title="unbounded_knapsack.java" - /* Complete knapsack: Dynamic programming */ + /* Unbounded knapsack: Dynamic programming */ int unboundedKnapsackDP(int[] wgt, int[] val, int cap) { int n = wgt.length; // Initialize dp table @@ -94,10 +94,10 @@ Comparing the code for the two problems, the state transition changes from $i-1$ for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[i][c] = dp[i - 1][c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[i][c] = Math.max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); } } @@ -109,77 +109,276 @@ Comparing the code for the two problems, the state transition changes from $i-1$ === "C#" ```csharp title="unbounded_knapsack.cs" - [class]{unbounded_knapsack}-[func]{UnboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + int UnboundedKnapsackDP(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // Initialize dp table + int[,] dp = new int[n + 1, cap + 1]; + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i, c] = dp[i - 1, c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + func unboundedKnapsackDP(wgt, val []int, cap int) int { + n := len(wgt) + // Initialize dp table + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, cap+1) + } + // State transition + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i-1][c] + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + func unboundedKnapsackDP(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // Initialize dp table + var dp = Array(repeating: Array(repeating: 0, count: cap + 1), count: n + 1) + // State transition + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + function unboundedKnapsackDP(wgt, val, cap) { + const n = wgt.length; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + function unboundedKnapsackDP( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: cap + 1 }, () => 0) + ); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + int unboundedKnapsackDP(List wgt, List val, int cap) { + int n = wgt.length; + // Initialize dp table + List> dp = List.generate(n + 1, (index) => List.filled(cap + 1, 0)); + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unbounded_knapsack_dp} + /* Unbounded knapsack: Dynamic programming */ + fn unbounded_knapsack_dp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // Initialize dp table + let mut dp = vec![vec![0; cap + 1]; n + 1]; + // State transition + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item 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} + /* Unbounded knapsack: Dynamic programming */ + int unboundedKnapsackDP(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // Initialize dp table + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(cap + 1, sizeof(int)); + } + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c]; + } else { + // The larger value between not selecting and selecting item i + dp[i][c] = myMax(dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[n][cap]; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + return res; + } ``` === "Kotlin" ```kotlin title="unbounded_knapsack.kt" - [class]{}-[func]{unboundedKnapsackDP} + /* Unbounded knapsack: Dynamic programming */ + fun unboundedKnapsackDP(wgt: IntArray, _val: IntArray, cap: Int): Int { + val n = wgt.size + // Initialize dp table + val dp = Array(n + 1) { IntArray(cap + 1) } + // State transition + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + } else { + // The larger value between not selecting and selecting item i + dp[i][c] = max(dp[i - 1][c], dp[i][c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[n][cap] + } ``` === "Ruby" ```ruby title="unbounded_knapsack.rb" - [class]{}-[func]{unbounded_knapsack_dp} + ### Unbounded knapsack: dynamic programming ### + def unbounded_knapsack_dp(wgt, val, cap) + n = wgt.length + # Initialize dp table + dp = Array.new(n + 1) { Array.new(cap + 1, 0) } + # State transition + for i in 1...(n + 1) + for c in 1...(cap + 1) + if wgt[i - 1] > c + # If exceeds knapsack capacity, don't select item i + dp[i][c] = dp[i - 1][c] + else + # The larger value between not selecting and selecting item i + dp[i][c] = [dp[i - 1][c], dp[i][c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[n][cap] + end ``` -=== "Zig" +### 3.   Space Optimization - ```zig title="unbounded_knapsack.zig" - [class]{}-[func]{unboundedKnapsackDP} - ``` +Since the current state is transferred from states on the left and above, **after space optimization, each row in the $dp$ table should be traversed in forward order**. -### 3.   Space optimization - -Since the current state comes from the state to the left and above, **the space-optimized solution should perform a forward traversal for each row in the $dp$ table**. - -This traversal order is the opposite of that for the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference. +This traversal order is exactly opposite to the 0-1 knapsack. Please refer to Figure 14-23 to understand the difference between the two. === "<1>" - ![Dynamic programming process for the unbounded knapsack problem after space optimization](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png){ class="animation-figure" } + ![Space-optimized dynamic programming process for unbounded knapsack problem](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step1.png){ class="animation-figure" } === "<2>" ![unbounded_knapsack_dp_comp_step2](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step2.png){ class="animation-figure" } @@ -196,27 +395,27 @@ This traversal order is the opposite of that for the 0-1 knapsack. Please refer === "<6>" ![unbounded_knapsack_dp_comp_step6](unbounded_knapsack_problem.assets/unbounded_knapsack_dp_comp_step6.png){ class="animation-figure" } -

Figure 14-23   Dynamic programming process for the unbounded knapsack problem after space optimization

+

Figure 14-23   Space-optimized dynamic programming process for unbounded knapsack problem

-The code implementation is quite simple, just remove the first dimension of the array `dp`: +The code implementation is relatively simple, just delete the first dimension of the array `dp`: === "Python" ```python title="unbounded_knapsack.py" def unbounded_knapsack_dp_comp(wgt: list[int], val: list[int], cap: int) -> int: - """Complete knapsack: Space-optimized dynamic programming""" + """Unbounded knapsack: Space-optimized dynamic programming""" n = len(wgt) # Initialize dp table dp = [0] * (cap + 1) # State transition for i in range(1, n + 1): - # Traverse in order + # Traverse in forward order for c in range(1, cap + 1): if wgt[i - 1] > c: - # If exceeding the knapsack capacity, do not choose item i + # If exceeds knapsack capacity, don't select item i dp[c] = dp[c] else: - # The greater value between not choosing and choosing item i + # The larger value between not selecting and selecting item i dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) return dp[cap] ``` @@ -224,7 +423,7 @@ The code implementation is quite simple, just remove the first dimension of the === "C++" ```cpp title="unbounded_knapsack.cpp" - /* Complete knapsack: Space-optimized dynamic programming */ + /* Unbounded knapsack: Space-optimized dynamic programming */ int unboundedKnapsackDPComp(vector &wgt, vector &val, int cap) { int n = wgt.size(); // Initialize dp table @@ -233,10 +432,10 @@ The code implementation is quite simple, just remove the first dimension of the for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[c] = dp[c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -248,7 +447,7 @@ The code implementation is quite simple, just remove the first dimension of the === "Java" ```java title="unbounded_knapsack.java" - /* Complete knapsack: Space-optimized dynamic programming */ + /* Unbounded knapsack: Space-optimized dynamic programming */ int unboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { int n = wgt.length; // Initialize dp table @@ -257,10 +456,10 @@ The code implementation is quite simple, just remove the first dimension of the for (int i = 1; i <= n; i++) { for (int c = 1; c <= cap; c++) { if (wgt[i - 1] > c) { - // If exceeding the knapsack capacity, do not choose item i + // If exceeds knapsack capacity, don't select item i dp[c] = dp[c]; } else { - // The greater value between not choosing and choosing item i + // The larger value between not selecting and selecting item i dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); } } @@ -272,117 +471,303 @@ The code implementation is quite simple, just remove the first dimension of the === "C#" ```csharp title="unbounded_knapsack.cs" - [class]{unbounded_knapsack}-[func]{UnboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + int UnboundedKnapsackDPComp(int[] wgt, int[] val, int cap) { + int n = wgt.Length; + // Initialize dp table + int[] dp = new int[cap + 1]; + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = Math.Max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Go" ```go title="unbounded_knapsack.go" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + func unboundedKnapsackDPComp(wgt, val []int, cap int) int { + n := len(wgt) + // Initialize dp table + dp := make([]int, cap+1) + // State transition + for i := 1; i <= n; i++ { + for c := 1; c <= cap; c++ { + if wgt[i-1] > c { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c] + } else { + // The larger value between not selecting and selecting item 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" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + func unboundedKnapsackDPComp(wgt: [Int], val: [Int], cap: Int) -> Int { + let n = wgt.count + // Initialize dp table + var dp = Array(repeating: 0, count: cap + 1) + // State transition + for i in 1 ... n { + for c in 1 ... cap { + if wgt[i - 1] > c { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c] + } else { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) + } + } + } + return dp[cap] + } ``` === "JS" ```javascript title="unbounded_knapsack.js" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + function unboundedKnapsackDPComp(wgt, val, cap) { + const n = wgt.length; + // Initialize dp table + const dp = Array.from({ length: cap + 1 }, () => 0); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "TS" ```typescript title="unbounded_knapsack.ts" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + function unboundedKnapsackDPComp( + wgt: Array, + val: Array, + cap: number + ): number { + const n = wgt.length; + // Initialize dp table + const dp = Array.from({ length: cap + 1 }, () => 0); + // State transition + for (let i = 1; i <= n; i++) { + for (let c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = Math.max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Dart" ```dart title="unbounded_knapsack.dart" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + int unboundedKnapsackDPComp(List wgt, List val, int cap) { + int n = wgt.length; + // Initialize dp table + List dp = List.filled(cap + 1, 0); + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + return dp[cap]; + } ``` === "Rust" ```rust title="unbounded_knapsack.rs" - [class]{}-[func]{unbounded_knapsack_dp_comp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + fn unbounded_knapsack_dp_comp(wgt: &[i32], val: &[i32], cap: usize) -> i32 { + let n = wgt.len(); + // Initialize dp table + let mut dp = vec![0; cap + 1]; + // State transition + for i in 1..=n { + for c in 1..=cap { + if wgt[i - 1] > c as i32 { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item 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} + /* Unbounded knapsack: Space-optimized dynamic programming */ + int unboundedKnapsackDPComp(int wgt[], int val[], int cap, int wgtSize) { + int n = wgtSize; + // Initialize dp table + int *dp = calloc(cap + 1, sizeof(int)); + // State transition + for (int i = 1; i <= n; i++) { + for (int c = 1; c <= cap; c++) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c]; + } else { + // The larger value between not selecting and selecting item i + dp[c] = myMax(dp[c], dp[c - wgt[i - 1]] + val[i - 1]); + } + } + } + int res = dp[cap]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="unbounded_knapsack.kt" - [class]{}-[func]{unboundedKnapsackDPComp} + /* Unbounded knapsack: Space-optimized dynamic programming */ + fun unboundedKnapsackDPComp( + wgt: IntArray, + _val: IntArray, + cap: Int + ): Int { + val n = wgt.size + // Initialize dp table + val dp = IntArray(cap + 1) + // State transition + for (i in 1..n) { + for (c in 1..cap) { + if (wgt[i - 1] > c) { + // If exceeds knapsack capacity, don't select item i + dp[c] = dp[c] + } else { + // The larger value between not selecting and selecting item i + dp[c] = max(dp[c], dp[c - wgt[i - 1]] + _val[i - 1]) + } + } + } + return dp[cap] + } ``` === "Ruby" ```ruby title="unbounded_knapsack.rb" - [class]{}-[func]{unbounded_knapsack_dp_comp} + ### Unbounded knapsack: space-optimized DP ### + def unbounded_knapsack_dp_comp(wgt, val, cap) + n = wgt.length + # Initialize dp table + dp = Array.new(cap + 1, 0) + # State transition + for i in 1...(n + 1) + # Traverse in forward order + for c in 1...(cap + 1) + if wgt[i -1] > c + # If exceeds knapsack capacity, don't select item i + dp[c] = dp[c] + else + # The larger value between not selecting and selecting item i + dp[c] = [dp[c], dp[c - wgt[i - 1]] + val[i - 1]].max + end + end + end + dp[cap] + end ``` -=== "Zig" +## 14.5.2   Coin Change Problem - ```zig title="unbounded_knapsack.zig" - [class]{}-[func]{unboundedKnapsackDPComp} - ``` - -## 14.5.2   Coin change problem - -The knapsack problem is a representative of a large class of dynamic programming problems and has many variants, such as the coin change problem. +The knapsack problem represents a large class of dynamic programming problems and has many variants, such as the coin change problem. !!! question - Given $n$ types of coins, the denomination of the $i^{th}$ type of coin is $coins[i - 1]$, and the target amount is $amt$. **Each type of coin can be selected multiple times**. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$. See the example below. + Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$. **Each type of coin can be selected multiple times**. What is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$. An example is shown in Figure 14-24. -![Example data for the coin change problem](unbounded_knapsack_problem.assets/coin_change_example.png){ class="animation-figure" } +![Example data for coin change problem](unbounded_knapsack_problem.assets/coin_change_example.png){ class="animation-figure" } -

Figure 14-24   Example data for the coin change problem

+

Figure 14-24   Example data for coin change problem

-### 1.   Dynamic programming approach +### 1.   Dynamic Programming Approach -**The coin change can be seen as a special case of the unbounded knapsack problem**, sharing the following similarities and differences. +**The coin change problem can be viewed as a special case of the unbounded knapsack problem**, with the following connections and differences. -- The two problems can be converted into each other: "item" corresponds to "coin", "item weight" corresponds to "coin denomination", and "backpack capacity" corresponds to "target amount". -- The optimization goals are opposite: the unbounded knapsack problem aims to maximize the value of items, while the coin change problem aims to minimize the number of coins. -- The unbounded knapsack problem seeks solutions "not exceeding" the backpack capacity, while the coin change seeks solutions that "exactly" make up the target amount. +- The two problems can be converted to each other: "item" corresponds to "coin", "item weight" corresponds to "coin denomination", and "knapsack capacity" corresponds to "target amount". +- The optimization goals are opposite: the unbounded knapsack problem aims to maximize item value, while the coin change problem aims to minimize the number of coins. +- The unbounded knapsack problem seeks solutions "not exceeding" the knapsack capacity, while the coin change problem seeks solutions that "exactly" make up the target amount. -**First step: Think through each round's decision-making, define the state, and thus derive the $dp$ table** +**Step 1: Think about the decisions in each round, define the state, and thus obtain the $dp$ table** -The state $[i, a]$ corresponds to the sub-problem: **the minimum number of coins that can make up the amount $a$ using the first $i$ types of coins**, denoted as $dp[i, a]$. +State $[i, a]$ corresponds to the subproblem: **the minimum number of coins among the first $i$ types of coins that can make up amount $a$**, denoted as $dp[i, a]$. -The two-dimensional $dp$ table is of size $(n+1) \times (amt+1)$. +The two-dimensional $dp$ table has size $(n+1) \times (amt+1)$. -**Second step: Identify the optimal substructure and derive the state transition equation** +**Step 2: Identify the optimal substructure, and then derive the state transition equation** -This problem differs from the unbounded knapsack problem in two aspects of the state transition equation. +This problem differs from the unbounded knapsack problem in the following two aspects regarding the state transition equation. -- This problem seeks the minimum, so the operator $\max()$ needs to be changed to $\min()$. -- The optimization is focused on the number of coins, so simply add $+1$ when a coin is chosen. +- This problem seeks the minimum value, so the operator $\max()$ needs to be changed to $\min()$. +- The optimization target is the number of coins rather than item value, so when a coin is selected, simply execute $+1$. $$ dp[i, a] = \min(dp[i-1, a], dp[i, a - coins[i-1]] + 1) $$ -**Third step: Define boundary conditions and state transition order** +**Step 3: Determine boundary conditions and state transition order** -When the target amount is $0$, the minimum number of coins needed to make it up is $0$, so all $dp[i, 0]$ in the first column are $0$. +When the target amount is $0$, the minimum number of coins needed to make it up is $0$, so all $dp[i, 0]$ in the first column equal $0$. -When there are no coins, **it is impossible to make up any amount >0**, which is an invalid solution. To allow the $\min()$ function in the state transition equation to recognize and filter out invalid solutions, consider using $+\infty$ to represent them, i.e., set all $dp[0, a]$ in the first row to $+\infty$. +When there are no coins, **it is impossible to make up any amount $> 0$**, which is an invalid solution. To enable the $\min()$ function in the state transition equation to identify and filter out invalid solutions, we consider using $+ \infty$ to represent them, i.e., set all $dp[0, a]$ in the first row to $+ \infty$. -### 2.   Code implementation +### 2.   Code Implementation -Most programming languages do not provide a $+\infty$ variable, only the maximum value of an integer `int` can be used as a substitute. This can lead to overflow: the $+1$ operation in the state transition equation may overflow. +Most programming languages do not provide a $+ \infty$ variable, and can only use the maximum value of integer type `int` as a substitute. However, this can lead to large number overflow: the $+ 1$ operation in the state transition equation may cause overflow. -For this reason, we use the number $amt + 1$ to represent an invalid solution, because the maximum number of coins needed to make up $amt$ is at most $amt$. Before returning the result, check if $dp[n, amt]$ equals $amt + 1$, and if so, return $-1$, indicating that the target amount cannot be made up. The code is as follows: +For this reason, we use the number $amt + 1$ to represent invalid solutions, because the maximum number of coins needed to make up $amt$ is at most $amt$. Before returning, check whether $dp[n, amt]$ equals $amt + 1$; if so, return $-1$, indicating that the target amount cannot be made up. The code is as follows: === "Python" @@ -396,14 +781,14 @@ For this reason, we use the number $amt + 1$ to represent an invalid solution, b # State transition: first row and first column for a in range(1, amt + 1): dp[0][a] = MAX - # State transition: the rest of the rows and columns + # State transition: rest of the rows and columns for i in range(1, n + 1): for a in range(1, amt + 1): if coins[i - 1] > a: - # If exceeding the target amount, do not choose coin i + # If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a] else: - # The smaller value between not choosing and choosing coin i + # The smaller value between not selecting and selecting coin 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 ``` @@ -421,14 +806,14 @@ For this reason, we use the number $amt + 1$ to represent an invalid solution, b for (int a = 1; a <= amt; a++) { dp[0][a] = MAX; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a]; } else { - // The smaller value between not choosing and choosing coin i + // The smaller value between not selecting and selecting coin i dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); } } @@ -450,14 +835,14 @@ For this reason, we use the number $amt + 1$ to represent an invalid solution, b for (int a = 1; a <= amt; a++) { dp[0][a] = MAX; } - // State transition: the rest of the rows and columns + // State transition: rest of the rows and columns for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a]; } else { - // The smaller value between not choosing and choosing coin i + // The smaller value between not selecting and selecting coin i dp[i][a] = Math.min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); } } @@ -469,73 +854,318 @@ For this reason, we use the number $amt + 1$ to represent an invalid solution, b === "C#" ```csharp title="coin_change.cs" - [class]{coin_change}-[func]{CoinChangeDP} + /* Coin change: Dynamic programming */ + int CoinChangeDP(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // Initialize dp table + int[,] dp = new int[n + 1, amt + 1]; + // State transition: first row and first column + for (int a = 1; a <= amt; a++) { + dp[0, a] = MAX; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i, a] = dp[i - 1, a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + func coinChangeDP(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // Initialize dp table + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // State transition: first row and first column + for a := 1; a <= amt; a++ { + dp[0][a] = max + } + // State transition: rest of the rows and columns + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i-1][a] + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + func coinChangeDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // Initialize dp table + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // State transition: first row and first column + for a in 1 ... amt { + dp[0][a] = MAX + } + // State transition: rest of the rows and columns + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + function coinChangeDP(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // State transition: first row and first column + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // State transition: rest of the rows and columns + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + function coinChangeDP(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // State transition: first row and first column + for (let a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // State transition: rest of the rows and columns + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + int coinChangeDP(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // Initialize dp table + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // State transition: first row and first column + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coin_change_dp} + /* Coin change: Dynamic programming */ + fn coin_change_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // Initialize dp table + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // State transition: first row and first column + for a in 1..=amt { + dp[0][a] = max; + } + // State transition: rest of the rows and columns + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // The smaller value between not selecting and selecting coin 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} + /* Coin change: Dynamic programming */ + int coinChangeDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // Initialize dp table + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // State transition: first row and first column + for (int a = 1; a <= amt; a++) { + dp[0][a] = MAX; + } + // State transition: rest of the rows and columns + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // The smaller value between not selecting and selecting coin i + dp[i][a] = myMin(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1); + } + } + } + int res = dp[n][amt] != MAX ? dp[n][amt] : -1; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change.kt" - [class]{}-[func]{coinChangeDP} + /* Coin change: Dynamic programming */ + fun coinChangeDP(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // Initialize dp table + val dp = Array(n + 1) { IntArray(amt + 1) } + // State transition: first row and first column + for (a in 1..amt) { + dp[0][a] = MAX + } + // State transition: rest of the rows and columns + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + } else { + // The smaller value between not selecting and selecting coin i + dp[i][a] = min(dp[i - 1][a], dp[i][a - coins[i - 1]] + 1) + } + } + } + return if (dp[n][amt] != MAX) dp[n][amt] else -1 + } ``` === "Ruby" ```ruby title="coin_change.rb" - [class]{}-[func]{coin_change_dp} + ### Coin change: dynamic programming ### + def coin_change_dp(coins, amt) + n = coins.length + _MAX = amt + 1 + # Initialize dp table + dp = Array.new(n + 1) { Array.new(amt + 1, 0) } + # State transition: first row and first column + (1...(amt + 1)).each { |a| dp[0][a] = _MAX } + # State transition: rest of the rows and columns + for i in 1...(n + 1) + for a in 1...(amt + 1) + if coins[i - 1] > a + # If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + else + # The smaller value between not selecting and selecting coin i + dp[i][a] = [dp[i - 1][a], dp[i][a - coins[i - 1]] + 1].min + end + end + end + dp[n][amt] != _MAX ? dp[n][amt] : -1 + end ``` -=== "Zig" - - ```zig title="coin_change.zig" - [class]{}-[func]{coinChangeDP} - ``` - -Figure 14-25 show the dynamic programming process for the coin change problem, which is very similar to the unbounded knapsack problem. +Figure 14-25 shows the dynamic programming process for coin change, which is very similar to the unbounded knapsack problem. === "<1>" - ![Dynamic programming process for the coin change problem](unbounded_knapsack_problem.assets/coin_change_dp_step1.png){ class="animation-figure" } + ![Dynamic programming process for coin change problem](unbounded_knapsack_problem.assets/coin_change_dp_step1.png){ class="animation-figure" } === "<2>" ![coin_change_dp_step2](unbounded_knapsack_problem.assets/coin_change_dp_step2.png){ class="animation-figure" } @@ -579,11 +1209,11 @@ Figure 14-25 show the dynamic programming process for the coin change problem, w === "<15>" ![coin_change_dp_step15](unbounded_knapsack_problem.assets/coin_change_dp_step15.png){ class="animation-figure" } -

Figure 14-25   Dynamic programming process for the coin change problem

+

Figure 14-25   Dynamic programming process for coin change problem

-### 3.   Space optimization +### 3.   Space Optimization -The space optimization for the coin change problem is handled in the same way as for the unbounded knapsack problem: +The space optimization for the coin change problem is handled in the same way as the unbounded knapsack problem: === "Python" @@ -597,13 +1227,13 @@ The space optimization for the coin change problem is handled in the same way as dp[0] = 0 # State transition for i in range(1, n + 1): - # Traverse in order + # Traverse in forward order for a in range(1, amt + 1): if coins[i - 1] > a: - # If exceeding the target amount, do not choose coin i + # If exceeds target amount, don't select coin i dp[a] = dp[a] else: - # The smaller value between not choosing and choosing coin i + # The smaller value between not selecting and selecting coin i dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) return dp[amt] if dp[amt] != MAX else -1 ``` @@ -622,10 +1252,10 @@ The space optimization for the coin change problem is handled in the same way as for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[a] = dp[a]; } else { - // The smaller value between not choosing and choosing coin i + // The smaller value between not selecting and selecting coin i dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1); } } @@ -649,10 +1279,10 @@ The space optimization for the coin change problem is handled in the same way as for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[a] = dp[a]; } else { - // The smaller value between not choosing and choosing coin i + // The smaller value between not selecting and selecting coin i dp[a] = Math.min(dp[a], dp[a - coins[i - 1]] + 1); } } @@ -664,92 +1294,307 @@ The space optimization for the coin change problem is handled in the same way as === "C#" ```csharp title="coin_change.cs" - [class]{coin_change}-[func]{CoinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + int CoinChangeDPComp(int[] coins, int amt) { + int n = coins.Length; + int MAX = amt + 1; + // Initialize dp table + int[] dp = new int[amt + 1]; + Array.Fill(dp, MAX); + dp[0] = 0; + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Dynamic programming */ + func coinChangeDPComp(coins []int, amt int) int { + n := len(coins) + max := amt + 1 + // Initialize dp table + dp := make([]int, amt+1) + for i := 1; i <= amt; i++ { + dp[i] = max + } + // State transition + for i := 1; i <= n; i++ { + // Traverse in forward order + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + func coinChangeDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + let MAX = amt + 1 + // Initialize dp table + var dp = Array(repeating: MAX, count: amt + 1) + dp[0] = 0 + // State transition + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + function coinChangeDPComp(coins, amt) { + const n = coins.length; + const MAX = amt + 1; + // Initialize dp table + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + function coinChangeDPComp(coins: Array, amt: number): number { + const n = coins.length; + const MAX = amt + 1; + // Initialize dp table + const dp = Array.from({ length: amt + 1 }, () => MAX); + dp[0] = 0; + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + int coinChangeDPComp(List coins, int amt) { + int n = coins.length; + int MAX = amt + 1; + // Initialize dp table + List dp = List.filled(amt + 1, MAX); + dp[0] = 0; + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin 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" - [class]{}-[func]{coin_change_dp_comp} + /* Coin change: Space-optimized dynamic programming */ + fn coin_change_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + let max = amt + 1; + // Initialize dp table + let mut dp = vec![0; amt + 1]; + dp.fill(max); + dp[0] = 0; + // State transition + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin 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} + /* Coin change: Space-optimized dynamic programming */ + int coinChangeDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + int MAX = amt + 1; + // Initialize dp table + int *dp = malloc((amt + 1) * sizeof(int)); + for (int j = 1; j <= amt; j++) { + dp[j] = MAX; + } + dp[0] = 0; + + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // The smaller value between not selecting and selecting coin i + dp[a] = myMin(dp[a], dp[a - coins[i - 1]] + 1); + } + } + } + int res = dp[amt] != MAX ? dp[amt] : -1; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change.kt" - [class]{}-[func]{coinChangeDPComp} + /* Coin change: Space-optimized dynamic programming */ + fun coinChangeDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + val MAX = amt + 1 + // Initialize dp table + val dp = IntArray(amt + 1) + dp.fill(MAX) + dp[0] = 0 + // State transition + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // The smaller value between not selecting and selecting coin i + dp[a] = min(dp[a], dp[a - coins[i - 1]] + 1) + } + } + } + return if (dp[amt] != MAX) dp[amt] else -1 + } ``` === "Ruby" ```ruby title="coin_change.rb" - [class]{}-[func]{coin_change_dp_comp} + ### Coin change: space-optimized DP ### + def coin_change_dp_comp(coins, amt) + n = coins.length + _MAX = amt + 1 + # Initialize dp table + dp = Array.new(amt + 1, _MAX) + dp[0] = 0 + # State transition + for i in 1...(n + 1) + # Traverse in forward order + for a in 1...(amt + 1) + if coins[i - 1] > a + # If exceeds target amount, don't select coin i + dp[a] = dp[a] + else + # The smaller value between not selecting and selecting coin i + dp[a] = [dp[a], dp[a - coins[i - 1]] + 1].min + end + end + end + dp[amt] != _MAX ? dp[amt] : -1 + end ``` -=== "Zig" - - ```zig title="coin_change.zig" - [class]{}-[func]{coinChangeDPComp} - ``` - -## 14.5.3   Coin change problem II +## 14.5.3   Coin Change Problem Ii !!! question - Given $n$ types of coins, where the denomination of the $i^{th}$ type of coin is $coins[i - 1]$, and the target amount is $amt$. Each type of coin can be selected multiple times, **ask how many combinations of coins can make up the target amount**. See the example below. + Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$. Each type of coin can be selected multiple times. **What is the number of coin combinations that can make up the target amount?** An example is shown in Figure 14-26. -![Example data for Coin Change Problem II](unbounded_knapsack_problem.assets/coin_change_ii_example.png){ class="animation-figure" } +![Example data for coin change problem II](unbounded_knapsack_problem.assets/coin_change_ii_example.png){ class="animation-figure" } -

Figure 14-26   Example data for Coin Change Problem II

+

Figure 14-26   Example data for coin change problem II

-### 1.   Dynamic programming approach +### 1.   Dynamic Programming Approach -Compared to the previous problem, the goal of this problem is to determine the number of combinations, so the sub-problem becomes: **the number of combinations that can make up amount $a$ using the first $i$ types of coins**. The $dp$ table remains a two-dimensional matrix of size $(n+1) \times (amt + 1)$. +Compared to the previous problem, this problem's goal is to find the number of combinations, so the subproblem becomes: **the number of combinations among the first $i$ types of coins that can make up amount $a$**. The $dp$ table remains a two-dimensional matrix of size $(n+1) \times (amt + 1)$. -The number of combinations for the current state is the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is: +The number of combinations for the current state equals the sum of the combinations from not selecting the current coin and selecting the current coin. The state transition equation is: $$ dp[i, a] = dp[i-1, a] + dp[i, a - coins[i-1]] $$ -When the target amount is $0$, no coins are needed to make up the target amount, so all $dp[i, 0]$ in the first column should be initialized to $1$. When there are no coins, it is impossible to make up any amount >0, so all $dp[0, a]$ in the first row should be set to $0$. +When the target amount is $0$, no coins need to be selected to make up the target amount, so all $dp[i, 0]$ in the first column should be initialized to $1$. When there are no coins, it is impossible to make up any amount $>0$, so all $dp[0, a]$ in the first row equal $0$. -### 2.   Code implementation +### 2.   Code Implementation === "Python" @@ -766,10 +1611,10 @@ When the target amount is $0$, no coins are needed to make up the target amount, for i in range(1, n + 1): for a in range(1, amt + 1): if coins[i - 1] > a: - # If exceeding the target amount, do not choose coin i + # If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a] else: - # The sum of the two options of not choosing and choosing coin i + # Sum of the two options: not selecting and selecting coin i dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] return dp[n][amt] ``` @@ -790,10 +1635,10 @@ When the target amount is $0$, no coins are needed to make up the target amount, for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a]; } else { - // The sum of the two options of not choosing and choosing coin i + // Sum of the two options: not selecting and selecting coin i dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; } } @@ -818,10 +1663,10 @@ When the target amount is $0$, no coins are needed to make up the target amount, for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[i][a] = dp[i - 1][a]; } else { - // The sum of the two options of not choosing and choosing coin i + // Sum of the two options: not selecting and selecting coin i dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; } } @@ -833,72 +1678,300 @@ When the target amount is $0$, no coins are needed to make up the target amount, === "C#" ```csharp title="coin_change_ii.cs" - [class]{coin_change_ii}-[func]{CoinChangeIIDP} + /* Coin change II: Dynamic programming */ + int CoinChangeIIDP(int[] coins, int amt) { + int n = coins.Length; + // Initialize dp table + int[,] dp = new int[n + 1, amt + 1]; + // Initialize first column + for (int i = 0; i <= n; i++) { + dp[i, 0] = 1; + } + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i, a] = dp[i - 1, a]; + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + func coinChangeIIDP(coins []int, amt int) int { + n := len(coins) + // Initialize dp table + dp := make([][]int, n+1) + for i := 0; i <= n; i++ { + dp[i] = make([]int, amt+1) + } + // Initialize first column + for i := 0; i <= n; i++ { + dp[i][0] = 1 + } + // State transition: rest of the rows and columns + for i := 1; i <= n; i++ { + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i-1][a] + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + func coinChangeIIDP(coins: [Int], amt: Int) -> Int { + let n = coins.count + // Initialize dp table + var dp = Array(repeating: Array(repeating: 0, count: amt + 1), count: n + 1) + // Initialize first column + for i in 0 ... n { + dp[i][0] = 1 + } + // State transition + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + function coinChangeIIDP(coins, amt) { + const n = coins.length; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // Initialize first column + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + function coinChangeIIDP(coins: Array, amt: number): number { + const n = coins.length; + // Initialize dp table + const dp = Array.from({ length: n + 1 }, () => + Array.from({ length: amt + 1 }, () => 0) + ); + // Initialize first column + for (let i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + int coinChangeIIDP(List coins, int amt) { + int n = coins.length; + // Initialize dp table + List> dp = List.generate(n + 1, (index) => List.filled(amt + 1, 0)); + // Initialize first column + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // Sum of the two options: not selecting and selecting coin 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" - [class]{}-[func]{coin_change_ii_dp} + /* Coin change II: Dynamic programming */ + fn coin_change_ii_dp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // Initialize dp table + let mut dp = vec![vec![0; amt + 1]; n + 1]; + // Initialize first column + for i in 0..=n { + dp[i][0] = 1; + } + // State transition + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // Sum of the two options: not selecting and selecting coin 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} + /* Coin change II: Dynamic programming */ + int coinChangeIIDP(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // Initialize dp table + int **dp = malloc((n + 1) * sizeof(int *)); + for (int i = 0; i <= n; i++) { + dp[i] = calloc(amt + 1, sizeof(int)); + } + // Initialize first column + for (int i = 0; i <= n; i++) { + dp[i][0] = 1; + } + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]]; + } + } + } + int res = dp[n][amt]; + // Free memory + for (int i = 0; i <= n; i++) { + free(dp[i]); + } + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change_ii.kt" - [class]{}-[func]{coinChangeIIDP} + /* Coin change II: Dynamic programming */ + fun coinChangeIIDP(coins: IntArray, amt: Int): Int { + val n = coins.size + // Initialize dp table + val dp = Array(n + 1) { IntArray(amt + 1) } + // Initialize first column + for (i in 0..n) { + dp[i][0] = 1 + } + // State transition + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + } else { + // Sum of the two options: not selecting and selecting coin i + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + } + } + } + return dp[n][amt] + } ``` === "Ruby" ```ruby title="coin_change_ii.rb" - [class]{}-[func]{coin_change_ii_dp} + ### Coin change II: dynamic programming ### + def coin_change_ii_dp(coins, amt) + n = coins.length + # Initialize dp table + dp = Array.new(n + 1) { Array.new(amt + 1, 0) } + # Initialize first column + (0...(n + 1)).each { |i| dp[i][0] = 1 } + # State transition + for i in 1...(n + 1) + for a in 1...(amt + 1) + if coins[i - 1] > a + # If exceeds target amount, don't select coin i + dp[i][a] = dp[i - 1][a] + else + # Sum of the two options: not selecting and selecting coin i + dp[i][a] = dp[i - 1][a] + dp[i][a - coins[i - 1]] + end + end + end + dp[n][amt] + end ``` -=== "Zig" +### 3.   Space Optimization - ```zig title="coin_change_ii.zig" - [class]{}-[func]{coinChangeIIDP} - ``` - -### 3.   Space optimization - -The space optimization approach is the same, just remove the coin dimension: +The space optimization is handled in the same way, just delete the coin dimension: === "Python" @@ -911,13 +1984,13 @@ The space optimization approach is the same, just remove the coin dimension: dp[0] = 1 # State transition for i in range(1, n + 1): - # Traverse in order + # Traverse in forward order for a in range(1, amt + 1): if coins[i - 1] > a: - # If exceeding the target amount, do not choose coin i + # If exceeds target amount, don't select coin i dp[a] = dp[a] else: - # The sum of the two options of not choosing and choosing coin i + # Sum of the two options: not selecting and selecting coin i dp[a] = dp[a] + dp[a - coins[i - 1]] return dp[amt] ``` @@ -935,10 +2008,10 @@ The space optimization approach is the same, just remove the coin dimension: for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[a] = dp[a]; } else { - // The sum of the two options of not choosing and choosing coin i + // Sum of the two options: not selecting and selecting coin i dp[a] = dp[a] + dp[a - coins[i - 1]]; } } @@ -960,10 +2033,10 @@ The space optimization approach is the same, just remove the coin dimension: for (int i = 1; i <= n; i++) { for (int a = 1; a <= amt; a++) { if (coins[i - 1] > a) { - // If exceeding the target amount, do not choose coin i + // If exceeds target amount, don't select coin i dp[a] = dp[a]; } else { - // The sum of the two options of not choosing and choosing coin i + // Sum of the two options: not selecting and selecting coin i dp[a] = dp[a] + dp[a - coins[i - 1]]; } } @@ -975,65 +2048,254 @@ The space optimization approach is the same, just remove the coin dimension: === "C#" ```csharp title="coin_change_ii.cs" - [class]{coin_change_ii}-[func]{CoinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + int CoinChangeIIDPComp(int[] coins, int amt) { + int n = coins.Length; + // Initialize dp table + int[] dp = new int[amt + 1]; + dp[0] = 1; + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Go" ```go title="coin_change_ii.go" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + func coinChangeIIDPComp(coins []int, amt int) int { + n := len(coins) + // Initialize dp table + dp := make([]int, amt+1) + dp[0] = 1 + // State transition + for i := 1; i <= n; i++ { + // Traverse in forward order + for a := 1; a <= amt; a++ { + if coins[i-1] > a { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a-coins[i-1]] + } + } + } + return dp[amt] + } ``` === "Swift" ```swift title="coin_change_ii.swift" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + func coinChangeIIDPComp(coins: [Int], amt: Int) -> Int { + let n = coins.count + // Initialize dp table + var dp = Array(repeating: 0, count: amt + 1) + dp[0] = 1 + // State transition + for i in 1 ... n { + for a in 1 ... amt { + if coins[i - 1] > a { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } ``` === "JS" ```javascript title="coin_change_ii.js" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + function coinChangeIIDPComp(coins, amt) { + const n = coins.length; + // Initialize dp table + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "TS" ```typescript title="coin_change_ii.ts" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + function coinChangeIIDPComp(coins: Array, amt: number): number { + const n = coins.length; + // Initialize dp table + const dp = Array.from({ length: amt + 1 }, () => 0); + dp[0] = 1; + // State transition + for (let i = 1; i <= n; i++) { + for (let a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Dart" ```dart title="coin_change_ii.dart" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + int coinChangeIIDPComp(List coins, int amt) { + int n = coins.length; + // Initialize dp table + List dp = List.filled(amt + 1, 0); + dp[0] = 1; + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + return dp[amt]; + } ``` === "Rust" ```rust title="coin_change_ii.rs" - [class]{}-[func]{coin_change_ii_dp_comp} + /* Coin change II: Space-optimized dynamic programming */ + fn coin_change_ii_dp_comp(coins: &[i32], amt: usize) -> i32 { + let n = coins.len(); + // Initialize dp table + let mut dp = vec![0; amt + 1]; + dp[0] = 1; + // State transition + for i in 1..=n { + for a in 1..=amt { + if coins[i - 1] > a as i32 { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1] as usize]; + } + } + } + dp[amt] + } ``` === "C" ```c title="coin_change_ii.c" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + int coinChangeIIDPComp(int coins[], int amt, int coinsSize) { + int n = coinsSize; + // Initialize dp table + int *dp = calloc(amt + 1, sizeof(int)); + dp[0] = 1; + // State transition + for (int i = 1; i <= n; i++) { + for (int a = 1; a <= amt; a++) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a]; + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]]; + } + } + } + int res = dp[amt]; + // Free memory + free(dp); + return res; + } ``` === "Kotlin" ```kotlin title="coin_change_ii.kt" - [class]{}-[func]{coinChangeIIDPComp} + /* Coin change II: Space-optimized dynamic programming */ + fun coinChangeIIDPComp(coins: IntArray, amt: Int): Int { + val n = coins.size + // Initialize dp table + val dp = IntArray(amt + 1) + dp[0] = 1 + // State transition + for (i in 1..n) { + for (a in 1..amt) { + if (coins[i - 1] > a) { + // If exceeds target amount, don't select coin i + dp[a] = dp[a] + } else { + // Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]] + } + } + } + return dp[amt] + } ``` === "Ruby" ```ruby title="coin_change_ii.rb" - [class]{}-[func]{coin_change_ii_dp_comp} - ``` - -=== "Zig" - - ```zig title="coin_change_ii.zig" - [class]{}-[func]{coinChangeIIDPComp} + ### Coin change II: space-optimized DP ### + def coin_change_ii_dp_comp(coins, amt) + n = coins.length + # Initialize dp table + dp = Array.new(amt + 1, 0) + dp[0] = 1 + # State transition + for i in 1...(n + 1) + # Traverse in forward order + for a in 1...(amt + 1) + if coins[i - 1] > a + # If exceeds target amount, don't select coin i + dp[a] = dp[a] + else + # Sum of the two options: not selecting and selecting coin i + dp[a] = dp[a] + dp[a - coins[i - 1]] + end + end + end + dp[amt] + end ``` diff --git a/en/docs/chapter_graph/graph.md b/en/docs/chapter_graph/graph.md index df1f2269e..7f3c7cb19 100644 --- a/en/docs/chapter_graph/graph.md +++ b/en/docs/chapter_graph/graph.md @@ -4,7 +4,7 @@ comments: true # 9.1   Graph -A graph is a type of nonlinear data structure, consisting of vertices and edges. A graph $G$ can be abstractly represented as a collection of a set of vertices $V$ and a set of edges $E$. The following example shows a graph containing 5 vertices and 7 edges. +A graph is a nonlinear data structure consisting of vertices and edges. We can abstractly represent a graph $G$ as a set of vertices $V$ and a set of edges $E$. The following example shows a graph containing 5 vertices and 7 edges. $$ \begin{aligned} @@ -14,33 +14,33 @@ G & = \{ V, E \} \newline \end{aligned} $$ -If vertices are viewed as nodes and edges as references (pointers) connecting the nodes, graphs can be seen as a data structure that extends from linked lists. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) are more complex due to their higher degree of freedom**. +If we view vertices as nodes and edges as references (pointers) connecting the nodes, we can see graphs as a data structure extended from linked lists. As shown in Figure 9-1, **compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex**. -![Relationship between linked lists, trees, and graphs](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } +![Relationships among linked lists, trees, and graphs](graph.assets/linkedlist_tree_graph.png){ class="animation-figure" } -

Figure 9-1   Relationship between linked lists, trees, and graphs

+

Figure 9-1   Relationships among linked lists, trees, and graphs

-## 9.1.1   Common types and terminologies of graphs +## 9.1.1   Common Types and Terminology of Graphs -Graphs can be divided into undirected graphs and directed graphs depending on whether edges have direction, as shown in Figure 9-2. +Graphs can be divided into undirected graphs and directed graphs based on whether edges have direction, as shown in Figure 9-2. -- In undirected graphs, edges represent a "bidirectional" connection between two vertices, for example, the "friends" in Facebook. -- In directed graphs, edges have directionality, that is, the edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other. For example, the "follow" and "followed" relationship on Instagram or TikTok. +- In undirected graphs, edges represent a "bidirectional" connection between two vertices, such as the "friend relationship" on WeChat or QQ. +- In directed graphs, edges have directionality, meaning edges $A \rightarrow B$ and $A \leftarrow B$ are independent of each other, such as the "follow" and "be followed" relationships on Weibo or TikTok. ![Directed and undirected graphs](graph.assets/directed_graph.png){ class="animation-figure" }

Figure 9-2   Directed and undirected graphs

-Depending on whether all vertices are connected, graphs can be divided into connected graphs and disconnected graphs, as shown in Figure 9-3. +Graphs can be divided into connected graphs and disconnected graphs based on whether all vertices are connected, as shown in Figure 9-3. -- For connected graphs, it is possible to reach any other vertex starting from an arbitrary vertex. -- For disconnected graphs, there is at least one vertex that cannot be reached from an arbitrary starting vertex. +- For connected graphs, starting from any vertex, all other vertices can be reached. +- For disconnected graphs, starting from a certain vertex, at least one vertex cannot be reached. ![Connected and disconnected graphs](graph.assets/connected_graph.png){ class="animation-figure" }

Figure 9-3   Connected and disconnected graphs

-We can also add a weight variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in Instagram, the system sorts your follower and following list by the level of interaction between you and other users (likes, views, comments, etc.). Such an interaction network can be represented by a weighted graph. +We can also add a "weight" variable to edges, resulting in weighted graphs as shown in Figure 9-4. For example, in mobile games like "Honor of Kings", the system calculates the "intimacy" between players based on their shared game time, and such intimacy networks can be represented using weighted graphs. ![Weighted and unweighted graphs](graph.assets/weighted_graph.png){ class="animation-figure" } @@ -48,56 +48,56 @@ We can also add a weight variable to edges, resulting in weighted graphs Graph data structures include the following commonly used terms. -- Adjacency: When there is an edge connecting two vertices, these two vertices are said to be "adjacent". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5. -- Path: The sequence of edges passed from vertex A to vertex B is called a path from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4. -- Degree: The number of edges a vertex has. For directed graphs, in-degree refers to how many edges point to the vertex, and out-degree refers to how many edges point out from the vertex. +- Adjacency: When two vertices are connected by an edge, these two vertices are said to be "adjacent". In Figure 9-4, the adjacent vertices of vertex 1 are vertices 2, 3, and 5. +- Path: The sequence of edges from vertex A to vertex B is called a "path" from A to B. In Figure 9-4, the edge sequence 1-5-2-4 is a path from vertex 1 to vertex 4. +- Degree: The number of edges a vertex has. For directed graphs, in-degree indicates how many edges point to the vertex, and out-degree indicates how many edges point out from the vertex. -## 9.1.2   Representation of graphs +## 9.1.2   Representation of Graphs -Common representations of graphs include "adjacency matrix" and "adjacency list". The following examples use undirected graphs. +Common representations of graphs include "adjacency matrices" and "adjacency lists". The following uses undirected graphs as examples. -### 1.   Adjacency matrix +### 1.   Adjacency Matrix -Let the number of vertices in the graph be $n$, the adjacency matrix uses an $n \times n$ matrix to represent the graph, where each row (column) represents a vertex, and the matrix elements represent edges, with $1$ or $0$ indicating whether there is an edge between two vertices. +Given a graph with $n$ vertices, an adjacency matrix uses an $n \times n$ matrix to represent the graph, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether an edge exists between two vertices. -As shown in Figure 9-5, let the adjacency matrix be $M$, and the list of vertices be $V$, then the matrix element $M[i, j] = 1$ indicates there is an edge between vertex $V[i]$ and vertex $V[j]$, conversely $M[i, j] = 0$ indicates there is no edge between the two vertices. +As shown in Figure 9-5, let the adjacency matrix be $M$ and the vertex list be $V$. Then matrix element $M[i, j] = 1$ indicates that an edge exists between vertex $V[i]$ and vertex $V[j]$, whereas $M[i, j] = 0$ indicates no edge between the two vertices. -![Representation of a graph with an adjacency matrix](graph.assets/adjacency_matrix.png){ class="animation-figure" } +![Adjacency matrix representation of a graph](graph.assets/adjacency_matrix.png){ class="animation-figure" } -

Figure 9-5   Representation of a graph with an adjacency matrix

+

Figure 9-5   Adjacency matrix representation of a graph

-Adjacency matrices have the following characteristics. +Adjacency matrices have the following properties. -- A vertex cannot be connected to itself, so the elements on the main diagonal of the adjacency matrix are meaningless. -- For undirected graphs, edges in both directions are equivalent, thus the adjacency matrix is symmetric with regard to the main diagonal. -- By replacing the elements of the adjacency matrix from $1$ and $0$ to weights, we can represent weighted graphs. +- In simple graphs, vertices cannot connect to themselves, so the elements on the main diagonal of the adjacency matrix are meaningless. +- For undirected graphs, edges in both directions are equivalent, so the adjacency matrix is symmetric about the main diagonal. +- Replacing the elements of the adjacency matrix from $1$ and $0$ to weights allows representation of weighted graphs. -When representing graphs with adjacency matrices, it is possible to directly access matrix elements to obtain edges, resulting in efficient operations of addition, deletion, lookup, and modification, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes more memory. +When using adjacency matrices to represent graphs, we can directly access matrix elements to obtain edges, resulting in highly efficient addition, deletion, lookup, and modification operations, all with a time complexity of $O(1)$. However, the space complexity of the matrix is $O(n^2)$, which consumes significant memory. -### 2.   Adjacency list +### 2.   Adjacency List -The adjacency list uses $n$ linked lists to represent the graph, with each linked list node representing a vertex. The $i$-th linked list corresponds to vertex $i$ and contains all adjacent vertices (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list. +An adjacency list uses $n$ linked lists to represent a graph, with linked list nodes representing vertices. The $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex (vertices connected to that vertex). Figure 9-6 shows an example of a graph stored using an adjacency list. -![Representation of a graph with an adjacency list](graph.assets/adjacency_list.png){ class="animation-figure" } +![Adjacency list representation of a graph](graph.assets/adjacency_list.png){ class="animation-figure" } -

Figure 9-6   Representation of a graph with an adjacency list

+

Figure 9-6   Adjacency list representation of a graph

-The adjacency list only stores actual edges, and the total number of edges is often much less than $n^2$, making it more space-efficient. However, finding edges in the adjacency list requires traversing the linked list, so its time efficiency is not as good as that of the adjacency matrix. +Adjacency lists only store edges that actually exist, and the total number of edges is typically much less than $n^2$, making them more space-efficient. However, finding edges in an adjacency list requires traversing the linked list, so its time efficiency is inferior to that of adjacency matrices. -Observing Figure 9-6, **the structure of the adjacency list is very similar to the "chaining" in hash tables, hence we can use similar methods to optimize efficiency**. For example, when the linked list is long, it can be transformed into an AVL tree or red-black tree, thus optimizing the time efficiency from $O(n)$ to $O(\log n)$; the linked list can also be transformed into a hash table, thus reducing the time complexity to $O(1)$. +Observing Figure 9-6, **the structure of adjacency lists is very similar to "chaining" in hash tables, so we can adopt similar methods to optimize efficiency**. For example, when linked lists are long, they can be converted to AVL trees or red-black trees, thereby optimizing time efficiency from $O(n)$ to $O(\log n)$; linked lists can also be converted to hash tables, thereby reducing time complexity to $O(1)$. -## 9.1.3   Common applications of graphs +## 9.1.3   Common Applications of Graphs -As shown in Table 9-1, many real-world systems can be modeled with graphs, and corresponding problems can be reduced to graph computing problems. +As shown in Table 9-1, many real-world systems can be modeled using graphs, and corresponding problems can be reduced to graph computation problems.

Table 9-1   Common graphs in real life

-| | Vertices | Edges | Graph Computing Problem | -| --------------- | ---------------- | --------------------------------------------- | -------------------------------- | -| Social Networks | Users | Follow / Followed | Potential Following Recommendations | -| Subway Lines | Stations | Connectivity Between Stations | Shortest Route Recommendations | -| Solar System | Celestial Bodies | Gravitational Forces Between Celestial Bodies | Planetary Orbit Calculations | +| | Vertices | Edges | Graph Computation Problem | +| -------------- | --------------- | -------------------------------------- | ----------------------------- | +| Social network | Users | Friend relationships | Potential friend recommendation | +| Subway lines | Stations | Connectivity between stations | Shortest route recommendation | +| Solar system | Celestial bodies | Gravitational forces between celestial bodies | Planetary orbit calculation |
diff --git a/en/docs/chapter_graph/graph_operations.md b/en/docs/chapter_graph/graph_operations.md index c343ad5e2..c0e53bbe1 100644 --- a/en/docs/chapter_graph/graph_operations.md +++ b/en/docs/chapter_graph/graph_operations.md @@ -2,18 +2,18 @@ comments: true --- -# 9.2   Basic operations on graphs +# 9.2   Basic Operations on Graphs -The basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Under the two representation methods of "adjacency matrix" and "adjacency list", the implementations are different. +Basic operations on graphs can be divided into operations on "edges" and operations on "vertices". Under the two representation methods of "adjacency matrix" and "adjacency list", the implementation methods differ. -## 9.2.1   Implementation based on adjacency matrix +## 9.2.1   Implementation Based on Adjacency Matrix Given an undirected graph with $n$ vertices, the various operations are implemented as shown in Figure 9-7. -- **Adding or removing an edge**: Directly modify the specified edge in the adjacency matrix, using $O(1)$ time. Since it is an undirected graph, it is necessary to update the edges in both directions simultaneously. +- **Adding or removing an edge**: Directly modify the specified edge in the adjacency matrix, using $O(1)$ time. Since it is an undirected graph, both directions of the edge need to be updated simultaneously. - **Adding a vertex**: Add a row and a column at the end of the adjacency matrix and fill them all with $0$s, using $O(n)$ time. -- **Removing a vertex**: Delete a row and a column in the adjacency matrix. The worst case is when the first row and column are removed, requiring $(n-1)^2$ elements to be "moved up and to the left", thus using $O(n^2)$ time. -- **Initialization**: Pass in $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an $n \times n$ size adjacency matrix `adjMat`, using $O(n^2)$ time. +- **Removing a vertex**: Delete a row and a column in the adjacency matrix. The worst case occurs when removing the first row and column, requiring $(n-1)^2$ elements to be "moved up and to the left", thus using $O(n^2)$ time. +- **Initialization**: Pass in $n$ vertices, initialize a vertex list `vertices` of length $n$, using $O(n)$ time; initialize an adjacency matrix `adjMat` of size $n \times n$, using $O(n^2)$ time. === "Initialize adjacency matrix" ![Initialization, adding and removing edges, adding and removing vertices in adjacency matrix](graph_operations.assets/adjacency_matrix_step1_initialization.png){ class="animation-figure" } @@ -32,7 +32,7 @@ Given an undirected graph with $n$ vertices, the various operations are implemen

Figure 9-7   Initialization, adding and removing edges, adding and removing vertices in adjacency matrix

-Below is the implementation code for graphs represented using an adjacency matrix: +The following is the implementation code for graphs represented using an adjacency matrix: === "Python" @@ -42,15 +42,15 @@ Below is the implementation code for graphs represented using an adjacency matri def __init__(self, vertices: list[int], edges: list[list[int]]): """Constructor""" - # Vertex list, elements represent "vertex value", index represents "vertex index" + # Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" self.vertices: list[int] = [] - # Adjacency matrix, row and column indices correspond to "vertex index" + # Adjacency matrix, where the row and column indices correspond to the "vertex index" self.adj_mat: list[list[int]] = [] - # Add vertex + # Add vertices for val in vertices: self.add_vertex(val) - # Add edge - # Edges elements represent vertex indices + # Add edges + # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices for e in edges: self.add_edge(e[0], e[1]) @@ -61,7 +61,7 @@ Below is the implementation code for graphs represented using an adjacency matri def add_vertex(self, val: int): """Add vertex""" n = self.size() - # Add new vertex value to the vertex list + # Add the value of the new vertex to the vertex list self.vertices.append(val) # Add a row to the adjacency matrix new_row = [0] * n @@ -74,27 +74,27 @@ Below is the implementation code for graphs represented using an adjacency matri """Remove vertex""" if index >= self.size(): raise IndexError() - # Remove vertex at `index` from the vertex list + # Remove the vertex at index from the vertex list self.vertices.pop(index) - # Remove the row at `index` from the adjacency matrix + # Remove the row at index from the adjacency matrix self.adj_mat.pop(index) - # Remove the column at `index` from the adjacency matrix + # Remove the column at index from the adjacency matrix for row in self.adj_mat: row.pop(index) def add_edge(self, i: int, j: int): """Add edge""" - # Parameters i, j correspond to vertices element indices + # Parameters i, j correspond to the vertices element indices # Handle index out of bounds and equality if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: raise IndexError() - # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., satisfies (i, j) == (j, i) + # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) self.adj_mat[i][j] = 1 self.adj_mat[j][i] = 1 def remove_edge(self, i: int, j: int): """Remove edge""" - # Parameters i, j correspond to vertices element indices + # Parameters i, j correspond to the vertices element indices # Handle index out of bounds and equality if i < 0 or j < 0 or i >= self.size() or j >= self.size() or i == j: raise IndexError() @@ -113,8 +113,8 @@ Below is the implementation code for graphs represented using an adjacency matri ```cpp title="graph_adjacency_matrix.cpp" /* Undirected graph class based on adjacency matrix */ class GraphAdjMat { - vector vertices; // Vertex list, elements represent "vertex value", index represents "vertex index" - vector> adjMat; // Adjacency matrix, row and column indices correspond to "vertex index" + vector vertices; // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + vector> adjMat; // Adjacency matrix, where the row and column indices correspond to the "vertex index" public: /* Constructor */ @@ -124,7 +124,7 @@ Below is the implementation code for graphs represented using an adjacency matri addVertex(val); } // Add edge - // Edges elements represent vertex indices + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices for (const vector &edge : edges) { addEdge(edge[0], edge[1]); } @@ -138,7 +138,7 @@ Below is the implementation code for graphs represented using an adjacency matri /* Add vertex */ void addVertex(int val) { int n = size(); - // Add new vertex value to the vertex list + // Add the value of the new vertex to the vertex list vertices.push_back(val); // Add a row to the adjacency matrix adjMat.emplace_back(vector(n, 0)); @@ -153,30 +153,30 @@ Below is the implementation code for graphs represented using an adjacency matri if (index >= size()) { throw out_of_range("Vertex does not exist"); } - // Remove vertex at `index` from the vertex list + // Remove the vertex at index from the vertex list vertices.erase(vertices.begin() + index); - // Remove the row at `index` from the adjacency matrix + // Remove the row at index from the adjacency matrix adjMat.erase(adjMat.begin() + index); - // Remove the column at `index` from the adjacency matrix + // Remove the column at index from the adjacency matrix for (vector &row : adjMat) { row.erase(row.begin() + index); } } /* Add edge */ - // Parameters i, j correspond to vertices element indices + // Parameters i, j correspond to the vertices element indices void addEdge(int i, int j) { // Handle index out of bounds and equality if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { throw out_of_range("Vertex does not exist"); } - // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., satisfies (i, j) == (j, i) + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) adjMat[i][j] = 1; adjMat[j][i] = 1; } /* Remove edge */ - // Parameters i, j correspond to vertices element indices + // Parameters i, j correspond to the vertices element indices void removeEdge(int i, int j) { // Handle index out of bounds and equality if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { @@ -201,8 +201,8 @@ Below is the implementation code for graphs represented using an adjacency matri ```java title="graph_adjacency_matrix.java" /* Undirected graph class based on adjacency matrix */ class GraphAdjMat { - List vertices; // Vertex list, elements represent "vertex value", index represents "vertex index" - List> adjMat; // Adjacency matrix, row and column indices correspond to "vertex index" + List vertices; // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + List> adjMat; // Adjacency matrix, where the row and column indices correspond to the "vertex index" /* Constructor */ public GraphAdjMat(int[] vertices, int[][] edges) { @@ -213,7 +213,7 @@ Below is the implementation code for graphs represented using an adjacency matri addVertex(val); } // Add edge - // Edges elements represent vertex indices + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices for (int[] e : edges) { addEdge(e[0], e[1]); } @@ -227,7 +227,7 @@ Below is the implementation code for graphs represented using an adjacency matri /* Add vertex */ public void addVertex(int val) { int n = size(); - // Add new vertex value to the vertex list + // Add the value of the new vertex to the vertex list vertices.add(val); // Add a row to the adjacency matrix List newRow = new ArrayList<>(n); @@ -245,29 +245,29 @@ Below is the implementation code for graphs represented using an adjacency matri public void removeVertex(int index) { if (index >= size()) throw new IndexOutOfBoundsException(); - // Remove vertex at `index` from the vertex list + // Remove the vertex at index from the vertex list vertices.remove(index); - // Remove the row at `index` from the adjacency matrix + // Remove the row at index from the adjacency matrix adjMat.remove(index); - // Remove the column at `index` from the adjacency matrix + // Remove the column at index from the adjacency matrix for (List row : adjMat) { row.remove(index); } } /* Add edge */ - // Parameters i, j correspond to vertices element indices + // Parameters i, j correspond to the vertices element indices public void addEdge(int i, int j) { // Handle index out of bounds and equality if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) throw new IndexOutOfBoundsException(); - // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., satisfies (i, j) == (j, i) + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) adjMat.get(i).set(j, 1); adjMat.get(j).set(i, 1); } /* Remove edge */ - // Parameters i, j correspond to vertices element indices + // Parameters i, j correspond to the vertices element indices public void removeEdge(int i, int j) { // Handle index out of bounds and equality if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) @@ -289,77 +289,931 @@ Below is the implementation code for graphs represented using an adjacency matri === "C#" ```csharp title="graph_adjacency_matrix.cs" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat { + List vertices; // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + List> adjMat; // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + public GraphAdjMat(int[] vertices, int[][] edges) { + this.vertices = []; + this.adjMat = []; + // Add vertex + foreach (int val in vertices) { + AddVertex(val); + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + foreach (int[] e in edges) { + AddEdge(e[0], e[1]); + } + } + + /* Get the number of vertices */ + int Size() { + return vertices.Count; + } + + /* Add vertex */ + public void AddVertex(int val) { + int n = Size(); + // Add the value of the new vertex to the vertex list + vertices.Add(val); + // Add a row to the adjacency matrix + List newRow = new(n); + for (int j = 0; j < n; j++) { + newRow.Add(0); + } + adjMat.Add(newRow); + // Add a column to the adjacency matrix + foreach (List row in adjMat) { + row.Add(0); + } + } + + /* Remove vertex */ + public void RemoveVertex(int index) { + if (index >= Size()) + throw new IndexOutOfRangeException(); + // Remove the vertex at index from the vertex list + vertices.RemoveAt(index); + // Remove the row at index from the adjacency matrix + adjMat.RemoveAt(index); + // Remove the column at index from the adjacency matrix + foreach (List row in adjMat) { + row.RemoveAt(index); + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + public void AddEdge(int i, int j) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + public void RemoveEdge(int i, int j) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= Size() || j >= Size() || i == j) + throw new IndexOutOfRangeException(); + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* Print adjacency matrix */ + public void Print() { + Console.Write("Vertex list = "); + PrintUtil.PrintList(vertices); + Console.WriteLine("Adjacency matrix ="); + PrintUtil.PrintMatrix(adjMat); + } + } ``` === "Go" ```go title="graph_adjacency_matrix.go" - [class]{graphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + type graphAdjMat struct { + // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + vertices []int + // Adjacency matrix, where the row and column indices correspond to the "vertex index" + adjMat [][]int + } + + /* Constructor */ + func newGraphAdjMat(vertices []int, edges [][]int) *graphAdjMat { + // Add vertex + n := len(vertices) + adjMat := make([][]int, n) + for i := range adjMat { + adjMat[i] = make([]int, n) + } + // Initialize graph + g := &graphAdjMat{ + vertices: vertices, + adjMat: adjMat, + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for i := range edges { + g.addEdge(edges[i][0], edges[i][1]) + } + return g + } + + /* Get the number of vertices */ + func (g *graphAdjMat) size() int { + return len(g.vertices) + } + + /* Add vertex */ + func (g *graphAdjMat) addVertex(val int) { + n := g.size() + // Add the value of the new vertex to the vertex list + g.vertices = append(g.vertices, val) + // Add a row to the adjacency matrix + newRow := make([]int, n) + g.adjMat = append(g.adjMat, newRow) + // Add a column to the adjacency matrix + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i], 0) + } + } + + /* Remove vertex */ + func (g *graphAdjMat) removeVertex(index int) { + if index >= g.size() { + return + } + // Remove the vertex at index from the vertex list + g.vertices = append(g.vertices[:index], g.vertices[index+1:]...) + // Remove the row at index from the adjacency matrix + g.adjMat = append(g.adjMat[:index], g.adjMat[index+1:]...) + // Remove the column at index from the adjacency matrix + for i := range g.adjMat { + g.adjMat[i] = append(g.adjMat[i][:index], g.adjMat[i][index+1:]...) + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + func (g *graphAdjMat) addEdge(i, j int) { + // Handle index out of bounds and equality + if i < 0 || j < 0 || i >= g.size() || j >= g.size() || i == j { + fmt.Errorf("%s", "Index Out Of Bounds Exception") + } + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + g.adjMat[i][j] = 1 + g.adjMat[j][i] = 1 + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + func (g *graphAdjMat) removeEdge(i, j int) { + // Handle index out of bounds and equality + 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 + } + + /* Print adjacency matrix */ + func (g *graphAdjMat) print() { + fmt.Printf("\tVertex list = %v\n", g.vertices) + fmt.Printf("\tAdjacency matrix = \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}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat { + private var vertices: [Int] // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + private var adjMat: [[Int]] // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + init(vertices: [Int], edges: [[Int]]) { + self.vertices = [] + adjMat = [] + // Add vertex + for val in vertices { + addVertex(val: val) + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for e in edges { + addEdge(i: e[0], j: e[1]) + } + } + + /* Get the number of vertices */ + func size() -> Int { + vertices.count + } + + /* Add vertex */ + func addVertex(val: Int) { + let n = size() + // Add the value of the new vertex to the vertex list + vertices.append(val) + // Add a row to the adjacency matrix + let newRow = Array(repeating: 0, count: n) + adjMat.append(newRow) + // Add a column to the adjacency matrix + for i in adjMat.indices { + adjMat[i].append(0) + } + } + + /* Remove vertex */ + func removeVertex(index: Int) { + if index >= size() { + fatalError("Out of bounds") + } + // Remove the vertex at index from the vertex list + vertices.remove(at: index) + // Remove the row at index from the adjacency matrix + adjMat.remove(at: index) + // Remove the column at index from the adjacency matrix + for i in adjMat.indices { + adjMat[i].remove(at: index) + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + func addEdge(i: Int, j: Int) { + // Handle index out of bounds and equality + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("Out of bounds") + } + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + func removeEdge(i: Int, j: Int) { + // Handle index out of bounds and equality + if i < 0 || j < 0 || i >= size() || j >= size() || i == j { + fatalError("Out of bounds") + } + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + + /* Print adjacency matrix */ + func print() { + Swift.print("Vertex list = ", terminator: "") + Swift.print(vertices) + Swift.print("Adjacency matrix =") + PrintUtil.printMatrix(matrix: adjMat) + } + } ``` === "JS" ```javascript title="graph_adjacency_matrix.js" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat { + vertices; // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + adjMat; // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + constructor(vertices, edges) { + this.vertices = []; + this.adjMat = []; + // Add vertex + for (const val of vertices) { + this.addVertex(val); + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* Get the number of vertices */ + size() { + return this.vertices.length; + } + + /* Add vertex */ + addVertex(val) { + const n = this.size(); + // Add the value of the new vertex to the vertex list + this.vertices.push(val); + // Add a row to the adjacency matrix + const newRow = []; + for (let j = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // Add a column to the adjacency matrix + for (const row of this.adjMat) { + row.push(0); + } + } + + /* Remove vertex */ + removeVertex(index) { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // Remove the vertex at index from the vertex list + this.vertices.splice(index, 1); + + // Remove the row at index from the adjacency matrix + this.adjMat.splice(index, 1); + // Remove the column at index from the adjacency matrix + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + addEdge(i, j) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i) + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + removeEdge(i, j) { + // Handle index out of bounds and equality + 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 adjacency matrix */ + print() { + console.log('Vertex list = ', this.vertices); + console.log('Adjacency matrix =', this.adjMat); + } + } ``` === "TS" ```typescript title="graph_adjacency_matrix.ts" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat { + vertices: number[]; // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + adjMat: number[][]; // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + constructor(vertices: number[], edges: number[][]) { + this.vertices = []; + this.adjMat = []; + // Add vertex + for (const val of vertices) { + this.addVertex(val); + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for (const e of edges) { + this.addEdge(e[0], e[1]); + } + } + + /* Get the number of vertices */ + size(): number { + return this.vertices.length; + } + + /* Add vertex */ + addVertex(val: number): void { + const n: number = this.size(); + // Add the value of the new vertex to the vertex list + this.vertices.push(val); + // Add a row to the adjacency matrix + const newRow: number[] = []; + for (let j: number = 0; j < n; j++) { + newRow.push(0); + } + this.adjMat.push(newRow); + // Add a column to the adjacency matrix + for (const row of this.adjMat) { + row.push(0); + } + } + + /* Remove vertex */ + removeVertex(index: number): void { + if (index >= this.size()) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // Remove the vertex at index from the vertex list + this.vertices.splice(index, 1); + + // Remove the row at index from the adjacency matrix + this.adjMat.splice(index, 1); + // Remove the column at index from the adjacency matrix + for (const row of this.adjMat) { + row.splice(index, 1); + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + addEdge(i: number, j: number): void { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= this.size() || j >= this.size() || i === j) { + throw new RangeError('Index Out Of Bounds Exception'); + } + // In undirected graph, adjacency matrix is symmetric about main diagonal, i.e., satisfies (i, j) === (j, i) + this.adjMat[i][j] = 1; + this.adjMat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + removeEdge(i: number, j: number): void { + // Handle index out of bounds and equality + 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 adjacency matrix */ + print(): void { + console.log('Vertex list = ', this.vertices); + console.log('Adjacency matrix =', this.adjMat); + } + } ``` === "Dart" ```dart title="graph_adjacency_matrix.dart" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat { + List vertices = []; // Vertex elements, elements represent "vertex values", indices represent "vertex indices" + List> adjMat = []; // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + GraphAdjMat(List vertices, List> edges) { + this.vertices = []; + this.adjMat = []; + // Add vertex + for (int val in vertices) { + addVertex(val); + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for (List e in edges) { + addEdge(e[0], e[1]); + } + } + + /* Get the number of vertices */ + int size() { + return vertices.length; + } + + /* Add vertex */ + void addVertex(int val) { + int n = size(); + // Add the value of the new vertex to the vertex list + vertices.add(val); + // Add a row to the adjacency matrix + List newRow = List.filled(n, 0, growable: true); + adjMat.add(newRow); + // Add a column to the adjacency matrix + for (List row in adjMat) { + row.add(0); + } + } + + /* Remove vertex */ + void removeVertex(int index) { + if (index >= size()) { + throw IndexError; + } + // Remove the vertex at index from the vertex list + vertices.removeAt(index); + // Remove the row at index from the adjacency matrix + adjMat.removeAt(index); + // Remove the column at index from the adjacency matrix + for (List row in adjMat) { + row.removeAt(index); + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + void addEdge(int i, int j) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + adjMat[i][j] = 1; + adjMat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + void removeEdge(int i, int j) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) { + throw IndexError; + } + adjMat[i][j] = 0; + adjMat[j][i] = 0; + } + + /* Print adjacency matrix */ + void printAdjMat() { + print("Vertex list = $vertices"); + print("Adjacency matrix = "); + printMatrix(adjMat); + } + } ``` === "Rust" ```rust title="graph_adjacency_matrix.rs" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph type based on adjacency matrix */ + pub struct GraphAdjMat { + // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + pub vertices: Vec, + // Adjacency matrix, where the row and column indices correspond to the "vertex index" + pub adj_mat: Vec>, + } + + impl GraphAdjMat { + /* Constructor */ + pub fn new(vertices: Vec, edges: Vec<[usize; 2]>) -> Self { + let mut graph = GraphAdjMat { + vertices: vec![], + adj_mat: vec![], + }; + // Add vertex + for val in vertices { + graph.add_vertex(val); + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for edge in edges { + graph.add_edge(edge[0], edge[1]) + } + + graph + } + + /* Get the number of vertices */ + pub fn size(&self) -> usize { + self.vertices.len() + } + + /* Add vertex */ + pub fn add_vertex(&mut self, val: i32) { + let n = self.size(); + // Add the value of the new vertex to the vertex list + self.vertices.push(val); + // Add a row to the adjacency matrix + self.adj_mat.push(vec![0; n]); + // Add a column to the adjacency matrix + for row in self.adj_mat.iter_mut() { + row.push(0); + } + } + + /* Remove vertex */ + pub fn remove_vertex(&mut self, index: usize) { + if index >= self.size() { + panic!("index error") + } + // Remove the vertex at index from the vertex list + self.vertices.remove(index); + // Remove the row at index from the adjacency matrix + self.adj_mat.remove(index); + // Remove the column at index from the adjacency matrix + for row in self.adj_mat.iter_mut() { + row.remove(index); + } + } + + /* Add edge */ + pub fn add_edge(&mut self, i: usize, j: usize) { + // Parameters i, j correspond to the vertices element indices + // Handle index out of bounds and equality + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + self.adj_mat[i][j] = 1; + self.adj_mat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + pub fn remove_edge(&mut self, i: usize, j: usize) { + // Parameters i, j correspond to the vertices element indices + // Handle index out of bounds and equality + if i >= self.size() || j >= self.size() || i == j { + panic!("index error") + } + self.adj_mat[i][j] = 0; + self.adj_mat[j][i] = 0; + } + + /* Print adjacency matrix */ + pub fn print(&self) { + println!("Vertex list = {:?}", self.vertices); + println!("Adjacency matrix ="); + println!("["); + for row in &self.adj_mat { + println!(" {:?},", row); + } + println!("]") + } + } ``` === "C" ```c title="graph_adjacency_matrix.c" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph structure based on adjacency matrix */ + typedef struct { + int vertices[MAX_SIZE]; + int adjMat[MAX_SIZE][MAX_SIZE]; + int size; + } GraphAdjMat; + + /* Constructor */ + GraphAdjMat *newGraphAdjMat() { + GraphAdjMat *graph = (GraphAdjMat *)malloc(sizeof(GraphAdjMat)); + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + for (int j = 0; j < MAX_SIZE; j++) { + graph->adjMat[i][j] = 0; + } + } + return graph; + } + + /* Destructor */ + void delGraphAdjMat(GraphAdjMat *graph) { + free(graph); + } + + /* Add vertex */ + void addVertex(GraphAdjMat *graph, int val) { + if (graph->size == MAX_SIZE) { + fprintf(stderr, "Graph vertex count has reached maximum\n"); + return; + } + // Add nth vertex and zero nth row and column + int n = graph->size; + graph->vertices[n] = val; + for (int i = 0; i <= n; i++) { + graph->adjMat[n][i] = graph->adjMat[i][n] = 0; + } + graph->size++; + } + + /* Remove vertex */ + void removeVertex(GraphAdjMat *graph, int index) { + if (index < 0 || index >= graph->size) { + fprintf(stderr, "Vertex index out of bounds\n"); + return; + } + // Remove the vertex at index from the vertex list + for (int i = index; i < graph->size - 1; i++) { + graph->vertices[i] = graph->vertices[i + 1]; + } + // Remove the row at index from the adjacency matrix + for (int i = index; i < graph->size - 1; i++) { + for (int j = 0; j < graph->size; j++) { + graph->adjMat[i][j] = graph->adjMat[i + 1][j]; + } + } + // Remove the column at index from the adjacency matrix + for (int i = 0; i < graph->size; i++) { + for (int j = index; j < graph->size - 1; j++) { + graph->adjMat[i][j] = graph->adjMat[i][j + 1]; + } + } + graph->size--; + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + void addEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "Edge index out of bounds or equal\n"); + return; + } + graph->adjMat[i][j] = 1; + graph->adjMat[j][i] = 1; + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + void removeEdge(GraphAdjMat *graph, int i, int j) { + if (i < 0 || j < 0 || i >= graph->size || j >= graph->size || i == j) { + fprintf(stderr, "Edge index out of bounds or equal\n"); + return; + } + graph->adjMat[i][j] = 0; + graph->adjMat[j][i] = 0; + } + + /* Print adjacency matrix */ + void printGraphAdjMat(GraphAdjMat *graph) { + printf("Vertex list = "); + printArray(graph->vertices, graph->size); + printf("Adjacency matrix =\n"); + for (int i = 0; i < graph->size; i++) { + printArray(graph->adjMat[i], graph->size); + } + } ``` === "Kotlin" ```kotlin title="graph_adjacency_matrix.kt" - [class]{GraphAdjMat}-[func]{} + /* Undirected graph class based on adjacency matrix */ + class GraphAdjMat(vertices: IntArray, edges: Array) { + val vertices = mutableListOf() // Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + val adjMat = mutableListOf>() // Adjacency matrix, where the row and column indices correspond to the "vertex index" + + /* Constructor */ + init { + // Add vertex + for (vertex in vertices) { + addVertex(vertex) + } + // Add edge + // Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + for (edge in edges) { + addEdge(edge[0], edge[1]) + } + } + + /* Get the number of vertices */ + fun size(): Int { + return vertices.size + } + + /* Add vertex */ + fun addVertex(_val: Int) { + val n = size() + // Add the value of the new vertex to the vertex list + vertices.add(_val) + // Add a row to the adjacency matrix + val newRow = mutableListOf() + for (j in 0..= size()) + throw IndexOutOfBoundsException() + // Remove the vertex at index from the vertex list + vertices.removeAt(index) + // Remove the row at index from the adjacency matrix + adjMat.removeAt(index) + // Remove the column at index from the adjacency matrix + for (row in adjMat) { + row.removeAt(index) + } + } + + /* Add edge */ + // Parameters i, j correspond to the vertices element indices + fun addEdge(i: Int, j: Int) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw IndexOutOfBoundsException() + // In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + adjMat[i][j] = 1 + adjMat[j][i] = 1 + } + + /* Remove edge */ + // Parameters i, j correspond to the vertices element indices + fun removeEdge(i: Int, j: Int) { + // Handle index out of bounds and equality + if (i < 0 || j < 0 || i >= size() || j >= size() || i == j) + throw IndexOutOfBoundsException() + adjMat[i][j] = 0 + adjMat[j][i] = 0 + } + + /* Print adjacency matrix */ + fun print() { + print("Vertex list = ") + println(vertices) + println("Adjacency matrix =") + printMatrix(adjMat) + } + } ``` === "Ruby" ```ruby title="graph_adjacency_matrix.rb" - [class]{GraphAdjMat}-[func]{} + ### Undirected graph class based on adjacency matrix ### + class GraphAdjMat + def initialize(vertices, edges) + ### Constructor ### + # Vertex list, where the element represents the "vertex value" and the index represents the "vertex index" + @vertices = [] + # Adjacency matrix, where the row and column indices correspond to the "vertex index" + @adj_mat = [] + # Add vertex + vertices.each { |val| add_vertex(val) } + # Add edge + # Note that the edges elements represent vertex indices, i.e., corresponding to the vertices element indices + edges.each { |e| add_edge(e[0], e[1]) } + end + + ### Get number of vertices ### + def size + @vertices.length + end + + ### Add vertex ### + def add_vertex(val) + n = size + # Add the value of the new vertex to the vertex list + @vertices << val + # Add a row to the adjacency matrix + new_row = Array.new(n, 0) + @adj_mat << new_row + # Add a column to the adjacency matrix + @adj_mat.each { |row| row << 0 } + end + + ### Delete vertex ### + def remove_vertex(index) + raise IndexError if index >= size + + # Remove the vertex at index from the vertex list + @vertices.delete_at(index) + # Remove the row at index from the adjacency matrix + @adj_mat.delete_at(index) + # Remove the column at index from the adjacency matrix + @adj_mat.each { |row| row.delete_at(index) } + end + + ### Add edge ### + def add_edge(i, j) + # Parameters i, j correspond to the vertices element indices + # Handle index out of bounds and equality + if i < 0 || j < 0 || i >= size || j >= size || i == j + raise IndexError + end + # In an undirected graph, the adjacency matrix is symmetric about the main diagonal, i.e., (i, j) == (j, i) + @adj_mat[i][j] = 1 + @adj_mat[j][i] = 1 + end + + ### Delete edge ### + def remove_edge(i, j) + # Parameters i, j correspond to the vertices element indices + # Handle index out of bounds and equality + if i < 0 || j < 0 || i >= size || j >= size || i == j + raise IndexError + end + @adj_mat[i][j] = 0 + @adj_mat[j][i] = 0 + end + + ### Print adjacency matrix ### + def __print__ + puts "Vertex list = #{@vertices}" + puts 'Adjacency matrix =' + print_matrix(@adj_mat) + end + end ``` -=== "Zig" - - ```zig title="graph_adjacency_matrix.zig" - [class]{GraphAdjMat}-[func]{} - ``` - -## 9.2.2   Implementation based on adjacency list +## 9.2.2   Implementation Based on Adjacency List Given an undirected graph with a total of $n$ vertices and $m$ edges, the various operations can be implemented as shown in Figure 9-8. -- **Adding an edge**: Simply add the edge at the end of the corresponding vertex's linked list, using $O(1)$ time. Because it is an undirected graph, it is necessary to add edges in both directions simultaneously. -- **Removing an edge**: Find and remove the specified edge in the corresponding vertex's linked list, using $O(m)$ time. In an undirected graph, it is necessary to remove edges in both directions simultaneously. -- **Adding a vertex**: Add a linked list in the adjacency list and make the new vertex the head node of the list, using $O(1)$ time. -- **Removing a vertex**: It is necessary to traverse the entire adjacency list, removing all edges that include the specified vertex, using $O(n + m)$ time. +- **Adding an edge**: Add the edge at the end of the corresponding vertex's linked list, using $O(1)$ time. Since it is an undirected graph, edges in both directions need to be added simultaneously. +- **Removing an edge**: Find and remove the specified edge in the corresponding vertex's linked list, using $O(m)$ time. In an undirected graph, edges in both directions need to be removed simultaneously. +- **Adding a vertex**: Add a linked list in the adjacency list and set the new vertex as the head node of the list, using $O(1)$ time. +- **Removing a vertex**: Traverse the entire adjacency list and remove all edges containing the specified vertex, using $O(n + m)$ time. - **Initialization**: Create $n$ vertices and $2m$ edges in the adjacency list, using $O(n + m)$ time. === "Initialize adjacency list" @@ -379,12 +1233,12 @@ Given an undirected graph with a total of $n$ vertices and $m$ edges, the variou

Figure 9-8   Initialization, adding and removing edges, adding and removing vertices in adjacency list

-Below is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences. +The following is the adjacency list code implementation. Compared to Figure 9-8, the actual code has the following differences. - For convenience in adding and removing vertices, and to simplify the code, we use lists (dynamic arrays) instead of linked lists. -- Use a hash table to store the adjacency list, `key` being the vertex instance, `value` being the list (linked list) of adjacent vertices of that vertex. +- A hash table is used to store the adjacency list, where `key` is the vertex instance and `value` is the list (linked list) of adjacent vertices for that vertex. -Additionally, we use the `Vertex` class to represent vertices in the adjacency list. The reason for this is: if, like with the adjacency matrix, list indexes were used to distinguish different vertices, then suppose you want to delete the vertex at index $i$, you would need to traverse the entire adjacency list and decrement all indexes greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, then deleting a vertex does not require any changes to other vertices. +Additionally, we use the `Vertex` class to represent vertices in the adjacency list. The reason for this is: if we used list indices to distinguish different vertices as with adjacency matrices, then to delete the vertex at index $i$, we would need to traverse the entire adjacency list and decrement all indices greater than $i$ by $1$, which is very inefficient. However, if each vertex is a unique `Vertex` instance, deleting a vertex does not require modifying other vertices. === "Python" @@ -426,22 +1280,22 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l """Add vertex""" if vet in self.adj_list: return - # Add a new linked list to the adjacency list + # Add a new linked list in the adjacency list self.adj_list[vet] = [] def remove_vertex(self, vet: Vertex): """Remove vertex""" if vet not in self.adj_list: raise ValueError() - # Remove the vertex vet's corresponding linked list from the adjacency list + # Remove the linked list corresponding to vertex vet in the adjacency list self.adj_list.pop(vet) - # Traverse other vertices' linked lists, removing all edges containing vet + # Traverse the linked lists of other vertices and remove all edges containing vet for vertex in self.adj_list: if vet in self.adj_list[vertex]: self.adj_list[vertex].remove(vet) def print(self): - """Print the adjacency list""" + """Print adjacency list""" print("Adjacency list =") for vertex in self.adj_list: tmp = [v.val for v in self.adj_list[vertex]] @@ -457,7 +1311,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l // Adjacency list, key: vertex, value: all adjacent vertices of that vertex unordered_map> adjList; - /* Remove a specified node from vector */ + /* Remove specified node from vector */ void remove(vector &vec, Vertex *vet) { for (int i = 0; i < vec.size(); i++) { if (vec[i] == vet) { @@ -504,7 +1358,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l void addVertex(Vertex *vet) { if (adjList.count(vet)) return; - // Add a new linked list to the adjacency list + // Add a new linked list in the adjacency list adjList[vet] = vector(); } @@ -512,15 +1366,15 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l void removeVertex(Vertex *vet) { if (!adjList.count(vet)) throw invalid_argument("Vertex does not exist"); - // Remove the vertex vet's corresponding linked list from the adjacency list + // Remove the linked list corresponding to vertex vet in the adjacency list adjList.erase(vet); - // Traverse other vertices' linked lists, removing all edges containing vet + // Traverse the linked lists of other vertices and remove all edges containing vet for (auto &adj : adjList) { remove(adj.second, vet); } } - /* Print the adjacency list */ + /* Print adjacency list */ void print() { cout << "Adjacency list =" << endl; for (auto &adj : adjList) { @@ -579,7 +1433,7 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l public void addVertex(Vertex vet) { if (adjList.containsKey(vet)) return; - // Add a new linked list to the adjacency list + // Add a new linked list in the adjacency list adjList.put(vet, new ArrayList<>()); } @@ -587,15 +1441,15 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l public void removeVertex(Vertex vet) { if (!adjList.containsKey(vet)) throw new IllegalArgumentException(); - // Remove the vertex vet's corresponding linked list from the adjacency list + // Remove the linked list corresponding to vertex vet in the adjacency list adjList.remove(vet); - // Traverse other vertices' linked lists, removing all edges containing vet + // Traverse the linked lists of other vertices and remove all edges containing vet for (List list : adjList.values()) { list.remove(vet); } } - /* Print the adjacency list */ + /* Print adjacency list */ public void print() { System.out.println("Adjacency list ="); for (Map.Entry> pair : adjList.entrySet()) { @@ -611,88 +1465,917 @@ Additionally, we use the `Vertex` class to represent vertices in the adjacency l === "C#" ```csharp title="graph_adjacency_list.cs" - [class]{GraphAdjList}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + public Dictionary> adjList; + + /* Constructor */ + public GraphAdjList(Vertex[][] edges) { + adjList = []; + // Add all vertices and edges + foreach (Vertex[] edge in edges) { + AddVertex(edge[0]); + AddVertex(edge[1]); + AddEdge(edge[0], edge[1]); + } + } + + /* Get the number of vertices */ + int Size() { + return adjList.Count; + } + + /* Add edge */ + public void AddEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // Add edge vet1 - vet2 + adjList[vet1].Add(vet2); + adjList[vet2].Add(vet1); + } + + /* Remove edge */ + public void RemoveEdge(Vertex vet1, Vertex vet2) { + if (!adjList.ContainsKey(vet1) || !adjList.ContainsKey(vet2) || vet1 == vet2) + throw new InvalidOperationException(); + // Remove edge vet1 - vet2 + adjList[vet1].Remove(vet2); + adjList[vet2].Remove(vet1); + } + + /* Add vertex */ + public void AddVertex(Vertex vet) { + if (adjList.ContainsKey(vet)) + return; + // Add a new linked list in the adjacency list + adjList.Add(vet, []); + } + + /* Remove vertex */ + public void RemoveVertex(Vertex vet) { + if (!adjList.ContainsKey(vet)) + throw new InvalidOperationException(); + // Remove the linked list corresponding to vertex vet in the adjacency list + adjList.Remove(vet); + // Traverse the linked lists of other vertices and remove all edges containing vet + foreach (List list in adjList.Values) { + list.Remove(vet); + } + } + + /* Print adjacency list */ + public void Print() { + Console.WriteLine("Adjacency list ="); + foreach (KeyValuePair> pair in adjList) { + List tmp = []; + foreach (Vertex vertex in pair.Value) + tmp.Add(vertex.val); + Console.WriteLine(pair.Key.val + ": [" + string.Join(", ", tmp) + "],"); + } + } + } ``` === "Go" ```go title="graph_adjacency_list.go" - [class]{graphAdjList}-[func]{} + /* Undirected graph class based on adjacency list */ + type graphAdjList struct { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + adjList map[Vertex][]Vertex + } + + /* Constructor */ + func newGraphAdjList(edges [][]Vertex) *graphAdjList { + g := &graphAdjList{ + adjList: make(map[Vertex][]Vertex), + } + // Add all vertices and edges + for _, edge := range edges { + g.addVertex(edge[0]) + g.addVertex(edge[1]) + g.addEdge(edge[0], edge[1]) + } + return g + } + + /* Get the number of vertices */ + func (g *graphAdjList) size() int { + return len(g.adjList) + } + + /* Add edge */ + func (g *graphAdjList) addEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // Add edge vet1 - vet2, add anonymous struct{}, + g.adjList[vet1] = append(g.adjList[vet1], vet2) + g.adjList[vet2] = append(g.adjList[vet2], vet1) + } + + /* Remove edge */ + func (g *graphAdjList) removeEdge(vet1 Vertex, vet2 Vertex) { + _, ok1 := g.adjList[vet1] + _, ok2 := g.adjList[vet2] + if !ok1 || !ok2 || vet1 == vet2 { + panic("error") + } + // Remove edge vet1 - vet2 + g.adjList[vet1] = DeleteSliceElms(g.adjList[vet1], vet2) + g.adjList[vet2] = DeleteSliceElms(g.adjList[vet2], vet1) + } + + /* Add vertex */ + func (g *graphAdjList) addVertex(vet Vertex) { + _, ok := g.adjList[vet] + if ok { + return + } + // Add a new linked list in the adjacency list + g.adjList[vet] = make([]Vertex, 0) + } + + /* Remove vertex */ + func (g *graphAdjList) removeVertex(vet Vertex) { + _, ok := g.adjList[vet] + if !ok { + panic("error") + } + // Remove the linked list corresponding to vertex vet in the adjacency list + delete(g.adjList, vet) + // Traverse the linked lists of other vertices and remove all edges containing vet + for v, list := range g.adjList { + g.adjList[v] = DeleteSliceElms(list, vet) + } + } + + /* Print adjacency list */ + func (g *graphAdjList) print() { + var builder strings.Builder + fmt.Printf("Adjacency list = \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}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + public private(set) var adjList: [Vertex: [Vertex]] + + /* Constructor */ + public init(edges: [[Vertex]]) { + adjList = [:] + // Add all vertices and edges + for edge in edges { + addVertex(vet: edge[0]) + addVertex(vet: edge[1]) + addEdge(vet1: edge[0], vet2: edge[1]) + } + } + + /* Get the number of vertices */ + public func size() -> Int { + adjList.count + } + + /* Add edge */ + public func addEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("Invalid parameter") + } + // Add edge vet1 - vet2 + adjList[vet1]?.append(vet2) + adjList[vet2]?.append(vet1) + } + + /* Remove edge */ + public func removeEdge(vet1: Vertex, vet2: Vertex) { + if adjList[vet1] == nil || adjList[vet2] == nil || vet1 == vet2 { + fatalError("Invalid parameter") + } + // Remove edge vet1 - vet2 + adjList[vet1]?.removeAll { $0 == vet2 } + adjList[vet2]?.removeAll { $0 == vet1 } + } + + /* Add vertex */ + public func addVertex(vet: Vertex) { + if adjList[vet] != nil { + return + } + // Add a new linked list in the adjacency list + adjList[vet] = [] + } + + /* Remove vertex */ + public func removeVertex(vet: Vertex) { + if adjList[vet] == nil { + fatalError("Invalid parameter") + } + // Remove the linked list corresponding to vertex vet in the adjacency list + adjList.removeValue(forKey: vet) + // Traverse the linked lists of other vertices and remove all edges containing vet + for key in adjList.keys { + adjList[key]?.removeAll { $0 == vet } + } + } + + /* Print adjacency list */ + public func print() { + Swift.print("Adjacency list =") + for (vertex, list) in adjList { + let list = list.map { $0.val } + Swift.print("\(vertex.val): \(list),") + } + } + } ``` === "JS" ```javascript title="graph_adjacency_list.js" - [class]{GraphAdjList}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + adjList; + + /* Constructor */ + constructor(edges) { + this.adjList = new Map(); + // Add all vertices and edges + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* Get the number of vertices */ + size() { + return this.adjList.size; + } + + /* Add edge */ + addEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // Add edge vet1 - vet2 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* Remove edge */ + removeEdge(vet1, vet2) { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 || + this.adjList.get(vet1).indexOf(vet2) === -1 + ) { + throw new Error('Illegal Argument Exception'); + } + // Remove edge 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); + } + + /* Add vertex */ + addVertex(vet) { + if (this.adjList.has(vet)) return; + // Add a new linked list in the adjacency list + this.adjList.set(vet, []); + } + + /* Remove vertex */ + removeVertex(vet) { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // Remove the linked list corresponding to vertex vet in the adjacency list + this.adjList.delete(vet); + // Traverse the linked lists of other vertices and remove all edges containing vet + for (const set of this.adjList.values()) { + const index = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* Print adjacency list */ + print() { + console.log('Adjacency list ='); + 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}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + adjList: Map; + + /* Constructor */ + constructor(edges: Vertex[][]) { + this.adjList = new Map(); + // Add all vertices and edges + for (const edge of edges) { + this.addVertex(edge[0]); + this.addVertex(edge[1]); + this.addEdge(edge[0], edge[1]); + } + } + + /* Get the number of vertices */ + size(): number { + return this.adjList.size; + } + + /* Add edge */ + addEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 + ) { + throw new Error('Illegal Argument Exception'); + } + // Add edge vet1 - vet2 + this.adjList.get(vet1).push(vet2); + this.adjList.get(vet2).push(vet1); + } + + /* Remove edge */ + removeEdge(vet1: Vertex, vet2: Vertex): void { + if ( + !this.adjList.has(vet1) || + !this.adjList.has(vet2) || + vet1 === vet2 || + this.adjList.get(vet1).indexOf(vet2) === -1 + ) { + throw new Error('Illegal Argument Exception'); + } + // Remove edge 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); + } + + /* Add vertex */ + addVertex(vet: Vertex): void { + if (this.adjList.has(vet)) return; + // Add a new linked list in the adjacency list + this.adjList.set(vet, []); + } + + /* Remove vertex */ + removeVertex(vet: Vertex): void { + if (!this.adjList.has(vet)) { + throw new Error('Illegal Argument Exception'); + } + // Remove the linked list corresponding to vertex vet in the adjacency list + this.adjList.delete(vet); + // Traverse the linked lists of other vertices and remove all edges containing vet + for (const set of this.adjList.values()) { + const index: number = set.indexOf(vet); + if (index > -1) { + set.splice(index, 1); + } + } + } + + /* Print adjacency list */ + print(): void { + console.log('Adjacency list ='); + 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}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + Map> adjList = {}; + + /* Constructor */ + GraphAdjList(List> edges) { + for (List edge in edges) { + addVertex(edge[0]); + addVertex(edge[1]); + addEdge(edge[0], edge[1]); + } + } + + /* Get the number of vertices */ + int size() { + return adjList.length; + } + + /* Add edge */ + void addEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // Add edge vet1 - vet2 + adjList[vet1]!.add(vet2); + adjList[vet2]!.add(vet1); + } + + /* Remove edge */ + void removeEdge(Vertex vet1, Vertex vet2) { + if (!adjList.containsKey(vet1) || + !adjList.containsKey(vet2) || + vet1 == vet2) { + throw ArgumentError; + } + // Remove edge vet1 - vet2 + adjList[vet1]!.remove(vet2); + adjList[vet2]!.remove(vet1); + } + + /* Add vertex */ + void addVertex(Vertex vet) { + if (adjList.containsKey(vet)) return; + // Add a new linked list in the adjacency list + adjList[vet] = []; + } + + /* Remove vertex */ + void removeVertex(Vertex vet) { + if (!adjList.containsKey(vet)) { + throw ArgumentError; + } + // Remove the linked list corresponding to vertex vet in the adjacency list + adjList.remove(vet); + // Traverse the linked lists of other vertices and remove all edges containing vet + adjList.forEach((key, value) { + value.remove(vet); + }); + } + + /* Print adjacency list */ + void printAdjList() { + print("Adjacency list ="); + 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" - [class]{GraphAdjList}-[func]{} + /* Undirected graph type based on adjacency list */ + pub struct GraphAdjList { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + pub adj_list: HashMap>, // maybe HashSet for value part is better? + } + + impl GraphAdjList { + /* Constructor */ + pub fn new(edges: Vec<[Vertex; 2]>) -> Self { + let mut graph = GraphAdjList { + adj_list: HashMap::new(), + }; + // Add all vertices and edges + for edge in edges { + graph.add_vertex(edge[0]); + graph.add_vertex(edge[1]); + graph.add_edge(edge[0], edge[1]); + } + + graph + } + + /* Get the number of vertices */ + #[allow(unused)] + pub fn size(&self) -> usize { + self.adj_list.len() + } + + /* Add edge */ + pub fn add_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if vet1 == vet2 { + panic!("value error"); + } + // Add edge vet1 - vet2 + self.adj_list.entry(vet1).or_default().push(vet2); + self.adj_list.entry(vet2).or_default().push(vet1); + } + + /* Remove edge */ + #[allow(unused)] + pub fn remove_edge(&mut self, vet1: Vertex, vet2: Vertex) { + if vet1 == vet2 { + panic!("value error"); + } + // Remove edge vet1 - vet2 + self.adj_list + .entry(vet1) + .and_modify(|v| v.retain(|&e| e != vet2)); + self.adj_list + .entry(vet2) + .and_modify(|v| v.retain(|&e| e != vet1)); + } + + /* Add vertex */ + pub fn add_vertex(&mut self, vet: Vertex) { + if self.adj_list.contains_key(&vet) { + return; + } + // Add a new linked list in the adjacency list + self.adj_list.insert(vet, vec![]); + } + + /* Remove vertex */ + #[allow(unused)] + pub fn remove_vertex(&mut self, vet: Vertex) { + // Remove the linked list corresponding to vertex vet in the adjacency list + self.adj_list.remove(&vet); + // Traverse the linked lists of other vertices and remove all edges containing vet + for list in self.adj_list.values_mut() { + list.retain(|&v| v != vet); + } + } + + /* Print adjacency list */ + pub fn print(&self) { + println!("Adjacency list ="); + 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" - [class]{AdjListNode}-[func]{} + /* Node structure */ + typedef struct AdjListNode { + Vertex *vertex; // Vertex + struct AdjListNode *next; // Successor node + } AdjListNode; - [class]{GraphAdjList}-[func]{} + /* Find node corresponding to vertex */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* Add edge helper function */ + void addEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *node = (AdjListNode *)malloc(sizeof(AdjListNode)); + node->vertex = vet; + // Head insertion + node->next = head->next; + head->next = node; + } + + /* Remove edge helper function */ + void removeEdgeHelper(AdjListNode *head, Vertex *vet) { + AdjListNode *pre = head; + AdjListNode *cur = head->next; + // Search for node corresponding to vet in list + while (cur != NULL && cur->vertex != vet) { + pre = cur; + cur = cur->next; + } + if (cur == NULL) + return; + // Remove node corresponding to vet from list + pre->next = cur->next; + // Free memory + free(cur); + } + + /* Undirected graph class based on adjacency list */ + typedef struct { + AdjListNode *heads[MAX_SIZE]; // Node array + int size; // Node count + } GraphAdjList; + + /* Constructor */ + GraphAdjList *newGraphAdjList() { + GraphAdjList *graph = (GraphAdjList *)malloc(sizeof(GraphAdjList)); + if (!graph) { + return NULL; + } + graph->size = 0; + for (int i = 0; i < MAX_SIZE; i++) { + graph->heads[i] = NULL; + } + return graph; + } + + /* Destructor */ + void delGraphAdjList(GraphAdjList *graph) { + for (int i = 0; i < graph->size; i++) { + AdjListNode *cur = graph->heads[i]; + while (cur != NULL) { + AdjListNode *next = cur->next; + if (cur != graph->heads[i]) { + free(cur); + } + cur = next; + } + free(graph->heads[i]->vertex); + free(graph->heads[i]); + } + free(graph); + } + + /* Find node corresponding to vertex */ + AdjListNode *findNode(GraphAdjList *graph, Vertex *vet) { + for (int i = 0; i < graph->size; i++) { + if (graph->heads[i]->vertex == vet) { + return graph->heads[i]; + } + } + return NULL; + } + + /* Add edge */ + void addEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL && head1 != head2); + // Add edge vet1 - vet2 + addEdgeHelper(head1, vet2); + addEdgeHelper(head2, vet1); + } + + /* Remove edge */ + void removeEdge(GraphAdjList *graph, Vertex *vet1, Vertex *vet2) { + AdjListNode *head1 = findNode(graph, vet1); + AdjListNode *head2 = findNode(graph, vet2); + assert(head1 != NULL && head2 != NULL); + // Remove edge vet1 - vet2 + removeEdgeHelper(head1, head2->vertex); + removeEdgeHelper(head2, head1->vertex); + } + + /* Add vertex */ + void addVertex(GraphAdjList *graph, Vertex *vet) { + assert(graph != NULL && graph->size < MAX_SIZE); + AdjListNode *head = (AdjListNode *)malloc(sizeof(AdjListNode)); + head->vertex = vet; + head->next = NULL; + // Add a new linked list in the adjacency list + graph->heads[graph->size++] = head; + } + + /* Remove vertex */ + void removeVertex(GraphAdjList *graph, Vertex *vet) { + AdjListNode *node = findNode(graph, vet); + assert(node != NULL); + // Remove the linked list corresponding to vertex vet in the adjacency list + AdjListNode *cur = node, *pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + free(pre); + } + // Traverse the linked lists of other vertices and remove all edges containing vet + for (int i = 0; i < graph->size; i++) { + cur = graph->heads[i]; + pre = NULL; + while (cur) { + pre = cur; + cur = cur->next; + if (cur && cur->vertex == vet) { + pre->next = cur->next; + free(cur); + break; + } + } + } + // Move vertices after this vertex forward to fill gap + int i; + for (i = 0; i < graph->size; i++) { + if (graph->heads[i] == node) + break; + } + for (int j = i; j < graph->size - 1; j++) { + graph->heads[j] = graph->heads[j + 1]; + } + graph->size--; + free(vet); + } ``` === "Kotlin" ```kotlin title="graph_adjacency_list.kt" - [class]{GraphAdjList}-[func]{} + /* Undirected graph class based on adjacency list */ + class GraphAdjList(edges: Array>) { + // Adjacency list, key: vertex, value: all adjacent vertices of that vertex + val adjList = HashMap>() + + /* Constructor */ + init { + // Add all vertices and edges + for (edge in edges) { + addVertex(edge[0]!!) + addVertex(edge[1]!!) + addEdge(edge[0]!!, edge[1]!!) + } + } + + /* Get the number of vertices */ + fun size(): Int { + return adjList.size + } + + /* Add edge */ + fun addEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // Add edge vet1 - vet2 + adjList[vet1]?.add(vet2) + adjList[vet2]?.add(vet1) + } + + /* Remove edge */ + fun removeEdge(vet1: Vertex, vet2: Vertex) { + if (!adjList.containsKey(vet1) || !adjList.containsKey(vet2) || vet1 == vet2) + throw IllegalArgumentException() + // Remove edge vet1 - vet2 + adjList[vet1]?.remove(vet2) + adjList[vet2]?.remove(vet1) + } + + /* Add vertex */ + fun addVertex(vet: Vertex) { + if (adjList.containsKey(vet)) + return + // Add a new linked list in the adjacency list + adjList[vet] = mutableListOf() + } + + /* Remove vertex */ + fun removeVertex(vet: Vertex) { + if (!adjList.containsKey(vet)) + throw IllegalArgumentException() + // Remove the linked list corresponding to vertex vet in the adjacency list + adjList.remove(vet) + // Traverse the linked lists of other vertices and remove all edges containing vet + for (list in adjList.values) { + list.remove(vet) + } + } + + /* Print adjacency list */ + fun print() { + println("Adjacency list =") + for (pair in adjList.entries) { + val tmp = mutableListOf() + for (vertex in pair.value) { + tmp.add(vertex._val) + } + println("${pair.key._val}: $tmp,") + } + } + } ``` === "Ruby" ```ruby title="graph_adjacency_list.rb" - [class]{GraphAdjList}-[func]{} + ### Undirected graph class based on adjacency list ### + class GraphAdjList + attr_reader :adj_list + + ### Constructor ### + def initialize(edges) + # Adjacency list, key: vertex, value: all adjacent vertices of that vertex + @adj_list = {} + # Add all vertices and edges + for edge in edges + add_vertex(edge[0]) + add_vertex(edge[1]) + add_edge(edge[0], edge[1]) + end + end + + ### Get number of vertices ### + def size + @adj_list.length + end + + ### Add edge ### + def add_edge(vet1, vet2) + raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2) + + @adj_list[vet1] << vet2 + @adj_list[vet2] << vet1 + end + + ### Delete edge ### + def remove_edge(vet1, vet2) + raise ArgumentError if !@adj_list.include?(vet1) || !@adj_list.include?(vet2) + + # Remove edge vet1 - vet2 + @adj_list[vet1].delete(vet2) + @adj_list[vet2].delete(vet1) + end + + ### Add vertex ### + def add_vertex(vet) + return if @adj_list.include?(vet) + + # Add a new linked list in the adjacency list + @adj_list[vet] = [] + end + + ### Delete vertex ### + def remove_vertex(vet) + raise ArgumentError unless @adj_list.include?(vet) + + # Remove the linked list corresponding to vertex vet in the adjacency list + @adj_list.delete(vet) + # Traverse the linked lists of other vertices and remove all edges containing vet + for vertex in @adj_list + @adj_list[vertex.first].delete(vet) if @adj_list[vertex.first].include?(vet) + end + end + + ### Print adjacency list ### + def __print__ + puts 'Adjacency list =' + for vertex in @adj_list + tmp = @adj_list[vertex.first].map { |v| v.val } + puts "#{vertex.first.val}: #{tmp}," + end + end + end ``` -=== "Zig" +## 9.2.3   Efficiency Comparison - ```zig title="graph_adjacency_list.zig" - [class]{GraphAdjList}-[func]{} - ``` - -## 9.2.3   Efficiency comparison - -Assuming there are $n$ vertices and $m$ edges in the graph, Table 9-2 compares the time efficiency and space efficiency of the adjacency matrix and adjacency list. +Assuming the graph has $n$ vertices and $m$ edges, Table 9-2 compares the time efficiency and space efficiency of adjacency matrices and adjacency lists. Note that the adjacency list (linked list) corresponds to the implementation in this text, while the adjacency list (hash table) refers specifically to the implementation where all linked lists are replaced with hash tables.

Table 9-2   Comparison of adjacency matrix and adjacency list

-| | Adjacency matrix | Adjacency list (Linked list) | Adjacency list (Hash table) | -| ------------------- | ---------------- | ---------------------------- | --------------------------- | -| Determine adjacency | $O(1)$ | $O(m)$ | $O(1)$ | -| Add an edge | $O(1)$ | $O(1)$ | $O(1)$ | -| Remove an edge | $O(1)$ | $O(m)$ | $O(1)$ | -| Add a vertex | $O(n)$ | $O(1)$ | $O(1)$ | -| Remove a vertex | $O(n^2)$ | $O(n + m)$ | $O(n)$ | -| Memory space usage | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ | +| | Adjacency matrix | Adjacency list (linked list) | Adjacency list (hash table) | +| ---------------------- | ---------------- | ---------------------------- | --------------------------- | +| Determine adjacency | $O(1)$ | $O(n)$ | $O(1)$ | +| Add an edge | $O(1)$ | $O(1)$ | $O(1)$ | +| Remove an edge | $O(1)$ | $O(n)$ | $O(1)$ | +| Add a vertex | $O(n)$ | $O(1)$ | $O(1)$ | +| Remove a vertex | $O(n^2)$ | $O(n + m)$ | $O(n)$ | +| Memory space usage | $O(n^2)$ | $O(n + m)$ | $O(n + m)$ |
-Observing Table 9-2, it seems that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, the adjacency matrix exemplifies the principle of "space for time", while the adjacency list exemplifies "time for space". +Observing Table 9-2, it appears that the adjacency list (hash table) has the best time efficiency and space efficiency. However, in practice, operating on edges in the adjacency matrix is more efficient, requiring only a single array access or assignment operation. Overall, adjacency matrices embody the principle of "trading space for time", while adjacency lists embody "trading time for space". diff --git a/en/docs/chapter_graph/graph_traversal.md b/en/docs/chapter_graph/graph_traversal.md index 08ede11d2..4b9c4c698 100644 --- a/en/docs/chapter_graph/graph_traversal.md +++ b/en/docs/chapter_graph/graph_traversal.md @@ -2,53 +2,57 @@ comments: true --- -# 9.3   Graph traversal +# 9.3   Graph Traversal -Trees represent a "one-to-many" relationship, while graphs have a higher degree of freedom and can represent any "many-to-many" relationship. Therefore, we can consider tree as a special case of graph. Clearly, **tree traversal operations are also a special case of graph traversal operations**. +Trees represent "one-to-many" relationships, while graphs have a higher degree of freedom and can represent any "many-to-many" relationships. Therefore, we can view trees as a special case of graphs. Clearly, **tree traversal operations are also a special case of graph traversal operations**. -Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal can be divided into two types: Breadth-First Search (BFS) and Depth-First Search (DFS). +Both graphs and trees require the application of search algorithms to implement traversal operations. Graph traversal methods can also be divided into two types: breadth-first traversal and depth-first traversal. -## 9.3.1   Breadth-first search +## 9.3.1   Breadth-First Search -**Breadth-first search is a near-to-far traversal method, starting from a certain node, always prioritizing the visit to the nearest vertices and expanding outwards layer by layer**. As shown in Figure 9-9, starting from the top left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. +**Breadth-first search is a near-to-far traversal method that, starting from a certain node, always prioritizes visiting the nearest vertices and expands outward layer by layer**. As shown in Figure 9-9, starting from the top-left vertex, first traverse all adjacent vertices of that vertex, then traverse all adjacent vertices of the next vertex, and so on, until all vertices have been visited. -![Breadth-first traversal of a graph](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } +![Breadth-first search of a graph](graph_traversal.assets/graph_bfs.png){ class="animation-figure" } -

Figure 9-9   Breadth-first traversal of a graph

+

Figure 9-9   Breadth-first search of a graph

-### 1.   Algorithm implementation +### 1.   Algorithm Implementation -BFS is usually implemented with the help of a queue, as shown in the code below. The queue is "first in, first out", which aligns with the BFS idea of traversing "from near to far". +BFS is typically implemented with the help of a queue, as shown in the code below. The queue has a "first in, first out" property, which aligns with the BFS idea of "near to far". -1. Add the starting vertex `startVet` to the queue and start the loop. +1. Add the starting vertex `startVet` to the queue and begin the loop. 2. In each iteration of the loop, pop the vertex at the front of the queue and record it as visited, then add all adjacent vertices of that vertex to the back of the queue. 3. Repeat step `2.` until all vertices have been visited. To prevent revisiting vertices, we use a hash set `visited` to record which nodes have been visited. +!!! tip + + A hash set can be viewed as a hash table that stores only `key` without storing `value`. It can perform addition, deletion, lookup, and modification operations on `key` in $O(1)$ time complexity. Based on the uniqueness of `key`, hash sets are typically used for data deduplication and similar scenarios. + === "Python" ```python title="graph_bfs.py" def graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: """Breadth-first traversal""" - # Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex # Vertex traversal sequence res = [] - # Hash set, used to record visited vertices + # Hash set for recording vertices that have been visited visited = set[Vertex]([start_vet]) # Queue used to implement BFS que = deque[Vertex]([start_vet]) # Starting from vertex vet, loop until all vertices are visited while len(que) > 0: - vet = que.popleft() # Dequeue the vertex at the head of the queue + vet = que.popleft() # Dequeue the front vertex res.append(vet) # Record visited vertex - # Traverse all adjacent vertices of that vertex + # Traverse all adjacent vertices of this vertex for adj_vet in graph.adj_list[vet]: if adj_vet in visited: - continue # Skip already visited vertices + continue # Skip vertices that have been visited que.append(adj_vet) # Only enqueue unvisited vertices - visited.add(adj_vet) # Mark the vertex as visited - # Return the vertex traversal sequence + visited.add(adj_vet) # Mark this vertex as visited + # Return vertex traversal sequence return res ``` @@ -56,11 +60,11 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node ```cpp title="graph_bfs.cpp" /* Breadth-first traversal */ - // Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex vector graphBFS(GraphAdjList &graph, Vertex *startVet) { // Vertex traversal sequence vector res; - // Hash set, used to record visited vertices + // Hash set for recording vertices that have been visited unordered_set visited = {startVet}; // Queue used to implement BFS queue que; @@ -68,17 +72,17 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node // Starting from vertex vet, loop until all vertices are visited while (!que.empty()) { Vertex *vet = que.front(); - que.pop(); // Dequeue the vertex at the head of the queue + que.pop(); // Dequeue the front vertex res.push_back(vet); // Record visited vertex - // Traverse all adjacent vertices of that vertex + // Traverse all adjacent vertices of this vertex for (auto adjVet : graph.adjList[vet]) { if (visited.count(adjVet)) - continue; // Skip already visited vertices + continue; // Skip vertices that have been visited que.push(adjVet); // Only enqueue unvisited vertices - visited.emplace(adjVet); // Mark the vertex as visited + visited.emplace(adjVet); // Mark this vertex as visited } } - // Return the vertex traversal sequence + // Return vertex traversal sequence return res; } ``` @@ -87,11 +91,11 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node ```java title="graph_bfs.java" /* Breadth-first traversal */ - // Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex List graphBFS(GraphAdjList graph, Vertex startVet) { // Vertex traversal sequence List res = new ArrayList<>(); - // Hash set, used to record visited vertices + // Hash set for recording vertices that have been visited Set visited = new HashSet<>(); visited.add(startVet); // Queue used to implement BFS @@ -99,17 +103,17 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node que.offer(startVet); // Starting from vertex vet, loop until all vertices are visited while (!que.isEmpty()) { - Vertex vet = que.poll(); // Dequeue the vertex at the head of the queue + Vertex vet = que.poll(); // Dequeue the front vertex res.add(vet); // Record visited vertex - // Traverse all adjacent vertices of that vertex + // Traverse all adjacent vertices of this vertex for (Vertex adjVet : graph.adjList.get(vet)) { if (visited.contains(adjVet)) - continue; // Skip already visited vertices + continue; // Skip vertices that have been visited que.offer(adjVet); // Only enqueue unvisited vertices - visited.add(adjVet); // Mark the vertex as visited + visited.add(adjVet); // Mark this vertex as visited } } - // Return the vertex traversal sequence + // Return vertex traversal sequence return res; } ``` @@ -117,74 +121,361 @@ To prevent revisiting vertices, we use a hash set `visited` to record which node === "C#" ```csharp title="graph_bfs.cs" - [class]{graph_bfs}-[func]{GraphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + List GraphBFS(GraphAdjList graph, Vertex startVet) { + // Vertex traversal sequence + List res = []; + // Hash set for recording vertices that have been visited + HashSet visited = [startVet]; + // Queue used to implement BFS + Queue que = new(); + que.Enqueue(startVet); + // Starting from vertex vet, loop until all vertices are visited + while (que.Count > 0) { + Vertex vet = que.Dequeue(); // Dequeue the front vertex + res.Add(vet); // Record visited vertex + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // Skip vertices that have been visited + } + que.Enqueue(adjVet); // Only enqueue unvisited vertices + visited.Add(adjVet); // Mark this vertex as visited + } + } + + // Return vertex traversal sequence + return res; + } ``` === "Go" ```go title="graph_bfs.go" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + func graphBFS(g *graphAdjList, startVet Vertex) []Vertex { + // Vertex traversal sequence + res := make([]Vertex, 0) + // Hash set for recording vertices that have been visited + visited := make(map[Vertex]struct{}) + visited[startVet] = struct{}{} + // Queue used to implement BFS, using slice to simulate queue + queue := make([]Vertex, 0) + queue = append(queue, startVet) + // Starting from vertex vet, loop until all vertices are visited + for len(queue) > 0 { + // Dequeue the front vertex + vet := queue[0] + queue = queue[1:] + // Record visited vertex + res = append(res, vet) + // Traverse all adjacent vertices of this vertex + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // Only enqueue unvisited vertices + if !isExist { + queue = append(queue, adjVet) + visited[adjVet] = struct{}{} + } + } + } + // Return vertex traversal sequence + return res + } ``` === "Swift" ```swift title="graph_bfs.swift" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + func graphBFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // Vertex traversal sequence + var res: [Vertex] = [] + // Hash set for recording vertices that have been visited + var visited: Set = [startVet] + // Queue used to implement BFS + var que: [Vertex] = [startVet] + // Starting from vertex vet, loop until all vertices are visited + while !que.isEmpty { + let vet = que.removeFirst() // Dequeue the front vertex + res.append(vet) // Record visited vertex + // Traverse all adjacent vertices of this vertex + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // Skip vertices that have been visited + } + que.append(adjVet) // Only enqueue unvisited vertices + visited.insert(adjVet) // Mark this vertex as visited + } + } + // Return vertex traversal sequence + return res + } ``` === "JS" ```javascript title="graph_bfs.js" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + function graphBFS(graph, startVet) { + // Vertex traversal sequence + const res = []; + // Hash set for recording vertices that have been visited + const visited = new Set(); + visited.add(startVet); + // Queue used to implement BFS + const que = [startVet]; + // Starting from vertex vet, loop until all vertices are visited + while (que.length) { + const vet = que.shift(); // Dequeue the front vertex + res.push(vet); // Record visited vertex + // Traverse all adjacent vertices of this vertex + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // Skip vertices that have been visited + } + que.push(adjVet); // Only enqueue unvisited vertices + visited.add(adjVet); // Mark this vertex as visited + } + } + // Return vertex traversal sequence + return res; + } ``` === "TS" ```typescript title="graph_bfs.ts" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + function graphBFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // Vertex traversal sequence + const res: Vertex[] = []; + // Hash set for recording vertices that have been visited + const visited: Set = new Set(); + visited.add(startVet); + // Queue used to implement BFS + const que = [startVet]; + // Starting from vertex vet, loop until all vertices are visited + while (que.length) { + const vet = que.shift(); // Dequeue the front vertex + res.push(vet); // Record visited vertex + // Traverse all adjacent vertices of this vertex + for (const adjVet of graph.adjList.get(vet) ?? []) { + if (visited.has(adjVet)) { + continue; // Skip vertices that have been visited + } + que.push(adjVet); // Only enqueue unvisited + visited.add(adjVet); // Mark this vertex as visited + } + } + // Return vertex traversal sequence + return res; + } ``` === "Dart" ```dart title="graph_bfs.dart" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + List graphBFS(GraphAdjList graph, Vertex startVet) { + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + // Vertex traversal sequence + List res = []; + // Hash set for recording vertices that have been visited + Set visited = {}; + visited.add(startVet); + // Queue used to implement BFS + Queue que = Queue(); + que.add(startVet); + // Starting from vertex vet, loop until all vertices are visited + while (que.isNotEmpty) { + Vertex vet = que.removeFirst(); // Dequeue the front vertex + res.add(vet); // Record visited vertex + // Traverse all adjacent vertices of this vertex + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // Skip vertices that have been visited + } + que.add(adjVet); // Only enqueue unvisited vertices + visited.add(adjVet); // Mark this vertex as visited + } + } + // Return vertex traversal sequence + return res; + } ``` === "Rust" ```rust title="graph_bfs.rs" - [class]{}-[func]{graph_bfs} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + fn graph_bfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // Vertex traversal sequence + let mut res = vec![]; + // Hash set for recording vertices that have been visited + let mut visited = HashSet::new(); + visited.insert(start_vet); + // Queue used to implement BFS + let mut que = VecDeque::new(); + que.push_back(start_vet); + // Starting from vertex vet, loop until all vertices are visited + while let Some(vet) = que.pop_front() { + res.push(vet); // Record visited vertex + + // Traverse all adjacent vertices of this vertex + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // Skip vertices that have been visited + } + que.push_back(adj_vet); // Only enqueue unvisited vertices + visited.insert(adj_vet); // Mark this vertex as visited + } + } + } + // Return vertex traversal sequence + res + } ``` === "C" ```c title="graph_bfs.c" - [class]{Queue}-[func]{} + /* Node queue structure */ + typedef struct { + Vertex *vertices[MAX_SIZE]; + int front, rear, size; + } Queue; - [class]{}-[func]{isVisited} + /* Constructor */ + Queue *newQueue() { + Queue *q = (Queue *)malloc(sizeof(Queue)); + q->front = q->rear = q->size = 0; + return q; + } - [class]{}-[func]{graphBFS} + /* Check if the queue is empty */ + int isEmpty(Queue *q) { + return q->size == 0; + } + + /* Enqueue operation */ + void enqueue(Queue *q, Vertex *vet) { + q->vertices[q->rear] = vet; + q->rear = (q->rear + 1) % MAX_SIZE; + q->size++; + } + + /* Dequeue operation */ + Vertex *dequeue(Queue *q) { + Vertex *vet = q->vertices[q->front]; + q->front = (q->front + 1) % MAX_SIZE; + q->size--; + return vet; + } + + /* Check if vertex has been visited */ + int isVisited(Vertex **visited, int size, Vertex *vet) { + // Traverse to find node using O(n) time + for (int i = 0; i < size; i++) { + if (visited[i] == vet) + return 1; + } + return 0; + } + + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + void graphBFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize, Vertex **visited, int *visitedSize) { + // Queue used to implement BFS + Queue *queue = newQueue(); + enqueue(queue, startVet); + visited[(*visitedSize)++] = startVet; + // Starting from vertex vet, loop until all vertices are visited + while (!isEmpty(queue)) { + Vertex *vet = dequeue(queue); // Dequeue the front vertex + res[(*resSize)++] = vet; // Record visited vertex + // Traverse all adjacent vertices of this vertex + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // Skip vertices that have been visited + if (!isVisited(visited, *visitedSize, node->vertex)) { + enqueue(queue, node->vertex); // Only enqueue unvisited vertices + visited[(*visitedSize)++] = node->vertex; // Mark this vertex as visited + } + node = node->next; + } + } + // Free memory + free(queue); + } ``` === "Kotlin" ```kotlin title="graph_bfs.kt" - [class]{}-[func]{graphBFS} + /* Breadth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + fun graphBFS(graph: GraphAdjList, startVet: Vertex): MutableList { + // Vertex traversal sequence + val res = mutableListOf() + // Hash set for recording vertices that have been visited + val visited = HashSet() + visited.add(startVet) + // Queue used to implement BFS + val que = LinkedList() + que.offer(startVet) + // Starting from vertex vet, loop until all vertices are visited + while (!que.isEmpty()) { + val vet = que.poll() // Dequeue the front vertex + res.add(vet) // Record visited vertex + // Traverse all adjacent vertices of this vertex + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) + continue // Skip vertices that have been visited + que.offer(adjVet) // Only enqueue unvisited vertices + visited.add(adjVet) // Mark this vertex as visited + } + } + // Return vertex traversal sequence + return res + } ``` === "Ruby" ```ruby title="graph_bfs.rb" - [class]{}-[func]{graph_bfs} + ### Breadth-first traversal ### + def graph_bfs(graph, start_vet) + # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + # Vertex traversal sequence + res = [] + # Hash set for recording vertices that have been visited + visited = Set.new([start_vet]) + # Queue used to implement BFS + que = [start_vet] + # Starting from vertex vet, loop until all vertices are visited + while que.length > 0 + vet = que.shift # Dequeue the front vertex + res << vet # Record visited vertex + # Traverse all adjacent vertices of this vertex + for adj_vet in graph.adj_list[vet] + next if visited.include?(adj_vet) # Skip vertices that have been visited + que << adj_vet # Only enqueue unvisited vertices + visited.add(adj_vet) # Mark this vertex as visited + end + end + # Return vertex traversal sequence + res + end ``` -=== "Zig" - - ```zig title="graph_bfs.zig" - [class]{}-[func]{graphBFS} - ``` - -The code is relatively abstract, you can compare it with Figure 9-10 to get a better understanding. +The code is relatively abstract; it is recommended to refer to Figure 9-10 to deepen understanding. === "<1>" ![Steps of breadth-first search of a graph](graph_traversal.assets/graph_bfs_step1.png){ class="animation-figure" } @@ -221,27 +512,27 @@ The code is relatively abstract, you can compare it with Figure 9-10 to get a be

Figure 9-10   Steps of breadth-first search of a graph

-!!! question "Is the sequence of breadth-first traversal unique?" +!!! question "Is the breadth-first traversal sequence unique?" - Not unique. Breadth-first traversal only requires traversing in a "near to far" order, **and the traversal order of the vertices with the same distance can be arbitrary**. For example, in Figure 9-10, the visit order of vertices $1$ and $3$ can be swapped, as can the order of vertices $2$, $4$, and $6$. + Not unique. Breadth-first search only requires traversing in a "near to far" order, **and the traversal order of vertices at the same distance can be arbitrarily shuffled**. Taking Figure 9-10 as an example, the visit order of vertices $1$ and $3$ can be swapped, as can the visit order of vertices $2$, $4$, and $6$. -### 2.   Complexity analysis +### 2.   Complexity Analysis **Time complexity**: All vertices will be enqueued and dequeued once, using $O(|V|)$ time; in the process of traversing adjacent vertices, since it is an undirected graph, all edges will be visited $2$ times, using $O(2|E|)$ time; overall using $O(|V| + |E|)$ time. -**Space complexity**: The maximum number of vertices in list `res`, hash set `visited`, and queue `que` is $|V|$, using $O(|V|)$ space. +**Space complexity**: The list `res`, hash set `visited`, and queue `que` can contain at most $|V|$ vertices, using $O(|V|)$ space. -## 9.3.2   Depth-first search +## 9.3.2   Depth-First Search -**Depth-first search is a traversal method that prioritizes going as far as possible and then backtracks when no further path is available**. As shown in Figure 9-11, starting from the top left vertex, visit some adjacent vertex of the current vertex until no further path is available, then return and continue until all vertices are traversed. +**Depth-first search is a traversal method that prioritizes going as far as possible, then backtracks when no path remains**. As shown in Figure 9-11, starting from the top-left vertex, visit an adjacent vertex of the current vertex, continuing until reaching a dead end, then return and continue going as far as possible before returning again, and so on, until all vertices have been traversed. -![Depth-first traversal of a graph](graph_traversal.assets/graph_dfs.png){ class="animation-figure" } +![Depth-first search of a graph](graph_traversal.assets/graph_dfs.png){ class="animation-figure" } -

Figure 9-11   Depth-first traversal of a graph

+

Figure 9-11   Depth-first search of a graph

-### 1.   Algorithm implementation +### 1.   Algorithm Implementation -This "go as far as possible and then return" algorithm paradigm is usually implemented based on recursion. Similar to breadth-first search, in depth-first search, we also need the help of a hash set `visited` to record the visited vertices to avoid revisiting. +This "go as far as possible then return" algorithm paradigm is typically implemented using recursion. Similar to breadth-first search, in depth-first search we also need a hash set `visited` to record visited vertices and avoid revisiting. === "Python" @@ -249,20 +540,20 @@ This "go as far as possible and then return" algorithm paradigm is usually imple def dfs(graph: GraphAdjList, visited: set[Vertex], res: list[Vertex], vet: Vertex): """Depth-first traversal helper function""" res.append(vet) # Record visited vertex - visited.add(vet) # Mark the vertex as visited - # Traverse all adjacent vertices of that vertex + visited.add(vet) # Mark this vertex as visited + # Traverse all adjacent vertices of this vertex for adjVet in graph.adj_list[vet]: if adjVet in visited: - continue # Skip already visited vertices + continue # Skip vertices that have been visited # Recursively visit adjacent vertices dfs(graph, visited, res, adjVet) def graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> list[Vertex]: """Depth-first traversal""" - # Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex # Vertex traversal sequence res = [] - # Hash set, used to record visited vertices + # Hash set for recording vertices that have been visited visited = set[Vertex]() dfs(graph, visited, res, start_vet) return res @@ -274,22 +565,22 @@ This "go as far as possible and then return" algorithm paradigm is usually imple /* Depth-first traversal helper function */ void dfs(GraphAdjList &graph, unordered_set &visited, vector &res, Vertex *vet) { res.push_back(vet); // Record visited vertex - visited.emplace(vet); // Mark the vertex as visited - // Traverse all adjacent vertices of that vertex + visited.emplace(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex for (Vertex *adjVet : graph.adjList[vet]) { if (visited.count(adjVet)) - continue; // Skip already visited vertices + continue; // Skip vertices that have been visited // Recursively visit adjacent vertices dfs(graph, visited, res, adjVet); } } /* Depth-first traversal */ - // Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex vector graphDFS(GraphAdjList &graph, Vertex *startVet) { // Vertex traversal sequence vector res; - // Hash set, used to record visited vertices + // Hash set for recording vertices that have been visited unordered_set visited; dfs(graph, visited, res, startVet); return res; @@ -302,22 +593,22 @@ This "go as far as possible and then return" algorithm paradigm is usually imple /* Depth-first traversal helper function */ void dfs(GraphAdjList graph, Set visited, List res, Vertex vet) { res.add(vet); // Record visited vertex - visited.add(vet); // Mark the vertex as visited - // Traverse all adjacent vertices of that vertex + visited.add(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex for (Vertex adjVet : graph.adjList.get(vet)) { if (visited.contains(adjVet)) - continue; // Skip already visited vertices + continue; // Skip vertices that have been visited // Recursively visit adjacent vertices dfs(graph, visited, res, adjVet); } } /* Depth-first traversal */ - // Use adjacency list to represent the graph, to obtain all adjacent vertices of a specified vertex + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex List graphDFS(GraphAdjList graph, Vertex startVet) { // Vertex traversal sequence List res = new ArrayList<>(); - // Hash set, used to record visited vertices + // Hash set for recording vertices that have been visited Set visited = new HashSet<>(); dfs(graph, visited, res, startVet); return res; @@ -327,99 +618,324 @@ This "go as far as possible and then return" algorithm paradigm is usually imple === "C#" ```csharp title="graph_dfs.cs" - [class]{graph_dfs}-[func]{DFS} + /* Depth-first traversal helper function */ + void DFS(GraphAdjList graph, HashSet visited, List res, Vertex vet) { + res.Add(vet); // Record visited vertex + visited.Add(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + foreach (Vertex adjVet in graph.adjList[vet]) { + if (visited.Contains(adjVet)) { + continue; // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + DFS(graph, visited, res, adjVet); + } + } - [class]{graph_dfs}-[func]{GraphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + List GraphDFS(GraphAdjList graph, Vertex startVet) { + // Vertex traversal sequence + List res = []; + // Hash set for recording vertices that have been visited + HashSet visited = []; + DFS(graph, visited, res, startVet); + return res; + } ``` === "Go" ```go title="graph_dfs.go" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + func dfs(g *graphAdjList, visited map[Vertex]struct{}, res *[]Vertex, vet Vertex) { + // append operation returns a new reference, must reassign original reference to new slice's reference + *res = append(*res, vet) + visited[vet] = struct{}{} + // Traverse all adjacent vertices of this vertex + for _, adjVet := range g.adjList[vet] { + _, isExist := visited[adjVet] + // Recursively visit adjacent vertices + if !isExist { + dfs(g, visited, res, adjVet) + } + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + func graphDFS(g *graphAdjList, startVet Vertex) []Vertex { + // Vertex traversal sequence + res := make([]Vertex, 0) + // Hash set for recording vertices that have been visited + visited := make(map[Vertex]struct{}) + dfs(g, visited, &res, startVet) + // Return vertex traversal sequence + return res + } ``` === "Swift" ```swift title="graph_dfs.swift" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + func dfs(graph: GraphAdjList, visited: inout Set, res: inout [Vertex], vet: Vertex) { + res.append(vet) // Record visited vertex + visited.insert(vet) // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + for adjVet in graph.adjList[vet] ?? [] { + if visited.contains(adjVet) { + continue // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + dfs(graph: graph, visited: &visited, res: &res, vet: adjVet) + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + func graphDFS(graph: GraphAdjList, startVet: Vertex) -> [Vertex] { + // Vertex traversal sequence + var res: [Vertex] = [] + // Hash set for recording vertices that have been visited + var visited: Set = [] + dfs(graph: graph, visited: &visited, res: &res, vet: startVet) + return res + } ``` === "JS" ```javascript title="graph_dfs.js" - [class]{}-[func]{dfs} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + function dfs(graph, visited, res, vet) { + res.push(vet); // Record visited vertex + visited.add(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + function graphDFS(graph, startVet) { + // Vertex traversal sequence + const res = []; + // Hash set for recording vertices that have been visited + const visited = new Set(); + dfs(graph, visited, res, startVet); + return res; + } ``` === "TS" ```typescript title="graph_dfs.ts" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + function dfs( + graph: GraphAdjList, + visited: Set, + res: Vertex[], + vet: Vertex + ): void { + res.push(vet); // Record visited vertex + visited.add(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + for (const adjVet of graph.adjList.get(vet)) { + if (visited.has(adjVet)) { + continue; // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + function graphDFS(graph: GraphAdjList, startVet: Vertex): Vertex[] { + // Vertex traversal sequence + const res: Vertex[] = []; + // Hash set for recording vertices that have been visited + const visited: Set = new Set(); + dfs(graph, visited, res, startVet); + return res; + } ``` === "Dart" ```dart title="graph_dfs.dart" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + void dfs( + GraphAdjList graph, + Set visited, + List res, + Vertex vet, + ) { + res.add(vet); // Record visited vertex + visited.add(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + for (Vertex adjVet in graph.adjList[vet]!) { + if (visited.contains(adjVet)) { + continue; // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + dfs(graph, visited, res, adjVet); + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + List graphDFS(GraphAdjList graph, Vertex startVet) { + // Vertex traversal sequence + List res = []; + // Hash set for recording vertices that have been visited + Set visited = {}; + dfs(graph, visited, res, startVet); + return res; + } ``` === "Rust" ```rust title="graph_dfs.rs" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + fn dfs(graph: &GraphAdjList, visited: &mut HashSet, res: &mut Vec, vet: Vertex) { + res.push(vet); // Record visited vertex + visited.insert(vet); // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + if let Some(adj_vets) = graph.adj_list.get(&vet) { + for &adj_vet in adj_vets { + if visited.contains(&adj_vet) { + continue; // Skip vertices that have been visited + } + // Recursively visit adjacent vertices + dfs(graph, visited, res, adj_vet); + } + } + } - [class]{}-[func]{graph_dfs} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + fn graph_dfs(graph: GraphAdjList, start_vet: Vertex) -> Vec { + // Vertex traversal sequence + let mut res = vec![]; + // Hash set for recording vertices that have been visited + let mut visited = HashSet::new(); + dfs(&graph, &mut visited, &mut res, start_vet); + + res + } ``` === "C" ```c title="graph_dfs.c" - [class]{}-[func]{isVisited} + /* Check if vertex has been visited */ + int isVisited(Vertex **res, int size, Vertex *vet) { + // Traverse to find node using O(n) time + for (int i = 0; i < size; i++) { + if (res[i] == vet) { + return 1; + } + } + return 0; + } - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + void dfs(GraphAdjList *graph, Vertex **res, int *resSize, Vertex *vet) { + // Record visited vertex + res[(*resSize)++] = vet; + // Traverse all adjacent vertices of this vertex + AdjListNode *node = findNode(graph, vet); + while (node != NULL) { + // Skip vertices that have been visited + if (!isVisited(res, *resSize, node->vertex)) { + // Recursively visit adjacent vertices + dfs(graph, res, resSize, node->vertex); + } + node = node->next; + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + void graphDFS(GraphAdjList *graph, Vertex *startVet, Vertex **res, int *resSize) { + dfs(graph, res, resSize, startVet); + } ``` === "Kotlin" ```kotlin title="graph_dfs.kt" - [class]{}-[func]{dfs} + /* Depth-first traversal helper function */ + fun dfs( + graph: GraphAdjList, + visited: MutableSet, + res: MutableList, + vet: Vertex? + ) { + res.add(vet) // Record visited vertex + visited.add(vet) // Mark this vertex as visited + // Traverse all adjacent vertices of this vertex + for (adjVet in graph.adjList[vet]!!) { + if (visited.contains(adjVet)) + continue // Skip vertices that have been visited + // Recursively visit adjacent vertices + dfs(graph, visited, res, adjVet) + } + } - [class]{}-[func]{graphDFS} + /* Depth-first traversal */ + // Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + fun graphDFS(graph: GraphAdjList, startVet: Vertex?): MutableList { + // Vertex traversal sequence + val res = mutableListOf() + // Hash set for recording vertices that have been visited + val visited = HashSet() + dfs(graph, visited, res, startVet) + return res + } ``` === "Ruby" ```ruby title="graph_dfs.rb" - [class]{}-[func]{dfs} + ### Depth-first traversal helper function ### + def dfs(graph, visited, res, vet) + res << vet # Record visited vertex + visited.add(vet) # Mark this vertex as visited + # Traverse all adjacent vertices of this vertex + for adj_vet in graph.adj_list[vet] + next if visited.include?(adj_vet) # Skip vertices that have been visited + # Recursively visit adjacent vertices + dfs(graph, visited, res, adj_vet) + end + end - [class]{}-[func]{graph_dfs} + ### Depth-first traversal ### + def graph_dfs(graph, start_vet) + # Use adjacency list to represent the graph, in order to obtain all adjacent vertices of a specified vertex + # Vertex traversal sequence + res = [] + # Hash set for recording vertices that have been visited + visited = Set.new + dfs(graph, visited, res, start_vet) + res + end ``` -=== "Zig" +The algorithm flow of depth-first search is shown in Figure 9-12. - ```zig title="graph_dfs.zig" - [class]{}-[func]{dfs} +- **Straight dashed lines represent downward recursion**, indicating that a new recursive method has been initiated to visit a new vertex. +- **Curved dashed lines represent upward backtracking**, indicating that this recursive method has returned to the position where it was initiated. - [class]{}-[func]{graphDFS} - ``` - -The algorithm process of depth-first search is shown in Figure 9-12. - -- **Dashed lines represent downward recursion**, indicating that a new recursive method has been initiated to visit a new vertex. -- **Curved dashed lines represent upward backtracking**, indicating that this recursive method has returned to the position where this method was initiated. - -To deepen the understanding, it is suggested to combine Figure 9-12 with the code to simulate (or draw) the entire DFS process in your mind, including when each recursive method is initiated and when it returns. +To deepen understanding, it is recommended to combine Figure 9-12 with the code to mentally simulate (or draw out) the entire DFS process, including when each recursive method is initiated and when it returns. === "<1>" ![Steps of depth-first search of a graph](graph_traversal.assets/graph_dfs_step1.png){ class="animation-figure" } @@ -456,14 +972,14 @@ To deepen the understanding, it is suggested to combine Figure 9-12 with the cod

Figure 9-12   Steps of depth-first search of a graph

-!!! question "Is the sequence of depth-first traversal unique?" +!!! question "Is the depth-first traversal sequence unique?" - Similar to breadth-first traversal, the order of the depth-first traversal sequence is also not unique. Given a certain vertex, exploring in any direction first is possible, that is, the order of adjacent vertices can be arbitrarily shuffled, all being part of depth-first traversal. + Similar to breadth-first search, the order of depth-first traversal sequences is also not unique. Given a certain vertex, exploring in any direction first is valid, meaning the order of adjacent vertices can be arbitrarily shuffled, all being depth-first search. - Taking tree traversal as an example, "root $\rightarrow$ left $\rightarrow$ right", "left $\rightarrow$ root $\rightarrow$ right", "left $\rightarrow$ right $\rightarrow$ root" correspond to pre-order, in-order, and post-order traversals, respectively. They showcase three types of traversal priorities, yet all three are considered depth-first traversal. + Taking tree traversal as an example, "root $\rightarrow$ left $\rightarrow$ right", "left $\rightarrow$ root $\rightarrow$ right", and "left $\rightarrow$ right $\rightarrow$ root" correspond to pre-order, in-order, and post-order traversals, respectively. They represent three different traversal priorities, yet all three belong to depth-first search. -### 2.   Complexity analysis +### 2.   Complexity Analysis -**Time complexity**: All vertices will be visited once, using $O(|V|)$ time; all edges will be visited twice, using $O(2|E|)$ time; overall using $O(|V| + |E|)$ time. +**Time complexity**: All vertices will be visited $1$ time, using $O(|V|)$ time; all edges will be visited $2$ times, using $O(2|E|)$ time; overall using $O(|V| + |E|)$ time. -**Space complexity**: The maximum number of vertices in list `res`, hash set `visited` is $|V|$, and the maximum recursion depth is $|V|$, therefore using $O(|V|)$ space. +**Space complexity**: The list `res` and hash set `visited` can contain at most $|V|$ vertices, and the maximum recursion depth is $|V|$, therefore using $O(|V|)$ space. diff --git a/en/docs/chapter_graph/index.md b/en/docs/chapter_graph/index.md index 967e2166c..c7f2c63cf 100644 --- a/en/docs/chapter_graph/index.md +++ b/en/docs/chapter_graph/index.md @@ -9,13 +9,13 @@ icon: material/graphql !!! abstract - In the journey of life, each of us is a node, connected by countless invisible edges. - - Each encounter and parting leaves a unique imprint on this vast graph of life. + In the journey of life, we are like nodes, connected by countless invisible edges. + + Each encounter and parting leaves a unique mark on this vast network graph. ## Chapter contents - [9.1   Graph](graph.md) -- [9.2   Basic graph operations](graph_operations.md) -- [9.3   Graph traversal](graph_traversal.md) +- [9.2   Basic Operations on Graphs](graph_operations.md) +- [9.3   Graph Traversal](graph_traversal.md) - [9.4   Summary](summary.md) diff --git a/en/docs/chapter_graph/summary.md b/en/docs/chapter_graph/summary.md index 2bb4dd497..ab9272a28 100644 --- a/en/docs/chapter_graph/summary.md +++ b/en/docs/chapter_graph/summary.md @@ -4,32 +4,32 @@ comments: true # 9.4   Summary -### 1.   Key review +### 1.   Key Review -- A graph is made up of vertices and edges. It can be described as a set of vertices and a set of edges. -- Compared to linear relationships (like linked lists) and hierarchical relationships (like trees), network relationships (graphs) offer greater flexibility, making them more complex. -- In a directed graph, edges have directions. In a connected graph, any vertex can be reached from any other vertex. In a weighted graph, each edge has an associated weight variable. -- An adjacency matrix is a way to represent a graph using matrix (2D array). The rows and columns represent the vertices. The matrix element value indicates whether there is an edge between two vertices, using $1$ for an edge or $0$ for no edge. Adjacency matrices are highly efficient for operations like adding, deleting, or checking edges, but they require more space. -- An adjacency list is another common way to represent a graph using a collection of linked lists. Each vertex in the graph has a list that contains all its adjacent vertices. The $i^{th}$ list represents vertex $i$. Adjacency lists use less space compared to adjacency matrices. However, since it requires traversing the list to find edges, the time efficiency is lower. -- When the linked lists in an adjacency list are long enough, they can be converted into red-black trees or hash tables to improve lookup efficiency. -- From the perspective of algorithmic design, an adjacency matrix reflects the concept of "trading space for time", whereas an adjacency list reflects "trading time for space". -- Graphs can be used to model various real-world systems, such as social networks, subway routes. -- A tree is a special case of a graph, and tree traversal is also a special case of graph traversal. -- Breadth-first traversal of a graph is a search method that expands layer by layer from near to far, typically using a queue. -- Depth-first traversal of a graph is a search method that prioritizes reaching the end before backtracking when no further path is available. It is often implemented using recursion. +- Graphs consist of vertices and edges and can be represented as a set of vertices and a set of edges. +- Compared to linear relationships (linked lists) and divide-and-conquer relationships (trees), network relationships (graphs) have a higher degree of freedom and are therefore more complex. +- Directed graphs have edges with directionality, connected graphs have all vertices reachable from any vertex, and weighted graphs have edges that each contain a weight variable. +- Adjacency matrices use matrices to represent graphs, where each row (column) represents a vertex, and matrix elements represent edges, using $1$ or $0$ to indicate whether two vertices have an edge or not. Adjacency matrices are highly efficient for addition, deletion, lookup, and modification operations, but consume significant space. +- Adjacency lists use multiple linked lists to represent graphs, where the $i$-th linked list corresponds to vertex $i$ and stores all adjacent vertices of that vertex. Adjacency lists are more space-efficient than adjacency matrices, but have lower time efficiency because they require traversing linked lists to find edges. +- When linked lists in adjacency lists become too long, they can be converted to red-black trees or hash tables, thereby improving lookup efficiency. +- From an algorithmic perspective, adjacency matrices embody "trading space for time", while adjacency lists embody "trading time for space". +- Graphs can be used to model various real-world systems, such as social networks and subway lines. +- Trees are a special case of graphs, and tree traversal is a special case of graph traversal. +- Breadth-first search of graphs is a near-to-far, layer-by-layer expansion search method, typically implemented using a queue. +- Depth-first search of graphs is a search method that prioritizes going as far as possible and backtracks when no path remains, commonly implemented using recursion. ### 2.   Q & A **Q**: Is a path defined as a sequence of vertices or a sequence of edges? -In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices. +The definitions in different language versions of Wikipedia are inconsistent: the English version states "a path is a sequence of edges", while the Chinese version states "a path is a sequence of vertices". The following is the original English text: In graph theory, a path in a graph is a finite or infinite sequence of edges which joins a sequence of vertices. -In this document, a path is considered a sequence of edges, rather than a sequence of vertices. This is because there might be multiple edges connecting two vertices, in which case each edge corresponds to a path. +In this text, a path is viewed as a sequence of edges, not a sequence of vertices. This is because there may be multiple edges connecting two vertices, in which case each edge corresponds to a path. -**Q**: In a disconnected graph, are there points that cannot be traversed? +**Q**: In a disconnected graph, will there be unreachable vertices? -In a disconnected graph, there is at least one vertex that cannot be reached from a specific point. To traverse a disconnected graph, you need to set multiple starting points to traverse all the connected components of the graph. +In a disconnected graph, starting from a certain vertex, at least one vertex cannot be reached. Traversing a disconnected graph requires setting multiple starting points to traverse all connected components of the graph. -**Q**: In an adjacency list, does the order of "all vertices connected to that vertex" matter? +**Q**: In an adjacency list, is there a requirement for the order of "all vertices connected to that vertex"? -It can be in any order. However, in real-world applications, it might be necessary to sort them according to certain rules, such as the order in which vertices are added, or the order of vertex values. This can help find vertices quickly with certain extreme values. +It can be in any order. However, in practical applications, it may be necessary to sort according to specified rules, such as the order in which vertices were added, or the order of vertex values, which helps quickly find vertices "with certain extreme values". diff --git a/en/docs/chapter_greedy/fractional_knapsack_problem.md b/en/docs/chapter_greedy/fractional_knapsack_problem.md index 8815e4446..0970aa26d 100644 --- a/en/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/en/docs/chapter_greedy/fractional_knapsack_problem.md @@ -2,42 +2,42 @@ comments: true --- -# 15.2   Fractional knapsack problem +# 15.2   Fractional Knapsack Problem !!! question - Given $n$ items, the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with a capacity of $cap$. Each item can be chosen only once, **but a part of the item can be selected, with its value calculated based on the proportion of the weight chosen**, what is the maximum value of the items in the knapsack under the limited capacity? An example is shown in Figure 15-3. + Given $n$ items, where the weight of the $i$-th item is $wgt[i-1]$ and its value is $val[i-1]$, and a knapsack with capacity $cap$. Each item can be selected only once, **but a portion of an item can be selected, with the value calculated based on the proportion of weight selected**, what is the maximum value of items in the knapsack under the limited capacity? An example is shown in Figure 15-3. -![Example data of the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" } +![Example data for the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_example.png){ class="animation-figure" } -

Figure 15-3   Example data of the fractional knapsack problem

+

Figure 15-3   Example data for the fractional knapsack problem

-The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, involving the current item $i$ and capacity $c$, aiming to maximize the value within the limited capacity of the knapsack. +The fractional knapsack problem is very similar overall to the 0-1 knapsack problem, with states including the current item $i$ and capacity $c$, and the goal being to maximize value under the limited knapsack capacity. -The difference is that, in this problem, only a part of an item can be chosen. As shown in Figure 15-4, **we can arbitrarily split the items and calculate the corresponding value based on the weight proportion**. +The difference is that this problem allows selecting only a portion of an item. As shown in Figure 15-4, **we can arbitrarily split items and calculate the corresponding value based on the weight proportion**. -1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as the unit value. -2. Suppose we put a part of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$. +1. For item $i$, its value per unit weight is $val[i-1] / wgt[i-1]$, referred to as unit value. +2. Suppose we put a portion of item $i$ with weight $w$ into the knapsack, then the value added to the knapsack is $w \times val[i-1] / wgt[i-1]$. -![Value per unit weight of the item](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" } +![Value of items per unit weight](fractional_knapsack_problem.assets/fractional_knapsack_unit_value.png){ class="animation-figure" } -

Figure 15-4   Value per unit weight of the item

+

Figure 15-4   Value of items per unit weight

-### 1.   Greedy strategy determination +### 1.   Greedy Strategy Determination -Maximizing the total value of the items in the knapsack **essentially means maximizing the value per unit weight**. From this, the greedy strategy shown in Figure 15-5 can be deduced. +Maximizing the total value of items in the knapsack **is essentially maximizing the value per unit weight of items**. From this, we can derive the greedy strategy shown in Figure 15-5. -1. Sort the items by their unit value from high to low. -2. Iterate over all items, **greedily choosing the item with the highest unit value in each round**. -3. If the remaining capacity of the knapsack is insufficient, use part of the current item to fill the knapsack. +1. Sort items by unit value from high to low. +2. Iterate through all items, **greedily selecting the item with the highest unit value in each round**. +3. If the remaining knapsack capacity is insufficient, use a portion of the current item to fill the knapsack. -![Greedy strategy of the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" } +![Greedy strategy for the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_greedy_strategy.png){ class="animation-figure" } -

Figure 15-5   Greedy strategy of the fractional knapsack problem

+

Figure 15-5   Greedy strategy for the fractional knapsack problem

-### 2.   Code implementation +### 2.   Code Implementation -We have created an `Item` class in order to sort the items by their unit value. We loop and make greedy choices until the knapsack is full, then exit and return the solution: +We created an `Item` class to facilitate sorting items by unit value. We loop to make greedy selections, breaking when the knapsack is full and returning the solution: === "Python" @@ -50,8 +50,8 @@ We have created an `Item` class in order to sort the items by their unit value. self.v = v # Item value def fractional_knapsack(wgt: list[int], val: list[int], cap: int) -> int: - """Fractional knapsack: Greedy""" - # Create an item list, containing two properties: weight, value + """Fractional knapsack: Greedy algorithm""" + # Create item list with two attributes: weight, value items = [Item(w, v) for w, v in zip(wgt, val)] # Sort by unit value item.v / item.w from high to low items.sort(key=lambda item: item.v / item.w, reverse=True) @@ -59,13 +59,13 @@ We have created an `Item` class in order to sort the items by their unit value. res = 0 for item in items: if item.w <= cap: - # If the remaining capacity is sufficient, put the entire item into the knapsack + # If remaining capacity is sufficient, put the entire current item into the knapsack res += item.v cap -= item.w else: - # If the remaining capacity is insufficient, put part of the item into the knapsack + # If remaining capacity is insufficient, put part of the current item into the knapsack res += (item.v / item.w) * cap - # No remaining capacity left, thus break the loop + # No remaining capacity, so break out of the loop break return res ``` @@ -83,9 +83,9 @@ We have created an `Item` class in order to sort the items by their unit value. } }; - /* Fractional knapsack: Greedy */ + /* Fractional knapsack: Greedy algorithm */ double fractionalKnapsack(vector &wgt, vector &val, int cap) { - // Create an item list, containing two properties: weight, value + // Create item list with two attributes: weight, value vector items; for (int i = 0; i < wgt.size(); i++) { items.push_back(Item(wgt[i], val[i])); @@ -96,13 +96,13 @@ We have created an `Item` class in order to sort the items by their unit value. double res = 0; for (auto &item : items) { if (item.w <= cap) { - // If the remaining capacity is sufficient, put the entire item into the knapsack + // If remaining capacity is sufficient, put the entire current item into the knapsack res += item.v; cap -= item.w; } else { - // If the remaining capacity is insufficient, put part of the item into the knapsack + // If remaining capacity is insufficient, put part of the current item into the knapsack res += (double)item.v / item.w * cap; - // No remaining capacity left, thus break the loop + // No remaining capacity, so break out of the loop break; } } @@ -124,9 +124,9 @@ We have created an `Item` class in order to sort the items by their unit value. } } - /* Fractional knapsack: Greedy */ + /* Fractional knapsack: Greedy algorithm */ double fractionalKnapsack(int[] wgt, int[] val, int cap) { - // Create an item list, containing two properties: weight, value + // Create item list with two attributes: weight, value Item[] items = new Item[wgt.length]; for (int i = 0; i < wgt.length; i++) { items[i] = new Item(wgt[i], val[i]); @@ -137,13 +137,13 @@ We have created an `Item` class in order to sort the items by their unit value. double res = 0; for (Item item : items) { if (item.w <= cap) { - // If the remaining capacity is sufficient, put the entire item into the knapsack + // If remaining capacity is sufficient, put the entire current item into the knapsack res += item.v; cap -= item.w; } else { - // If the remaining capacity is insufficient, put part of the item into the knapsack + // If remaining capacity is insufficient, put part of the current item into the knapsack res += (double) item.v / item.w * cap; - // No remaining capacity left, thus break the loop + // No remaining capacity, so break out of the loop break; } } @@ -154,104 +154,398 @@ We have created an `Item` class in order to sort the items by their unit value. === "C#" ```csharp title="fractional_knapsack.cs" - [class]{Item}-[func]{} + /* Item */ + class Item(int w, int v) { + public int w = w; // Item weight + public int v = v; // Item value + } - [class]{fractional_knapsack}-[func]{FractionalKnapsack} + /* Fractional knapsack: Greedy algorithm */ + double FractionalKnapsack(int[] wgt, int[] val, int cap) { + // Create item list with two attributes: weight, value + Item[] items = new Item[wgt.Length]; + for (int i = 0; i < wgt.Length; i++) { + items[i] = new Item(wgt[i], val[i]); + } + // Sort by unit value item.v / item.w from high to low + Array.Sort(items, (x, y) => (y.v / y.w).CompareTo(x.v / x.w)); + // Loop for greedy selection + double res = 0; + foreach (Item item in items) { + if (item.w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v; + cap -= item.w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += (double)item.v / item.w * cap; + // No remaining capacity, so break out of the loop + break; + } + } + return res; + } ``` === "Go" ```go title="fractional_knapsack.go" - [class]{Item}-[func]{} + /* Item */ + type Item struct { + w int // Item weight + v int // Item value + } - [class]{}-[func]{fractionalKnapsack} + /* Fractional knapsack: Greedy algorithm */ + func fractionalKnapsack(wgt []int, val []int, cap int) float64 { + // Create item list with two attributes: weight, value + items := make([]Item, len(wgt)) + for i := 0; i < len(wgt); i++ { + items[i] = Item{wgt[i], val[i]} + } + // Sort by unit value item.v / item.w from high to low + 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) + }) + // Loop for greedy selection + res := 0.0 + for _, item := range items { + if item.w <= cap { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += float64(item.v) + cap -= item.w + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += float64(item.v) / float64(item.w) * float64(cap) + // No remaining capacity, so break out of the loop + break + } + } + return res + } ``` === "Swift" ```swift title="fractional_knapsack.swift" - [class]{Item}-[func]{} + /* Item */ + class Item { + var w: Int // Item weight + var v: Int // Item value - [class]{}-[func]{fractionalKnapsack} + init(w: Int, v: Int) { + self.w = w + self.v = v + } + } + + /* Fractional knapsack: Greedy algorithm */ + func fractionalKnapsack(wgt: [Int], val: [Int], cap: Int) -> Double { + // Create item list with two attributes: weight, value + var items = zip(wgt, val).map { Item(w: $0, v: $1) } + // Sort by unit value item.v / item.w from high to low + items.sort { -(Double($0.v) / Double($0.w)) < -(Double($1.v) / Double($1.w)) } + // Loop for greedy selection + var res = 0.0 + var cap = cap + for item in items { + if item.w <= cap { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += Double(item.v) + cap -= item.w + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += Double(item.v) / Double(item.w) * Double(cap) + // No remaining capacity, so break out of the loop + break + } + } + return res + } ``` === "JS" ```javascript title="fractional_knapsack.js" - [class]{Item}-[func]{} + /* Item */ + class Item { + constructor(w, v) { + this.w = w; // Item weight + this.v = v; // Item value + } + } - [class]{}-[func]{fractionalKnapsack} + /* Fractional knapsack: Greedy algorithm */ + function fractionalKnapsack(wgt, val, cap) { + // Create item list with two attributes: weight, value + const items = wgt.map((w, i) => new Item(w, val[i])); + // Sort by unit value item.v / item.w from high to low + items.sort((a, b) => b.v / b.w - a.v / a.w); + // Loop for greedy selection + let res = 0; + for (const item of items) { + if (item.w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v; + cap -= item.w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += (item.v / item.w) * cap; + // No remaining capacity, so break out of the loop + break; + } + } + return res; + } ``` === "TS" ```typescript title="fractional_knapsack.ts" - [class]{Item}-[func]{} + /* Item */ + class Item { + w: number; // Item weight + v: number; // Item value - [class]{}-[func]{fractionalKnapsack} + constructor(w: number, v: number) { + this.w = w; + this.v = v; + } + } + + /* Fractional knapsack: Greedy algorithm */ + function fractionalKnapsack(wgt: number[], val: number[], cap: number): number { + // Create item list with two attributes: weight, value + const items: Item[] = wgt.map((w, i) => new Item(w, val[i])); + // Sort by unit value item.v / item.w from high to low + items.sort((a, b) => b.v / b.w - a.v / a.w); + // Loop for greedy selection + let res = 0; + for (const item of items) { + if (item.w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v; + cap -= item.w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += (item.v / item.w) * cap; + // No remaining capacity, so break out of the loop + break; + } + } + return res; + } ``` === "Dart" ```dart title="fractional_knapsack.dart" - [class]{Item}-[func]{} + /* Item */ + class Item { + int w; // Item weight + int v; // Item value - [class]{}-[func]{fractionalKnapsack} + Item(this.w, this.v); + } + + /* Fractional knapsack: Greedy algorithm */ + double fractionalKnapsack(List wgt, List val, int cap) { + // Create item list with two attributes: weight, value + List items = List.generate(wgt.length, (i) => Item(wgt[i], val[i])); + // Sort by unit value item.v / item.w from high to low + items.sort((a, b) => (b.v / b.w).compareTo(a.v / a.w)); + // Loop for greedy selection + double res = 0; + for (Item item in items) { + if (item.w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v; + cap -= item.w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += item.v / item.w * cap; + // No remaining capacity, so break out of the loop + break; + } + } + return res; + } ``` === "Rust" ```rust title="fractional_knapsack.rs" - [class]{Item}-[func]{} + /* Item */ + struct Item { + w: i32, // Item weight + v: i32, // Item value + } - [class]{}-[func]{fractional_knapsack} + impl Item { + fn new(w: i32, v: i32) -> Self { + Self { w, v } + } + } + + /* Fractional knapsack: Greedy algorithm */ + fn fractional_knapsack(wgt: &[i32], val: &[i32], mut cap: i32) -> f64 { + // Create item list with two attributes: weight, value + let mut items = wgt + .iter() + .zip(val.iter()) + .map(|(&w, &v)| Item::new(w, v)) + .collect::>(); + // Sort by unit value item.v / item.w from high to low + items.sort_by(|a, b| { + (b.v as f64 / b.w as f64) + .partial_cmp(&(a.v as f64 / a.w as f64)) + .unwrap() + }); + // Loop for greedy selection + let mut res = 0.0; + for item in &items { + if item.w <= cap { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v as f64; + cap -= item.w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += item.v as f64 / item.w as f64 * cap as f64; + // No remaining capacity, so break out of the loop + break; + } + } + res + } ``` === "C" ```c title="fractional_knapsack.c" - [class]{Item}-[func]{} + /* Item */ + typedef struct { + int w; // Item weight + int v; // Item value + } Item; - [class]{}-[func]{fractionalKnapsack} + /* Fractional knapsack: Greedy algorithm */ + float fractionalKnapsack(int wgt[], int val[], int itemCount, int cap) { + // Create item list with two attributes: weight, value + Item *items = malloc(sizeof(Item) * itemCount); + for (int i = 0; i < itemCount; i++) { + items[i] = (Item){.w = wgt[i], .v = val[i]}; + } + // Sort by unit value item.v / item.w from high to low + qsort(items, (size_t)itemCount, sizeof(Item), sortByValueDensity); + // Loop for greedy selection + float res = 0.0; + for (int i = 0; i < itemCount; i++) { + if (items[i].w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += items[i].v; + cap -= items[i].w; + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += (float)cap / items[i].w * items[i].v; + cap = 0; + break; + } + } + free(items); + return res; + } ``` === "Kotlin" ```kotlin title="fractional_knapsack.kt" - [class]{Item}-[func]{} + /* Item */ + class Item( + val w: Int, // Item + val v: Int // Item value + ) - [class]{}-[func]{fractionalKnapsack} + /* Fractional knapsack: Greedy algorithm */ + fun fractionalKnapsack(wgt: IntArray, _val: IntArray, c: Int): Double { + // Create item list with two attributes: weight, value + var cap = c + val items = arrayOfNulls(wgt.size) + for (i in wgt.indices) { + items[i] = Item(wgt[i], _val[i]) + } + // Sort by unit value item.v / item.w from high to low + items.sortBy { item: Item? -> -(item!!.v.toDouble() / item.w) } + // Loop for greedy selection + var res = 0.0 + for (item in items) { + if (item!!.w <= cap) { + // If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v + cap -= item.w + } else { + // If remaining capacity is insufficient, put part of the current item into the knapsack + res += item.v.toDouble() / item.w * cap + // No remaining capacity, so break out of the loop + break + } + } + return res + } ``` === "Ruby" ```ruby title="fractional_knapsack.rb" - [class]{Item}-[func]{} + ### Item ### + class Item + attr_accessor :w # Item weight + attr_accessor :v # Item value - [class]{}-[func]{fractional_knapsack} + def initialize(w, v) + @w = w + @v = v + end + end + + ### Fractional knapsack: greedy ### + def fractional_knapsack(wgt, val, cap) + # Create item list with two attributes: weight, value + items = wgt.each_with_index.map { |w, i| Item.new(w, val[i]) } + # Sort by unit value item.v / item.w from high to low + items.sort! { |a, b| (b.v.to_f / b.w) <=> (a.v.to_f / a.w) } + # Loop for greedy selection + res = 0 + for item in items + if item.w <= cap + # If remaining capacity is sufficient, put the entire current item into the knapsack + res += item.v + cap -= item.w + else + # If remaining capacity is insufficient, put part of the current item into the knapsack + res += (item.v.to_f / item.w) * cap + # No remaining capacity, so break out of the loop + break + end + end + res + end ``` -=== "Zig" +The time complexity of built-in sorting algorithms is usually $O(\log n)$, and the space complexity is usually $O(\log n)$ or $O(n)$, depending on the specific implementation of the programming language. - ```zig title="fractional_knapsack.zig" - [class]{Item}-[func]{} - - [class]{}-[func]{fractionalKnapsack} - ``` - -Apart from sorting, in the worst case, the entire list of items needs to be traversed, **hence the time complexity is $O(n)$**, where $n$ is the number of items. +Apart from sorting, in the worst case the entire item list needs to be traversed, **therefore the time complexity is $O(n)$**, where $n$ is the number of items. Since an `Item` object list is initialized, **the space complexity is $O(n)$**. -### 3.   Correctness proof +### 3.   Correctness Proof -Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value `res`, but the solution does not include item $x$. +Using proof by contradiction. Suppose item $x$ has the highest unit value, and some algorithm yields a maximum value of `res`, but this solution does not include item $x$. -Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since the unit value of item $x$ is the highest, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**. +Now remove a unit weight of any item from the knapsack and replace it with a unit weight of item $x$. Since item $x$ has the highest unit value, the total value after replacement will definitely be greater than `res`. **This contradicts the assumption that `res` is the optimal solution, proving that the optimal solution must include item $x$**. -For other items in this solution, we can also construct the above contradiction. Overall, **items with greater unit value are always better choices**, proving that the greedy strategy is effective. +For other items in this solution, we can also construct the above contradiction. In summary, **items with greater unit value are always better choices**, which proves that the greedy strategy is effective. -As shown in Figure 15-6, if the item weight and unit value are viewed as the horizontal and vertical axes of a two-dimensional chart respectively, the fractional knapsack problem can be transformed into "seeking the largest area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective. +As shown in Figure 15-6, if we view item weight and item unit value as the horizontal and vertical axes of a two-dimensional chart respectively, then the fractional knapsack problem can be transformed into "finding the maximum area enclosed within a limited horizontal axis range". This analogy can help us understand the effectiveness of the greedy strategy from a geometric perspective. ![Geometric representation of the fractional knapsack problem](fractional_knapsack_problem.assets/fractional_knapsack_area_chart.png){ class="animation-figure" } diff --git a/en/docs/chapter_greedy/greedy_algorithm.md b/en/docs/chapter_greedy/greedy_algorithm.md index 9ad8343b0..b04c2d537 100644 --- a/en/docs/chapter_greedy/greedy_algorithm.md +++ b/en/docs/chapter_greedy/greedy_algorithm.md @@ -2,22 +2,22 @@ comments: true --- -# 15.1   Greedy algorithms +# 15.1   Greedy Algorithm -Greedy algorithm is a common algorithm for solving optimization problems, which fundamentally involves making the seemingly best choice at each decision-making stage of the problem, i.e., greedily making locally optimal decisions in hopes of finding a globally optimal solution. Greedy algorithms are concise and efficient, and are widely used in many practical problems. +Greedy algorithm is a common algorithm for solving optimization problems. Its basic idea is to make the seemingly best choice at each decision stage of the problem, that is, to greedily make locally optimal decisions in hopes of obtaining a globally optimal solution. Greedy algorithms are simple and efficient, and are widely applied in many practical problems. -Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as relying on the property of optimal substructure, but they operate differently. +Greedy algorithms and dynamic programming are both commonly used to solve optimization problems. They share some similarities, such as both relying on the optimal substructure property, but they work differently. -- Dynamic programming considers all previous decisions at the current decision stage and uses solutions to past subproblems to construct solutions for the current subproblem. -- Greedy algorithms do not consider past decisions; instead, they proceed with greedy choices, continually narrowing the scope of the problem until it is solved. +- Dynamic programming considers all previous decisions when making the current decision, and uses solutions to past subproblems to construct the solution to the current subproblem. +- Greedy algorithms do not consider past decisions, but instead make greedy choices moving forward, continually reducing the problem size until the problem is solved. -Let's first understand the working principle of the greedy algorithm through the example of "coin change," which has been introduced in the "Complete Knapsack Problem" chapter. I believe you are already familiar with it. +We will first understand how greedy algorithms work through the example problem "coin change". This problem has already been introduced in the "Complete Knapsack Problem" chapter, so I believe you are not unfamiliar with it. !!! question - Given $n$ types of coins, where the denomination of the $i$th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available indefinitely, what is the minimum number of coins needed to make up the target amount? If it is not possible to make up the target amount, return $-1$. + Given $n$ types of coins, where the denomination of the $i$-th type of coin is $coins[i - 1]$, and the target amount is $amt$, with each type of coin available for repeated selection, what is the minimum number of coins needed to make up the target amount? If it is impossible to make up the target amount, return $-1$. -The greedy strategy adopted in this problem is shown in Figure 15-1. Given the target amount, **we greedily choose the coin that is closest to and not greater than it**, repeatedly following this step until the target amount is met. +The greedy strategy adopted for this problem is shown in Figure 15-1. Given a target amount, **we greedily select the coin that is not greater than and closest to it**, and continuously repeat this step until the target amount is reached. ![Greedy strategy for coin change](greedy_algorithm.assets/coin_change_greedy_strategy.png){ class="animation-figure" } @@ -29,13 +29,13 @@ The implementation code is as follows: ```python title="coin_change_greedy.py" def coin_change_greedy(coins: list[int], amt: int) -> int: - """Coin change: Greedy""" - # Assume coins list is ordered + """Coin change: Greedy algorithm""" + # Assume coins list is sorted i = len(coins) - 1 count = 0 - # Loop for greedy selection until no remaining amount + # Loop to make greedy choices until no remaining amount while amt > 0: - # Find the smallest coin close to and less than the remaining amount + # Find the coin that is less than and closest to the remaining amount while i > 0 and coins[i] > amt: i -= 1 # Choose coins[i] @@ -48,14 +48,14 @@ The implementation code is as follows: === "C++" ```cpp title="coin_change_greedy.cpp" - /* Coin change: Greedy */ + /* Coin change: Greedy algorithm */ int coinChangeGreedy(vector &coins, int amt) { - // Assume coins list is ordered + // Assume coins list is sorted int i = coins.size() - 1; int count = 0; - // Loop for greedy selection until no remaining amount + // Loop to make greedy choices until no remaining amount while (amt > 0) { - // Find the smallest coin close to and less than the remaining amount + // Find the coin that is less than and closest to the remaining amount while (i > 0 && coins[i] > amt) { i--; } @@ -71,14 +71,14 @@ The implementation code is as follows: === "Java" ```java title="coin_change_greedy.java" - /* Coin change: Greedy */ + /* Coin change: Greedy algorithm */ int coinChangeGreedy(int[] coins, int amt) { - // Assume coins list is ordered + // Assume coins list is sorted int i = coins.length - 1; int count = 0; - // Loop for greedy selection until no remaining amount + // Loop to make greedy choices until no remaining amount while (amt > 0) { - // Find the smallest coin close to and less than the remaining amount + // Find the coin that is less than and closest to the remaining amount while (i > 0 && coins[i] > amt) { i--; } @@ -94,137 +94,310 @@ The implementation code is as follows: === "C#" ```csharp title="coin_change_greedy.cs" - [class]{coin_change_greedy}-[func]{CoinChangeGreedy} + /* Coin change: Greedy algorithm */ + int CoinChangeGreedy(int[] coins, int amt) { + // Assume coins list is sorted + int i = coins.Length - 1; + int count = 0; + // Loop to make greedy choices until no remaining amount + while (amt > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > amt) { + i--; + } + // Choose coins[i] + amt -= coins[i]; + count++; + } + // If no feasible solution is found, return -1 + return amt == 0 ? count : -1; + } ``` === "Go" ```go title="coin_change_greedy.go" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + func coinChangeGreedy(coins []int, amt int) int { + // Assume coins list is sorted + i := len(coins) - 1 + count := 0 + // Loop to make greedy choices until no remaining amount + for amt > 0 { + // Find the coin that is less than and closest to the remaining amount + for i > 0 && coins[i] > amt { + i-- + } + // Choose coins[i] + amt -= coins[i] + count++ + } + // If no feasible solution is found, return -1 + if amt != 0 { + return -1 + } + return count + } ``` === "Swift" ```swift title="coin_change_greedy.swift" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + func coinChangeGreedy(coins: [Int], amt: Int) -> Int { + // Assume coins list is sorted + var i = coins.count - 1 + var count = 0 + var amt = amt + // Loop to make greedy choices until no remaining amount + while amt > 0 { + // Find the coin that is less than and closest to the remaining amount + while i > 0 && coins[i] > amt { + i -= 1 + } + // Choose coins[i] + amt -= coins[i] + count += 1 + } + // If no feasible solution is found, return -1 + return amt == 0 ? count : -1 + } ``` === "JS" ```javascript title="coin_change_greedy.js" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + function coinChangeGreedy(coins, amt) { + // Assume coins array is sorted + let i = coins.length - 1; + let count = 0; + // Loop to make greedy choices until no remaining amount + while (amt > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > amt) { + i--; + } + // Choose coins[i] + amt -= coins[i]; + count++; + } + // If no feasible solution is found, return -1 + return amt === 0 ? count : -1; + } ``` === "TS" ```typescript title="coin_change_greedy.ts" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + function coinChangeGreedy(coins: number[], amt: number): number { + // Assume coins array is sorted + let i = coins.length - 1; + let count = 0; + // Loop to make greedy choices until no remaining amount + while (amt > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > amt) { + i--; + } + // Choose coins[i] + amt -= coins[i]; + count++; + } + // If no feasible solution is found, return -1 + return amt === 0 ? count : -1; + } ``` === "Dart" ```dart title="coin_change_greedy.dart" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + int coinChangeGreedy(List coins, int amt) { + // Assume coins list is sorted + int i = coins.length - 1; + int count = 0; + // Loop to make greedy choices until no remaining amount + while (amt > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > amt) { + i--; + } + // Choose coins[i] + amt -= coins[i]; + count++; + } + // If no feasible solution is found, return -1 + return amt == 0 ? count : -1; + } ``` === "Rust" ```rust title="coin_change_greedy.rs" - [class]{}-[func]{coin_change_greedy} + /* Coin change: Greedy algorithm */ + fn coin_change_greedy(coins: &[i32], mut amt: i32) -> i32 { + // Assume coins list is sorted + let mut i = coins.len() - 1; + let mut count = 0; + // Loop to make greedy choices until no remaining amount + while amt > 0 { + // Find the coin that is less than and closest to the remaining amount + while i > 0 && coins[i] > amt { + i -= 1; + } + // Choose coins[i] + amt -= coins[i]; + count += 1; + } + // If no feasible solution is found, return -1 + if amt == 0 { + count + } else { + -1 + } + } ``` === "C" ```c title="coin_change_greedy.c" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + int coinChangeGreedy(int *coins, int size, int amt) { + // Assume coins list is sorted + int i = size - 1; + int count = 0; + // Loop to make greedy choices until no remaining amount + while (amt > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > amt) { + i--; + } + // Choose coins[i] + amt -= coins[i]; + count++; + } + // If no feasible solution is found, return -1 + return amt == 0 ? count : -1; + } ``` === "Kotlin" ```kotlin title="coin_change_greedy.kt" - [class]{}-[func]{coinChangeGreedy} + /* Coin change: Greedy algorithm */ + fun coinChangeGreedy(coins: IntArray, amt: Int): Int { + // Assume coins list is sorted + var am = amt + var i = coins.size - 1 + var count = 0 + // Loop to make greedy choices until no remaining amount + while (am > 0) { + // Find the coin that is less than and closest to the remaining amount + while (i > 0 && coins[i] > am) { + i-- + } + // Choose coins[i] + am -= coins[i] + count++ + } + // If no feasible solution is found, return -1 + return if (am == 0) count else -1 + } ``` === "Ruby" ```ruby title="coin_change_greedy.rb" - [class]{}-[func]{coin_change_greedy} - ``` - -=== "Zig" - - ```zig title="coin_change_greedy.zig" - [class]{}-[func]{coinChangeGreedy} + ### Coin change: greedy ### + def coin_change_greedy(coins, amt) + # Assume coins list is sorted + i = coins.length - 1 + count = 0 + # Loop to make greedy choices until no remaining amount + while amt > 0 + # Find the coin that is less than and closest to the remaining amount + while i > 0 && coins[i] > amt + i -= 1 + end + # Choose coins[i] + amt -= coins[i] + count += 1 + end + # Return -1 if no solution found + amt == 0 ? count : -1 + end ``` You might exclaim: So clean! The greedy algorithm solves the coin change problem in about ten lines of code. -## 15.1.1   Advantages and limitations of greedy algorithms +## 15.1.1   Advantages and Limitations of Greedy Algorithms -**Greedy algorithms are not only straightforward and simple to implement, but they are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution, which is $O(n \times amt)$. +**Greedy algorithms are not only straightforward and simple to implement, but are also usually very efficient**. In the code above, if the smallest coin denomination is $\min(coins)$, the greedy choice loops at most $amt / \min(coins)$ times, giving a time complexity of $O(amt / \min(coins))$. This is an order of magnitude smaller than the time complexity of the dynamic programming solution $O(n \times amt)$. -However, **for some combinations of coin denominations, greedy algorithms cannot find the optimal solution**. Figure 15-2 provides two examples. +However, **for certain coin denomination combinations, greedy algorithms cannot find the optimal solution**. Figure 15-2 provides two examples. -- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: In this coin combination, given any $amt$, the greedy algorithm can find the optimal solution. -- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling 11 coins, but dynamic programming can find the optimal solution of $20 + 20 + 20$, needing only 3 coins. -- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling 49 coins, but dynamic programming can find the optimal solution of $49 + 49$, needing only 2 coins. +- **Positive example $coins = [1, 5, 10, 20, 50, 100]$**: With this coin combination, given any $amt$, the greedy algorithm can find the optimal solution. +- **Negative example $coins = [1, 20, 50]$**: Suppose $amt = 60$, the greedy algorithm can only find the combination $50 + 1 \times 10$, totaling $11$ coins, but dynamic programming can find the optimal solution $20 + 20 + 20$, requiring only $3$ coins. +- **Negative example $coins = [1, 49, 50]$**: Suppose $amt = 98$, the greedy algorithm can only find the combination $50 + 1 \times 48$, totaling $49$ coins, but dynamic programming can find the optimal solution $49 + 49$, requiring only $2$ coins. -![Examples where greedy algorithms do not find the optimal solution](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" } +![Examples where greedy algorithms cannot find the optimal solution](greedy_algorithm.assets/coin_change_greedy_vs_dp.png){ class="animation-figure" } -

Figure 15-2   Examples where greedy algorithms do not find the optimal solution

+

Figure 15-2   Examples where greedy algorithms cannot find the optimal solution

-This means that for the coin change problem, greedy algorithms cannot guarantee finding the globally optimal solution, and they might find a very poor solution. They are better suited for dynamic programming. +In other words, for the coin change problem, greedy algorithms cannot guarantee finding the global optimal solution, and may even find very poor solutions. It is better suited for solving with dynamic programming. -Generally, the suitability of greedy algorithms falls into two categories. +Generally, the applicability of greedy algorithms falls into the following two situations. -1. **Guaranteed to find the optimal solution**: In these cases, greedy algorithms are often the best choice, as they tend to be more efficient than backtracking or dynamic programming. -2. **Can find a near-optimal solution**: Greedy algorithms are also applicable here. For many complex problems, finding the global optimal solution is very challenging, and being able to find a high-efficiency suboptimal solution is also very commendable. +1. **Can guarantee finding the optimal solution**: In this situation, greedy algorithms are often the best choice, because they tend to be more efficient than backtracking and dynamic programming. +2. **Can find an approximate optimal solution**: Greedy algorithms are also applicable in this situation. For many complex problems, finding the global optimal solution is very difficult, and being able to find a suboptimal solution with high efficiency is also very good. -## 15.1.2   Characteristics of greedy algorithms +## 15.1.2   Characteristics of Greedy Algorithms -So, what kind of problems are suitable for solving with greedy algorithms? Or rather, under what conditions can greedy algorithms guarantee to find the optimal solution? +So the question arises: what kind of problems are suitable for solving with greedy algorithms? Or in other words, under what conditions can greedy algorithms guarantee finding the optimal solution? -Compared to dynamic programming, greedy algorithms have stricter usage conditions, focusing mainly on two properties of the problem. +Compared to dynamic programming, the conditions for using greedy algorithms are stricter, mainly focusing on two properties of the problem. -- **Greedy choice property**: Only when the locally optimal choice can always lead to a globally optimal solution can greedy algorithms guarantee to obtain the optimal solution. -- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to its subproblems. +- **Greedy choice property**: Only when locally optimal choices can always lead to a globally optimal solution can greedy algorithms guarantee obtaining the optimal solution. +- **Optimal substructure**: The optimal solution to the original problem contains the optimal solutions to subproblems. -Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so it is not discussed further here. It's important to note that some problems do not have an obvious optimal substructure, but can still be solved using greedy algorithms. +Optimal substructure has already been introduced in the "Dynamic Programming" chapter, so we won't elaborate on it here. It's worth noting that the optimal substructure of some problems is not obvious, but they can still be solved using greedy algorithms. -We mainly explore the method for determining the greedy choice property. Although its description seems simple, **in practice, proving the greedy choice property for many problems is not easy**. +We mainly explore methods for determining the greedy choice property. Although its description seems relatively simple, **in practice, for many problems, proving the greedy choice property is not easy**. -For example, in the coin change problem, although we can easily cite counterexamples to disprove the greedy choice property, proving it is much more challenging. If asked, **what conditions must a coin combination meet to be solvable using a greedy algorithm**? We often have to rely on intuition or examples to provide an ambiguous answer, as it is difficult to provide a rigorous mathematical proof. +For example, in the coin change problem, although we can easily provide counterexamples to disprove the greedy choice property, proving it is quite difficult. If asked: **what conditions must a coin combination satisfy to be solvable using a greedy algorithm**? We often can only rely on intuition or examples to give an ambiguous answer, and find it difficult to provide a rigorous mathematical proof. !!! quote - A paper presents an algorithm with a time complexity of $O(n^3)$ for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount. + There is a paper that presents an algorithm with $O(n^3)$ time complexity for determining whether a coin combination can use a greedy algorithm to find the optimal solution for any amount. Pearson, D. A polynomial-time algorithm for the change-making problem[J]. Operations Research Letters, 2005, 33(3): 231-234. -## 15.1.3   Steps for solving problems with greedy algorithms +## 15.1.3   Steps for Solving Problems with Greedy Algorithms The problem-solving process for greedy problems can generally be divided into the following three steps. -1. **Problem analysis**: Sort out and understand the characteristics of the problem, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming. -2. **Determine the greedy strategy**: Determine how to make a greedy choice at each step. This strategy can reduce the scale of the problem at each step and eventually solve the entire problem. -3. **Proof of correctness**: It is usually necessary to prove that the problem has both a greedy choice property and optimal substructure. This step may require mathematical proofs, such as induction or reductio ad absurdum. +1. **Problem analysis**: Sort out and understand the problem characteristics, including state definition, optimization objectives, and constraints, etc. This step is also involved in backtracking and dynamic programming. +2. **Determine the greedy strategy**: Determine how to make greedy choices at each step. This strategy should be able to reduce the problem size at each step, ultimately solving the entire problem. +3. **Correctness proof**: It is usually necessary to prove that the problem has both greedy choice property and optimal substructure. This step may require mathematical proofs, such as mathematical induction or proof by contradiction. Determining the greedy strategy is the core step in solving the problem, but it may not be easy to implement, mainly for the following reasons. -- **Greedy strategies vary greatly between different problems**. For many problems, the greedy strategy is fairly straightforward, and we can come up with it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which is a real test of individual problem-solving experience and algorithmic capability. -- **Some greedy strategies are quite misleading**. When we confidently design a greedy strategy, write the code, and submit it for testing, it is quite possible that some test cases will not pass. This is because the designed greedy strategy is only "partially correct," as described above with the coin change example. +- **Greedy strategies differ greatly between different problems**. For many problems, the greedy strategy is relatively straightforward, and we can derive it through some general thinking and attempts. However, for some complex problems, the greedy strategy may be very elusive, which really tests one's problem-solving experience and algorithmic ability. +- **Some greedy strategies are highly misleading**. When we confidently design a greedy strategy, write the solution code and submit it for testing, we may find that some test cases cannot pass. This is because the designed greedy strategy is only "partially correct", as exemplified by the coin change problem discussed above. -To ensure accuracy, we should provide rigorous mathematical proofs for the greedy strategy, **usually involving reductio ad absurdum or mathematical induction**. +To ensure correctness, we should rigorously mathematically prove the greedy strategy, **usually using proof by contradiction or mathematical induction**. -However, proving correctness may not be an easy task. If we are at a loss, we usually choose to debug the code based on test cases, modifying and verifying the greedy strategy step by step. +However, correctness proofs may also not be easy. If we have no clue, we usually choose to debug the code based on test cases, step by step modifying and verifying the greedy strategy. -## 15.1.4   Typical problems solved by greedy algorithms +## 15.1.4   Typical Problems Solved by Greedy Algorithms -Greedy algorithms are often applied to optimization problems that satisfy the properties of greedy choice and optimal substructure. Below are some typical greedy algorithm problems. +Greedy algorithms are often applied to optimization problems that satisfy greedy choice property and optimal substructure. Below are some typical greedy algorithm problems. -- **Coin change problem**: In some coin combinations, the greedy algorithm always provides the optimal solution. -- **Interval scheduling problem**: Suppose you have several tasks, each of which takes place over a period of time. Your goal is to complete as many tasks as possible. If you always choose the task that ends the earliest, then the greedy algorithm can achieve the optimal solution. -- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), the greedy algorithm can achieve the optimal solution in some cases. -- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but you cannot buy again until after you have sold if you already own stocks. The goal is to achieve the maximum profit. -- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree, it always merges the two nodes with the lowest frequency, resulting in a Huffman tree with the minimum weighted path length (coding length). +- **Coin change problem**: With certain coin combinations, greedy algorithms can always obtain the optimal solution. +- **Interval scheduling problem**: Suppose you have some tasks, each taking place during a period of time, and your goal is to complete as many tasks as possible. If you always choose the task that ends earliest, then the greedy algorithm can obtain the optimal solution. +- **Fractional knapsack problem**: Given a set of items and a carrying capacity, your goal is to select a set of items such that the total weight does not exceed the carrying capacity and the total value is maximized. If you always choose the item with the highest value-to-weight ratio (value / weight), then the greedy algorithm can obtain the optimal solution in some cases. +- **Stock trading problem**: Given a set of historical stock prices, you can make multiple trades, but if you already hold stocks, you cannot buy again before selling, and the goal is to obtain the maximum profit. +- **Huffman coding**: Huffman coding is a greedy algorithm used for lossless data compression. By constructing a Huffman tree and always merging the two nodes with the lowest frequency, the resulting Huffman tree has the minimum weighted path length (encoding length). - **Dijkstra's algorithm**: It is a greedy algorithm for solving the shortest path problem from a given source vertex to all other vertices. diff --git a/en/docs/chapter_greedy/index.md b/en/docs/chapter_greedy/index.md index 546953f15..44fdd7f33 100644 --- a/en/docs/chapter_greedy/index.md +++ b/en/docs/chapter_greedy/index.md @@ -9,14 +9,14 @@ icon: material/head-heart-outline !!! abstract - Sunflowers turn towards the sun, always seeking the greatest possible growth for themselves. + Sunflowers turn toward the sun, constantly pursuing the maximum potential for their own growth. - Greedy strategy guides to the best answer step by step through rounds of simple choices. + Through rounds of simple choices, greedy strategies gradually lead to the best answer. ## Chapter contents -- [15.1   Greedy algorithms](greedy_algorithm.md) -- [15.2   Fractional knapsack problem](fractional_knapsack_problem.md) -- [15.3   Maximum capacity problem](max_capacity_problem.md) -- [15.4   Maximum product cutting problem](max_product_cutting_problem.md) +- [15.1   Greedy Algorithm](greedy_algorithm.md) +- [15.2   Fractional Knapsack Problem](fractional_knapsack_problem.md) +- [15.3   Maximum Capacity Problem](max_capacity_problem.md) +- [15.4   Maximum Product Cutting Problem](max_product_cutting_problem.md) - [15.5   Summary](summary.md) diff --git a/en/docs/chapter_greedy/max_capacity_problem.md b/en/docs/chapter_greedy/max_capacity_problem.md index 03e8dd910..66c4ce9dd 100644 --- a/en/docs/chapter_greedy/max_capacity_problem.md +++ b/en/docs/chapter_greedy/max_capacity_problem.md @@ -2,63 +2,63 @@ comments: true --- -# 15.3   Maximum capacity problem +# 15.3   Max Capacity Problem !!! question Input an array $ht$, where each element represents the height of a vertical partition. Any two partitions in the array, along with the space between them, can form a container. - - The capacity of the container is the product of the height and the width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. - - Please select two partitions in the array that maximize the container's capacity and return this maximum capacity. An example is shown in Figure 15-7. -![Example data for the maximum capacity problem](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" } + The capacity of the container equals the product of height and width (area), where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. -

Figure 15-7   Example data for the maximum capacity problem

+ Please select two partitions in the array such that the capacity of the formed container is maximized, and return the maximum capacity. An example is shown in Figure 15-7. -The container is formed by any two partitions, **therefore the state of this problem is represented by the indices of the two partitions, denoted as $[i, j]$**. +![Example data for the max capacity problem](max_capacity_problem.assets/max_capacity_example.png){ class="animation-figure" } -According to the problem statement, the capacity equals the product of height and width, where the height is determined by the shorter partition, and the width is the difference in array indices between the two partitions. The formula for capacity $cap[i, j]$ is: +

Figure 15-7   Example data for the max capacity problem

+ +The container is formed by any two partitions, **therefore the state of this problem is the indices of two partitions, denoted as $[i, j]$**. + +According to the problem description, capacity equals height multiplied by width, where height is determined by the shorter partition, and width is the difference in array indices between the two partitions. Let the capacity be $cap[i, j]$, then the calculation formula is: $$ cap[i, j] = \min(ht[i], ht[j]) \times (j - i) $$ -Assuming the length of the array is $n$, the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. The most straightforward approach is to **enumerate all possible states**, resulting in a time complexity of $O(n^2)$. +Let the array length be $n$, then the number of combinations of two partitions (total number of states) is $C_n^2 = \frac{n(n - 1)}{2}$. Most directly, **we can exhaustively enumerate all states** to find the maximum capacity, with time complexity $O(n^2)$. -### 1.   Determination of a greedy strategy +### 1.   Greedy Strategy Determination -There is a more efficient solution to this problem. As shown in Figure 15-8, we select a state $[i, j]$ where the indices $i < j$ and the height $ht[i] < ht[j]$, meaning $i$ is the shorter partition, and $j$ is the taller one. +This problem has a more efficient solution. As shown in Figure 15-8, select a state $[i, j]$ where index $i < j$ and height $ht[i] < ht[j]$, meaning $i$ is the short partition and $j$ is the long partition. ![Initial state](max_capacity_problem.assets/max_capacity_initial_state.png){ class="animation-figure" }

Figure 15-8   Initial state

-As shown in Figure 15-9, **if we move the taller partition $j$ closer to the shorter partition $i$, the capacity will definitely decrease**. +As shown in Figure 15-9, **if we now move the long partition $j$ closer to the short partition $i$, the capacity will definitely decrease**. -This is because when moving the taller partition $j$, the width $j-i$ definitely decreases; and since the height is determined by the shorter partition, the height can only remain the same (if $i$ remains the shorter partition) or decrease (if the moved $j$ becomes the shorter partition). +This is because after moving the long partition $j$, the width $j-i$ definitely decreases; and since height is determined by the short partition, the height can only remain unchanged ($i$ is still the short partition) or decrease (the moved $j$ becomes the short partition). -![State after moving the taller partition inward](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" } +![State after moving the long partition inward](max_capacity_problem.assets/max_capacity_moving_long_board.png){ class="animation-figure" } -

Figure 15-9   State after moving the taller partition inward

+

Figure 15-9   State after moving the long partition inward

-Conversely, **we can only possibly increase the capacity by moving the shorter partition $i$ inward**. Although the width will definitely decrease, **the height may increase** (if the moved shorter partition $i$ becomes taller). For example, in Figure 15-10, the area increases after moving the shorter partition. +Conversely, **we can only possibly increase capacity by contracting the short partition $i$ inward**. Because although width will definitely decrease, **height may increase** (the moved short partition $i$ may become taller). For example, in Figure 15-10, the area increases after moving the short partition. -![State after moving the shorter partition inward](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" } +![State after moving the short partition inward](max_capacity_problem.assets/max_capacity_moving_short_board.png){ class="animation-figure" } -

Figure 15-10   State after moving the shorter partition inward

+

Figure 15-10   State after moving the short partition inward

-This leads us to the greedy strategy for this problem: initialize two pointers at the ends of the container, and in each round, move the pointer corresponding to the shorter partition inward until the two pointers meet. +From this we can derive the greedy strategy for this problem: initialize two pointers at both ends of the container, and in each round contract the pointer corresponding to the short partition inward, until the two pointers meet. -Figure 15-11 illustrate the execution of the greedy strategy. +Figure 15-11 shows the execution process of the greedy strategy. -1. Initially, the pointers $i$ and $j$ are positioned at the ends of the array. -2. Calculate the current state's capacity $cap[i, j]$ and update the maximum capacity. -3. Compare the heights of partitions $i$ and $j$, and move the shorter partition inward by one step. -4. Repeat steps `2.` and `3.` until $i$ and $j$ meet. +1. In the initial state, pointers $i$ and $j$ are at both ends of the array. +2. Calculate the capacity of the current state $cap[i, j]$, and update the maximum capacity. +3. Compare the heights of partition $i$ and partition $j$, and move the short partition inward by one position. +4. Loop through steps `2.` and `3.` until $i$ and $j$ meet. === "<1>" - ![The greedy process for maximum capacity problem](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } + ![Greedy process for the max capacity problem](max_capacity_problem.assets/max_capacity_greedy_step1.png){ class="animation-figure" } === "<2>" ![max_capacity_greedy_step2](max_capacity_problem.assets/max_capacity_greedy_step2.png){ class="animation-figure" } @@ -84,26 +84,26 @@ Figure 15-11 illustrate the execution of the greedy strategy. === "<9>" ![max_capacity_greedy_step9](max_capacity_problem.assets/max_capacity_greedy_step9.png){ class="animation-figure" } -

Figure 15-11   The greedy process for maximum capacity problem

+

Figure 15-11   Greedy process for the max capacity problem

-### 2.   Implementation +### 2.   Code Implementation -The code loops at most $n$ times, **thus the time complexity is $O(n)$**. +The code loops at most $n$ rounds, **therefore the time complexity is $O(n)$**. -The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus the space complexity is $O(1)$**. +Variables $i$, $j$, and $res$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**. === "Python" ```python title="max_capacity.py" def max_capacity(ht: list[int]) -> int: - """Maximum capacity: Greedy""" - # Initialize i, j, making them split the array at both ends + """Max capacity: Greedy algorithm""" + # Initialize i, j to be at both ends of the array i, j = 0, len(ht) - 1 - # Initial maximum capacity is 0 + # Initial max capacity is 0 res = 0 # Loop for greedy selection until the two boards meet while i < j: - # Update maximum capacity + # Update max capacity cap = min(ht[i], ht[j]) * (j - i) res = max(res, cap) # Move the shorter board inward @@ -117,15 +117,15 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t === "C++" ```cpp title="max_capacity.cpp" - /* Maximum capacity: Greedy */ + /* Max capacity: Greedy algorithm */ int maxCapacity(vector &ht) { - // Initialize i, j, making them split the array at both ends + // Initialize i, j to be at both ends of the array int i = 0, j = ht.size() - 1; - // Initial maximum capacity is 0 + // Initial max capacity is 0 int res = 0; // Loop for greedy selection until the two boards meet while (i < j) { - // Update maximum capacity + // Update max capacity int cap = min(ht[i], ht[j]) * (j - i); res = max(res, cap); // Move the shorter board inward @@ -142,15 +142,15 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t === "Java" ```java title="max_capacity.java" - /* Maximum capacity: Greedy */ + /* Max capacity: Greedy algorithm */ int maxCapacity(int[] ht) { - // Initialize i, j, making them split the array at both ends + // Initialize i, j to be at both ends of the array int i = 0, j = ht.length - 1; - // Initial maximum capacity is 0 + // Initial max capacity is 0 int res = 0; // Loop for greedy selection until the two boards meet while (i < j) { - // Update maximum capacity + // Update max capacity int cap = Math.min(ht[i], ht[j]) * (j - i); res = Math.max(res, cap); // Move the shorter board inward @@ -167,83 +167,274 @@ The variables $i$, $j$, and $res$ use a constant amount of extra space, **thus t === "C#" ```csharp title="max_capacity.cs" - [class]{max_capacity}-[func]{MaxCapacity} + /* Max capacity: Greedy algorithm */ + int MaxCapacity(int[] ht) { + // Initialize i, j to be at both ends of the array + int i = 0, j = ht.Length - 1; + // Initial max capacity is 0 + int res = 0; + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + int cap = Math.Min(ht[i], ht[j]) * (j - i); + res = Math.Max(res, cap); + // Move the shorter board inward + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Go" ```go title="max_capacity.go" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + func maxCapacity(ht []int) int { + // Initialize i, j to be at both ends of the array + i, j := 0, len(ht)-1 + // Initial max capacity is 0 + res := 0 + // Loop for greedy selection until the two boards meet + for i < j { + // Update max capacity + capacity := int(math.Min(float64(ht[i]), float64(ht[j]))) * (j - i) + res = int(math.Max(float64(res), float64(capacity))) + // Move the shorter board inward + if ht[i] < ht[j] { + i++ + } else { + j-- + } + } + return res + } ``` === "Swift" ```swift title="max_capacity.swift" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + func maxCapacity(ht: [Int]) -> Int { + // Initialize i, j to be at both ends of the array + var i = ht.startIndex, j = ht.endIndex - 1 + // Initial max capacity is 0 + var res = 0 + // Loop for greedy selection until the two boards meet + while i < j { + // Update max capacity + let cap = min(ht[i], ht[j]) * (j - i) + res = max(res, cap) + // Move the shorter board inward + if ht[i] < ht[j] { + i += 1 + } else { + j -= 1 + } + } + return res + } ``` === "JS" ```javascript title="max_capacity.js" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + function maxCapacity(ht) { + // Initialize i, j to be at both ends of the array + let i = 0, + j = ht.length - 1; + // Initial max capacity is 0 + let res = 0; + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + const cap = Math.min(ht[i], ht[j]) * (j - i); + res = Math.max(res, cap); + // Move the shorter board inward + if (ht[i] < ht[j]) { + i += 1; + } else { + j -= 1; + } + } + return res; + } ``` === "TS" ```typescript title="max_capacity.ts" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + function maxCapacity(ht: number[]): number { + // Initialize i, j to be at both ends of the array + let i = 0, + j = ht.length - 1; + // Initial max capacity is 0 + let res = 0; + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + const cap: number = Math.min(ht[i], ht[j]) * (j - i); + res = Math.max(res, cap); + // Move the shorter board inward + if (ht[i] < ht[j]) { + i += 1; + } else { + j -= 1; + } + } + return res; + } ``` === "Dart" ```dart title="max_capacity.dart" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + int maxCapacity(List ht) { + // Initialize i, j to be at both ends of the array + int i = 0, j = ht.length - 1; + // Initial max capacity is 0 + int res = 0; + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + int cap = min(ht[i], ht[j]) * (j - i); + res = max(res, cap); + // Move the shorter board inward + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Rust" ```rust title="max_capacity.rs" - [class]{}-[func]{max_capacity} + /* Max capacity: Greedy algorithm */ + fn max_capacity(ht: &[i32]) -> i32 { + // Initialize i, j to be at both ends of the array + let mut i = 0; + let mut j = ht.len() - 1; + // Initial max capacity is 0 + let mut res = 0; + // Loop for greedy selection until the two boards meet + while i < j { + // Update max capacity + let cap = std::cmp::min(ht[i], ht[j]) * (j - i) as i32; + res = std::cmp::max(res, cap); + // Move the shorter board inward + if ht[i] < ht[j] { + i += 1; + } else { + j -= 1; + } + } + res + } ``` === "C" ```c title="max_capacity.c" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + int maxCapacity(int ht[], int htLength) { + // Initialize i, j to be at both ends of the array + int i = 0; + int j = htLength - 1; + // Initial max capacity is 0 + int res = 0; + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + int capacity = myMin(ht[i], ht[j]) * (j - i); + res = myMax(res, capacity); + // Move the shorter board inward + if (ht[i] < ht[j]) { + i++; + } else { + j--; + } + } + return res; + } ``` === "Kotlin" ```kotlin title="max_capacity.kt" - [class]{}-[func]{maxCapacity} + /* Max capacity: Greedy algorithm */ + fun maxCapacity(ht: IntArray): Int { + // Initialize i, j to be at both ends of the array + var i = 0 + var j = ht.size - 1 + // Initial max capacity is 0 + var res = 0 + // Loop for greedy selection until the two boards meet + while (i < j) { + // Update max capacity + val cap = min(ht[i], ht[j]) * (j - i) + res = max(res, cap) + // Move the shorter board inward + if (ht[i] < ht[j]) { + i++ + } else { + j-- + } + } + return res + } ``` === "Ruby" ```ruby title="max_capacity.rb" - [class]{}-[func]{max_capacity} + ### Maximum capacity: greedy ### + def max_capacity(ht) + # Initialize i, j to be at both ends of the array + i, j = 0, ht.length - 1 + # Initial max capacity is 0 + res = 0 + + # Loop for greedy selection until the two boards meet + while i < j + # Update max capacity + cap = [ht[i], ht[j]].min * (j - i) + res = [res, cap].max + # Move the shorter board inward + if ht[i] < ht[j] + i += 1 + else + j -= 1 + end + end + + res + end ``` -=== "Zig" +### 3.   Correctness Proof - ```zig title="max_capacity.zig" - [class]{}-[func]{maxCapacity} - ``` +The reason greedy is faster than exhaustive enumeration is that each round of greedy selection "skips" some states. -### 3.   Proof of correctness - -The reason why the greedy method is faster than enumeration is that each round of greedy selection "skips" some states. - -For example, under the state $cap[i, j]$ where $i$ is the shorter partition and $j$ is the taller partition, greedily moving the shorter partition $i$ inward by one step leads to the "skipped" states shown in Figure 15-12. **This means that these states' capacities cannot be verified later**. +For example, in state $cap[i, j]$ where $i$ is the short partition and $j$ is the long partition, if we greedily move the short partition $i$ inward by one position, the states shown in Figure 15-12 will be "skipped". **This means that the capacities of these states cannot be verified later**. $$ cap[i, i+1], cap[i, i+2], \dots, cap[i, j-2], cap[i, j-1] $$ -![States skipped by moving the shorter partition](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" } +![States skipped by moving the short partition](max_capacity_problem.assets/max_capacity_skipped_states.png){ class="animation-figure" } -

Figure 15-12   States skipped by moving the shorter partition

+

Figure 15-12   States skipped by moving the short partition

-It is observed that **these skipped states are actually all states where the taller partition $j$ is moved inward**. We have already proven that moving the taller partition inward will definitely decrease the capacity. Therefore, the skipped states cannot possibly be the optimal solution, **and skipping them does not lead to missing the optimal solution**. +Observing carefully, **these skipped states are actually all the states obtained by moving the long partition $j$ inward**. We have already proven that moving the long partition inward will definitely decrease capacity. That is, the skipped states cannot possibly be the optimal solution, **skipping them will not cause us to miss the optimal solution**. -The analysis shows that the operation of moving the shorter partition is "safe", and the greedy strategy is effective. +The above analysis shows that the operation of moving the short partition is "safe", and the greedy strategy is effective. diff --git a/en/docs/chapter_greedy/max_product_cutting_problem.md b/en/docs/chapter_greedy/max_product_cutting_problem.md index 333448f68..404c41e8e 100644 --- a/en/docs/chapter_greedy/max_product_cutting_problem.md +++ b/en/docs/chapter_greedy/max_product_cutting_problem.md @@ -2,33 +2,33 @@ comments: true --- -# 15.4   Maximum product cutting problem +# 15.4   Max Product Cutting Problem !!! question - Given a positive integer $n$, split it into at least two positive integers that sum up to $n$, and find the maximum product of these integers, as illustrated in Figure 15-13. + Given a positive integer $n$, split it into the sum of at least two positive integers, and find the maximum product of all integers after splitting, as shown in Figure 15-13. -![Definition of the maximum product cutting problem](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } +![Problem definition of max product cutting](max_product_cutting_problem.assets/max_product_cutting_definition.png){ class="animation-figure" } -

Figure 15-13   Definition of the maximum product cutting problem

+

Figure 15-13   Problem definition of max product cutting

-Assume we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is, +Suppose we split $n$ into $m$ integer factors, where the $i$-th factor is denoted as $n_i$, that is $$ n = \sum_{i=1}^{m}n_i $$ -The goal of this problem is to find the maximum product of all integer factors, namely, +The goal of this problem is to find the maximum product of all integer factors, namely $$ \max(\prod_{i=1}^{m}n_i) $$ -We need to consider: How large should the number of splits $m$ be, and what should each $n_i$ be? +We need to think about: how large should the splitting count $m$ be, and what should each $n_i$ be? -### 1.   Greedy strategy determination +### 1.   Greedy Strategy Determination -Experience suggests that the product of two integers is often greater than their sum. Suppose we split a factor of $2$ from $n$, then their product is $2(n-2)$. Compare this product with $n$: +Based on experience, the product of two integers is often greater than their sum. Suppose we split out a factor of $2$ from $n$, then their product is $2(n-2)$. We compare this product with $n$: $$ \begin{aligned} @@ -38,53 +38,53 @@ n & \geq 4 \end{aligned} $$ -As shown in Figure 15-14, when $n \geq 4$, splitting out a $2$ increases the product, **which indicates that integers greater than or equal to $4$ should be split**. +As shown in Figure 15-14, when $n \geq 4$, splitting out a $2$ will increase the product, **which indicates that integers greater than or equal to $4$ should all be split**. -**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, they should be further split. The final split should only include factors $1$, $2$, and $3$. +**Greedy strategy one**: If the splitting scheme includes factors $\geq 4$, then they should continue to be split. The final splitting scheme should only contain factors $1$, $2$, and $3$. -![Product increase due to splitting](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } +![Splitting causes product to increase](max_product_cutting_problem.assets/max_product_cutting_greedy_infer1.png){ class="animation-figure" } -

Figure 15-14   Product increase due to splitting

+

Figure 15-14   Splitting causes product to increase

-Next, consider which factor is optimal. Among the factors $1$, $2$, and $3$, clearly $1$ is the worst, as $1 \times (n-1) < n$ always holds, meaning splitting out $1$ actually decreases the product. +Next, consider which factor is optimal. Among the three factors $1$, $2$, and $3$, clearly $1$ is the worst, because $1 \times (n-1) < n$ always holds, meaning splitting out $1$ will actually decrease the product. -As shown in Figure 15-15, when $n = 6$, $3 \times 3 > 2 \times 2 \times 2$. **This means splitting out $3$ is better than splitting out $2$**. +As shown in Figure 15-15, when $n = 6$, we have $3 \times 3 > 2 \times 2 \times 2$. **This means that splitting out $3$ is better than splitting out $2$**. -**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a higher product. +**Greedy strategy two**: In the splitting scheme, there should be at most two $2$s. Because three $2$s can always be replaced by two $3$s to obtain a larger product. -![Optimal splitting factors](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" } +![Optimal splitting factor](max_product_cutting_problem.assets/max_product_cutting_greedy_infer2.png){ class="animation-figure" } -

Figure 15-15   Optimal splitting factors

+

Figure 15-15   Optimal splitting factor

-From the above, the following greedy strategies can be derived. +In summary, the following greedy strategies can be derived. -1. Input integer $n$, continually split out factor $3$ until the remainder is $0$, $1$, or $2$. -2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is taken. -3. When the remainder is $2$, do not continue to split, keep it. +1. Input integer $n$, continuously split out factor $3$ until the remainder is $0$, $1$, or $2$. +2. When the remainder is $0$, it means $n$ is a multiple of $3$, so no further action is needed. +3. When the remainder is $2$, do not continue splitting, keep it. 4. When the remainder is $1$, since $2 \times 2 > 1 \times 3$, the last $3$ should be replaced with $2$. -### 2.   Code implementation +### 2.   Code Implementation -As shown in Figure 15-16, we do not need to use loops to split the integer but can use the floor division operation to get the number of $3$s, $a$, and the modulo operation to get the remainder, $b$, thus: +As shown in Figure 15-16, we don't need to use loops to split the integer, but can use integer division to get the count of $3$s as $a$, and modulo operation to get the remainder as $b$, at which point we have: $$ -n = 3a + b +n = 3 a + b $$ -Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, with a product of $1 \times (n - 1)$. +Please note that for the edge case of $n \leq 3$, a $1$ must be split out, with product $1 \times (n - 1)$. === "Python" ```python title="max_product_cutting.py" def max_product_cutting(n: int) -> int: - """Maximum product of cutting: Greedy""" + """Max product cutting: Greedy algorithm""" # When n <= 3, must cut out a 1 if n <= 3: return 1 * (n - 1) - # Greedy cut out 3s, a is the number of 3s, b is the remainder + # Greedily cut out 3, a is the number of 3s, b is the remainder a, b = n // 3, n % 3 if b == 1: - # When the remainder is 1, convert a pair of 1 * 3 into 2 * 2 + # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 return int(math.pow(3, a - 1)) * 2 * 2 if b == 2: # When the remainder is 2, do nothing @@ -96,17 +96,17 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi === "C++" ```cpp title="max_product_cutting.cpp" - /* Maximum product of cutting: Greedy */ + /* Max product cutting: Greedy algorithm */ int maxProductCutting(int n) { // When n <= 3, must cut out a 1 if (n <= 3) { return 1 * (n - 1); } - // Greedy cut out 3s, a is the number of 3s, b is the remainder + // Greedily cut out 3, a is the number of 3s, b is the remainder int a = n / 3; int b = n % 3; if (b == 1) { - // When the remainder is 1, convert a pair of 1 * 3 into 2 * 2 + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 return (int)pow(3, a - 1) * 2 * 2; } if (b == 2) { @@ -121,17 +121,17 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi === "Java" ```java title="max_product_cutting.java" - /* Maximum product of cutting: Greedy */ + /* Max product cutting: Greedy algorithm */ int maxProductCutting(int n) { // When n <= 3, must cut out a 1 if (n <= 3) { return 1 * (n - 1); } - // Greedy cut out 3s, a is the number of 3s, b is the remainder + // Greedily cut out 3, a is the number of 3s, b is the remainder int a = n / 3; int b = n % 3; if (b == 1) { - // When the remainder is 1, convert a pair of 1 * 3 into 2 * 2 + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 return (int) Math.pow(3, a - 1) * 2 * 2; } if (b == 2) { @@ -146,84 +146,261 @@ Please note, for the boundary case where $n \leq 3$, a $1$ must be split out, wi === "C#" ```csharp title="max_product_cutting.cs" - [class]{max_product_cutting}-[func]{MaxProductCutting} + /* Max product cutting: Greedy algorithm */ + int MaxProductCutting(int n) { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + int a = n / 3; + int b = n % 3; + if (b == 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return (int)Math.Pow(3, a - 1) * 2 * 2; + } + if (b == 2) { + // When the remainder is 2, do nothing + return (int)Math.Pow(3, a) * 2; + } + // When the remainder is 0, do nothing + return (int)Math.Pow(3, a); + } ``` === "Go" ```go title="max_product_cutting.go" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + func maxProductCutting(n int) int { + // When n <= 3, must cut out a 1 + if n <= 3 { + return 1 * (n - 1) + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + a := n / 3 + b := n % 3 + if b == 1 { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return int(math.Pow(3, float64(a-1))) * 2 * 2 + } + if b == 2 { + // When the remainder is 2, do nothing + return int(math.Pow(3, float64(a))) * 2 + } + // When the remainder is 0, do nothing + return int(math.Pow(3, float64(a))) + } ``` === "Swift" ```swift title="max_product_cutting.swift" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + func maxProductCutting(n: Int) -> Int { + // When n <= 3, must cut out a 1 + if n <= 3 { + return 1 * (n - 1) + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + let a = n / 3 + let b = n % 3 + if b == 1 { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return pow(3, a - 1) * 2 * 2 + } + if b == 2 { + // When the remainder is 2, do nothing + return pow(3, a) * 2 + } + // When the remainder is 0, do nothing + return pow(3, a) + } ``` === "JS" ```javascript title="max_product_cutting.js" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + function maxProductCutting(n) { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + let a = Math.floor(n / 3); + let b = n % 3; + if (b === 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return Math.pow(3, a - 1) * 2 * 2; + } + if (b === 2) { + // When the remainder is 2, do nothing + return Math.pow(3, a) * 2; + } + // When the remainder is 0, do nothing + return Math.pow(3, a); + } ``` === "TS" ```typescript title="max_product_cutting.ts" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + function maxProductCutting(n: number): number { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + let a: number = Math.floor(n / 3); + let b: number = n % 3; + if (b === 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return Math.pow(3, a - 1) * 2 * 2; + } + if (b === 2) { + // When the remainder is 2, do nothing + return Math.pow(3, a) * 2; + } + // When the remainder is 0, do nothing + return Math.pow(3, a); + } ``` === "Dart" ```dart title="max_product_cutting.dart" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + int maxProductCutting(int n) { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + int a = n ~/ 3; + int b = n % 3; + if (b == 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return (pow(3, a - 1) * 2 * 2).toInt(); + } + if (b == 2) { + // When the remainder is 2, do nothing + return (pow(3, a) * 2).toInt(); + } + // When the remainder is 0, do nothing + return pow(3, a).toInt(); + } ``` === "Rust" ```rust title="max_product_cutting.rs" - [class]{}-[func]{max_product_cutting} + /* Max product cutting: Greedy algorithm */ + fn max_product_cutting(n: i32) -> i32 { + // When n <= 3, must cut out a 1 + if n <= 3 { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + let a = n / 3; + let b = n % 3; + if b == 1 { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + 3_i32.pow(a as u32 - 1) * 2 * 2 + } else if b == 2 { + // When the remainder is 2, do nothing + 3_i32.pow(a as u32) * 2 + } else { + // When the remainder is 0, do nothing + 3_i32.pow(a as u32) + } + } ``` === "C" ```c title="max_product_cutting.c" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + int maxProductCutting(int n) { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1); + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + int a = n / 3; + int b = n % 3; + if (b == 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return pow(3, a - 1) * 2 * 2; + } + if (b == 2) { + // When the remainder is 2, do nothing + return pow(3, a) * 2; + } + // When the remainder is 0, do nothing + return pow(3, a); + } ``` === "Kotlin" ```kotlin title="max_product_cutting.kt" - [class]{}-[func]{maxProductCutting} + /* Max product cutting: Greedy algorithm */ + fun maxProductCutting(n: Int): Int { + // When n <= 3, must cut out a 1 + if (n <= 3) { + return 1 * (n - 1) + } + // Greedily cut out 3, a is the number of 3s, b is the remainder + val a = n / 3 + val b = n % 3 + if (b == 1) { + // When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return 3.0.pow((a - 1)).toInt() * 2 * 2 + } + if (b == 2) { + // When the remainder is 2, do nothing + return 3.0.pow(a).toInt() * 2 * 2 + } + // When the remainder is 0, do nothing + return 3.0.pow(a).toInt() + } ``` === "Ruby" ```ruby title="max_product_cutting.rb" - [class]{}-[func]{max_product_cutting} + ### Maximum cutting product: greedy ### + def max_product_cutting(n) + # When n <= 3, must cut out a 1 + return 1 * (n - 1) if n <= 3 + # Greedily cut out 3, a is the number of 3s, b is the remainder + a, b = n / 3, n % 3 + # When the remainder is 1, convert a pair of 1 * 3 to 2 * 2 + return (3.pow(a - 1) * 2 * 2).to_i if b == 1 + # When the remainder is 2, do nothing + return (3.pow(a) * 2).to_i if b == 2 + # When the remainder is 0, do nothing + 3.pow(a).to_i + end ``` -=== "Zig" +![Calculation method for max product cutting](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" } - ```zig title="max_product_cutting.zig" - [class]{}-[func]{maxProductCutting} - ``` +

Figure 15-16   Calculation method for max product cutting

-![Calculation method of the maximum product after cutting](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" } +**The time complexity depends on the implementation of the exponentiation operation in the programming language**. Taking Python as an example, there are three commonly used power calculation functions. -

Figure 15-16   Calculation method of the maximum product after cutting

+- Both the operator `**` and the function `pow()` have time complexity $O(\log⁡ a)$. +- The function `math.pow()` internally calls the C library's `pow()` function, which performs floating-point exponentiation, with time complexity $O(1)$. -**Time complexity depends on the implementation of the power operation in the programming language**. For Python, the commonly used power calculation functions are three types: +Variables $a$ and $b$ use a constant amount of extra space, **therefore the space complexity is $O(1)$**. -- Both the operator `**` and the function `pow()` have a time complexity of $O(\log⁡ a)$. -- The `math.pow()` function internally calls the C language library's `pow()` function, performing floating-point exponentiation, with a time complexity of $O(1)$. +### 3.   Correctness Proof -Variables $a$ and $b$ use constant size of extra space, **hence the space complexity is $O(1)$**. +Using proof by contradiction, only analyzing the case where $n \geq 4$. -### 3.   Correctness proof - -Using the proof by contradiction, only analyze cases where $n \geq 3$. - -1. **All factors $\leq 3$**: Assume the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely be further split into $2(x-2)$, obtaining a larger product. This contradicts the assumption. -2. **The splitting scheme does not contain $1$**: Assume the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption. -3. **The splitting scheme contains at most two $2$s**: Assume the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s, achieving a higher product. This contradicts the assumption. +1. **All factors $\leq 3$**: Suppose the optimal splitting scheme includes a factor $x \geq 4$, then it can definitely continue to be split into $2(x-2)$ to obtain a larger (or equal) product. This contradicts the assumption. +2. **The splitting scheme does not contain $1$**: Suppose the optimal splitting scheme includes a factor of $1$, then it can definitely be merged into another factor to obtain a larger product. This contradicts the assumption. +3. **The splitting scheme contains at most two $2$s**: Suppose the optimal splitting scheme includes three $2$s, then they can definitely be replaced by two $3$s for a larger product. This contradicts the assumption. diff --git a/en/docs/chapter_greedy/summary.md b/en/docs/chapter_greedy/summary.md index 638218343..511f02e78 100644 --- a/en/docs/chapter_greedy/summary.md +++ b/en/docs/chapter_greedy/summary.md @@ -4,13 +4,15 @@ comments: true # 15.5   Summary -- Greedy algorithms are often used to solve optimization problems, where the principle is to make locally optimal decisions at each decision stage in order to achieve a globally optimal solution. -- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller sub-problem with each round, until the problem is resolved. -- Greedy algorithms are not only simple to implement but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms generally have a lower time complexity. -- In the problem of coin change, greedy algorithms can guarantee the optimal solution for certain combinations of coins; for others, however, the greedy algorithm might find a very poor solution. -- Problems suitable for greedy algorithm solutions possess two main properties: greedy-choice property and optimal substructure. The greedy-choice property represents the effectiveness of the greedy strategy. -- For some complex problems, proving the greedy-choice property is not straightforward. Contrarily, proving the invalidity is often easier, such as with the coin change problem. -- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and proving correctness. Among these, determining the greedy strategy is the key step, while proving correctness often poses the challenge. -- The fractional knapsack problem builds on the 0-1 knapsack problem by allowing the selection of a part of the items, hence it can be solved using a greedy algorithm. The correctness of the greedy strategy can be proved by contradiction. -- The maximum capacity problem can be solved using the exhaustive method, with a time complexity of $O(n^2)$. By designing a greedy strategy, each round moves inwardly shortening the board, optimizing the time complexity to $O(n)$. -- In the problem of maximum product after cutting, we deduce two greedy strategies: integers $\geq 4$ should continue to be cut, with the optimal cutting factor being $3$. The code includes power operations, and the time complexity depends on the method of implementing power operations, generally being $O(1)$ or $O(\log n)$. +### 1.   Key Review + +- Greedy algorithms are typically used to solve optimization problems. The principle is to make locally optimal decisions at each decision stage in hopes of obtaining a globally optimal solution. +- Greedy algorithms iteratively make one greedy choice after another, transforming the problem into a smaller subproblem in each round, until the problem is solved. +- Greedy algorithms are not only simple to implement, but also have high problem-solving efficiency. Compared to dynamic programming, greedy algorithms typically have lower time complexity. +- In the coin change problem, for certain coin combinations, greedy algorithms can guarantee finding the optimal solution; for other coin combinations, however, greedy algorithms may find very poor solutions. +- Problems suitable for solving with greedy algorithms have two major properties: greedy choice property and optimal substructure. The greedy choice property represents the effectiveness of the greedy strategy. +- For some complex problems, proving the greedy choice property is not simple. Relatively speaking, disproving it is easier, such as in the coin change problem. +- Solving greedy problems mainly consists of three steps: problem analysis, determining the greedy strategy, and correctness proof. Among these, determining the greedy strategy is the core step, and correctness proof is often the difficult point. +- The fractional knapsack problem, based on the 0-1 knapsack problem, allows selecting a portion of items, and therefore can be solved using greedy algorithms. The correctness of the greedy strategy can be proven using proof by contradiction. +- The max capacity problem can be solved using exhaustive enumeration with time complexity $O(n^2)$. By designing a greedy strategy to move the short partition inward in each round, the time complexity can be optimized to $O(n)$. +- In the max product cutting problem, we successively derive two greedy strategies: integers $\geq 4$ should all continue to be split, and the optimal splitting factor is $3$. The code includes exponentiation operations, and the time complexity depends on the implementation method of exponentiation, typically being $O(1)$ or $O(\log n)$. diff --git a/en/docs/chapter_hashing/hash_algorithm.md b/en/docs/chapter_hashing/hash_algorithm.md index 9745527dc..cafd626c5 100644 --- a/en/docs/chapter_hashing/hash_algorithm.md +++ b/en/docs/chapter_hashing/hash_algorithm.md @@ -2,17 +2,17 @@ comments: true --- -# 6.3   Hash algorithms +# 6.3   Hash Algorithm -The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and chaining can **only ensure that the hash table functions normally when collisions occur, but cannot reduce the frequency of hash collisions**. +The previous two sections introduced the working principle of hash tables and the methods to handle hash collisions. However, both open addressing and separate chaining **can only ensure that the hash table functions normally when hash collisions occur, but cannot reduce the frequency of hash collisions**. -If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to $O(n)$. +If hash collisions occur too frequently, the performance of the hash table will deteriorate drastically. As shown in Figure 6-8, for a separate chaining hash table, in the ideal case, the key-value pairs are evenly distributed across the buckets, achieving optimal query efficiency; in the worst case, all key-value pairs are stored in the same bucket, degrading the time complexity to $O(n)$. ![Ideal and worst cases of hash collisions](hash_algorithm.assets/hash_collision_best_worst_condition.png){ class="animation-figure" }

Figure 6-8   Ideal and worst cases of hash collisions

-**The distribution of key-value pairs is determined by the hash function**. Recalling the steps of calculating a hash function, first compute the hash value, then modulo it by the array length: +**The distribution of key-value pairs is determined by the hash function**. Recalling the calculation steps of the hash function, first compute the hash value, then take the modulo by the array length: ```shell index = hash(key) % capacity @@ -22,7 +22,7 @@ Observing the above formula, when the hash table capacity `capacity` is fixed, * This means that, to reduce the probability of hash collisions, we should focus on the design of the hash algorithm `hash()`. -## 6.3.1   Goals of hash algorithms +## 6.3.1   Goals of Hash Algorithms To achieve a "fast and stable" hash table data structure, hash algorithms should have the following characteristics: @@ -41,9 +41,9 @@ For cryptographic applications, to prevent reverse engineering such as deducing - **Collision resistance**: It should be extremely difficult to find two different inputs that produce the same hash value. - **Avalanche effect**: Minor changes in the input should lead to significant and unpredictable changes in the output. -Note that **"Uniform Distribution" and "Collision Resistance" are two separate concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password. +Note that **"uniform distribution" and "collision resistance" are two independent concepts**. Satisfying uniform distribution does not necessarily mean collision resistance. For example, under random input `key`, the hash function `key % 100` can produce a uniformly distributed output. However, this hash algorithm is too simple, and all `key` with the same last two digits will have the same output, making it easy to deduce a usable `key` from the hash value, thereby cracking the password. -## 6.3.2   Design of hash algorithms +## 6.3.2   Design of Hash Algorithms The design of hash algorithms is a complex issue that requires consideration of many factors. However, for some less demanding scenarios, we can also design some simple hash algorithms. @@ -179,133 +179,467 @@ The design of hash algorithms is a complex issue that requires consideration of === "C#" ```csharp title="simple_hash.cs" - [class]{simple_hash}-[func]{AddHash} + /* Additive hash */ + int AddHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = (hash + c) % MODULUS; + } + return (int)hash; + } - [class]{simple_hash}-[func]{MulHash} + /* Multiplicative hash */ + int MulHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = (31 * hash + c) % MODULUS; + } + return (int)hash; + } - [class]{simple_hash}-[func]{XorHash} + /* XOR hash */ + int XorHash(string key) { + int hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash ^= c; + } + return hash & MODULUS; + } - [class]{simple_hash}-[func]{RotHash} + /* Rotational hash */ + int RotHash(string key) { + long hash = 0; + const int MODULUS = 1000000007; + foreach (char c in key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c) % MODULUS; + } + return (int)hash; + } ``` === "Go" ```go title="simple_hash.go" - [class]{}-[func]{addHash} + /* Additive hash */ + func addHash(key string) int { + var hash int64 + var modulus int64 - [class]{}-[func]{mulHash} + modulus = 1000000007 + for _, b := range []byte(key) { + hash = (hash + int64(b)) % modulus + } + return int(hash) + } - [class]{}-[func]{xorHash} + /* Multiplicative hash */ + func mulHash(key string) int { + var hash int64 + var modulus int64 - [class]{}-[func]{rotHash} + modulus = 1000000007 + for _, b := range []byte(key) { + hash = (31*hash + int64(b)) % modulus + } + return int(hash) + } + + /* XOR hash */ + func xorHash(key string) int { + hash := 0 + modulus := 1000000007 + for _, b := range []byte(key) { + fmt.Println(int(b)) + hash ^= int(b) + hash = (31*hash + int(b)) % modulus + } + return hash & modulus + } + + /* Rotational hash */ + func rotHash(key string) int { + var hash int64 + var modulus int64 + + modulus = 1000000007 + for _, b := range []byte(key) { + hash = ((hash << 4) ^ (hash >> 28) ^ int64(b)) % modulus + } + return int(hash) + } ``` === "Swift" ```swift title="simple_hash.swift" - [class]{}-[func]{addHash} + /* Additive hash */ + func addHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = (hash + Int(scalar.value)) % MODULUS + } + } + return hash + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + func mulHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = (31 * hash + Int(scalar.value)) % MODULUS + } + } + return hash + } - [class]{}-[func]{xorHash} + /* XOR hash */ + func xorHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash ^= Int(scalar.value) + } + } + return hash & MODULUS + } - [class]{}-[func]{rotHash} + /* Rotational hash */ + func rotHash(key: String) -> Int { + var hash = 0 + let MODULUS = 1_000_000_007 + for c in key { + for scalar in c.unicodeScalars { + hash = ((hash << 4) ^ (hash >> 28) ^ Int(scalar.value)) % MODULUS + } + } + return hash + } ``` === "JS" ```javascript title="simple_hash.js" - [class]{}-[func]{addHash} + /* Additive hash */ + function addHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + function mulHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR hash */ + function xorHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash % MODULUS; + } - [class]{}-[func]{rotHash} + /* Rotational hash */ + function rotHash(key) { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "TS" ```typescript title="simple_hash.ts" - [class]{}-[func]{addHash} + /* Additive hash */ + function addHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + function mulHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = (31 * hash + c.charCodeAt(0)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR hash */ + function xorHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash ^= c.charCodeAt(0); + } + return hash % MODULUS; + } - [class]{}-[func]{rotHash} + /* Rotational hash */ + function rotHash(key: string): number { + let hash = 0; + const MODULUS = 1000000007; + for (const c of key) { + hash = ((hash << 4) ^ (hash >> 28) ^ c.charCodeAt(0)) % MODULUS; + } + return hash; + } ``` === "Dart" ```dart title="simple_hash.dart" - [class]{}-[func]{addHash} + /* Additive hash */ + int addHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = (hash + key.codeUnitAt(i)) % MODULUS; + } + return hash; + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + int mulHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = (31 * hash + key.codeUnitAt(i)) % MODULUS; + } + return hash; + } - [class]{}-[func]{xorHash} + /* XOR hash */ + int xorHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash ^= key.codeUnitAt(i); + } + return hash & MODULUS; + } - [class]{}-[func]{rotHash} + /* Rotational hash */ + int rotHash(String key) { + int hash = 0; + final int MODULUS = 1000000007; + for (int i = 0; i < key.length; i++) { + hash = ((hash << 4) ^ (hash >> 28) ^ key.codeUnitAt(i)) % MODULUS; + } + return hash; + } ``` === "Rust" ```rust title="simple_hash.rs" - [class]{}-[func]{add_hash} + /* Additive hash */ + fn add_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; - [class]{}-[func]{mul_hash} + for c in key.chars() { + hash = (hash + c as i64) % MODULUS; + } - [class]{}-[func]{xor_hash} + hash as i32 + } - [class]{}-[func]{rot_hash} + /* Multiplicative hash */ + fn mul_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash = (31 * hash + c as i64) % MODULUS; + } + + hash as i32 + } + + /* XOR hash */ + fn xor_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash ^= c as i64; + } + + (hash & MODULUS) as i32 + } + + /* Rotational hash */ + fn rot_hash(key: &str) -> i32 { + let mut hash = 0_i64; + const MODULUS: i64 = 1000000007; + + for c in key.chars() { + hash = ((hash << 4) ^ (hash >> 28) ^ c as i64) % MODULUS; + } + + hash as i32 + } ``` === "C" ```c title="simple_hash.c" - [class]{}-[func]{addHash} + /* Additive hash */ + int addHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = (hash + (unsigned char)key[i]) % MODULUS; + } + return (int)hash; + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + int mulHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = (31 * hash + (unsigned char)key[i]) % MODULUS; + } + return (int)hash; + } - [class]{}-[func]{xorHash} + /* XOR hash */ + int xorHash(char *key) { + int hash = 0; + const int MODULUS = 1000000007; - [class]{}-[func]{rotHash} + for (int i = 0; i < strlen(key); i++) { + hash ^= (unsigned char)key[i]; + } + return hash & MODULUS; + } + + /* Rotational hash */ + int rotHash(char *key) { + long long hash = 0; + const int MODULUS = 1000000007; + for (int i = 0; i < strlen(key); i++) { + hash = ((hash << 4) ^ (hash >> 28) ^ (unsigned char)key[i]) % MODULUS; + } + + return (int)hash; + } ``` === "Kotlin" ```kotlin title="simple_hash.kt" - [class]{}-[func]{addHash} + /* Additive hash */ + fun addHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = (hash + c.code) % MODULUS + } + return hash.toInt() + } - [class]{}-[func]{mulHash} + /* Multiplicative hash */ + fun mulHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = (31 * hash + c.code) % MODULUS + } + return hash.toInt() + } - [class]{}-[func]{xorHash} + /* XOR hash */ + fun xorHash(key: String): Int { + var hash = 0 + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = hash xor c.code + } + return hash and MODULUS + } - [class]{}-[func]{rotHash} + /* Rotational hash */ + fun rotHash(key: String): Int { + var hash = 0L + val MODULUS = 1000000007 + for (c in key.toCharArray()) { + hash = ((hash shl 4) xor (hash shr 28) xor c.code.toLong()) % MODULUS + } + return hash.toInt() + } ``` === "Ruby" ```ruby title="simple_hash.rb" - [class]{}-[func]{add_hash} + ### Additive hash ### + def add_hash(key) + hash = 0 + modulus = 1_000_000_007 - [class]{}-[func]{mul_hash} + key.each_char { |c| hash += c.ord } - [class]{}-[func]{xor_hash} + hash % modulus + end - [class]{}-[func]{rot_hash} - ``` + ### Multiplicative hash ### + def mul_hash(key) + hash = 0 + modulus = 1_000_000_007 -=== "Zig" + key.each_char { |c| hash = 31 * hash + c.ord } - ```zig title="simple_hash.zig" - [class]{}-[func]{addHash} + hash % modulus + end - [class]{}-[func]{mulHash} + ### XOR hash ### + def xor_hash(key) + hash = 0 + modulus = 1_000_000_007 - [class]{}-[func]{xorHash} + key.each_char { |c| hash ^= c.ord } - [class]{}-[func]{rotHash} + hash % modulus + end + + ### Rotational hash ### + def rot_hash(key) + hash = 0 + modulus = 1_000_000_007 + + key.each_char { |c| hash = (hash << 4) ^ (hash >> 28) ^ c.ord } + + hash % modulus + end ``` It is observed that the last step of each hash algorithm is to take the modulus of the large prime number $1000000007$ to ensure that the hash value is within an appropriate range. It is worth pondering why emphasis is placed on modulo a prime number, or what are the disadvantages of modulo a composite number? This is an interesting question. @@ -336,7 +670,7 @@ It is worth noting that if the `key` is guaranteed to be randomly and uniformly In summary, we usually choose a prime number as the modulus, and this prime number should be large enough to eliminate periodic patterns as much as possible, enhancing the robustness of the hash algorithm. -## 6.3.3   Common hash algorithms +## 6.3.3   Common Hash Algorithms It is not hard to see that the simple hash algorithms mentioned above are quite "fragile" and far from reaching the design goals of hash algorithms. For example, since addition and XOR obey the commutative law, additive hash and XOR hash cannot distinguish strings with the same content but in different order, which may exacerbate hash collisions and cause security issues. @@ -362,7 +696,7 @@ Over the past century, hash algorithms have been in a continuous process of upgr -# Hash values in data structures +# Hash Values in Data Structures We know that the keys in a hash table can be of various data types such as integers, decimals, or strings. Programming languages usually provide built-in hash algorithms for these data types to calculate the bucket indices in the hash table. Taking Python as an example, we can use the `hash()` function to compute the hash values for various data types. @@ -608,19 +942,62 @@ We know that the keys in a hash table can be of various data types such as integ === "Kotlin" ```kotlin title="built_in_hash.kt" + val num = 3 + val hashNum = num.hashCode() + // Hash value of integer 3 is 3 + val bol = true + val hashBol = bol.hashCode() + // Hash value of boolean true is 1231 + + val dec = 3.14159 + val hashDec = dec.hashCode() + // Hash value of decimal 3.14159 is -1340954729 + + val str = "Hello 算法" + val hashStr = str.hashCode() + // Hash value of string "Hello 算法" is -727081396 + + val arr = arrayOf(12836, "小哈") + val hashTup = arr.hashCode() + // Hash value of array [12836, 小哈] is 189568618 + + val obj = ListNode(0) + val hashObj = obj.hashCode() + // Hash value of ListNode object utils.ListNode@1d81eb93 is 495053715 ``` -=== "Zig" +=== "Ruby" - ```zig title="built_in_hash.zig" + ```ruby title="built_in_hash.rb" + num = 3 + hash_num = num.hash + # Hash value of integer 3 is -4385856518450339636 + bol = true + hash_bol = bol.hash + # Hash value of boolean true is -1617938112149317027 + + dec = 3.14159 + hash_dec = dec.hash + # Hash value of decimal 3.14159 is -1479186995943067893 + + str = "Hello 算法" + hash_str = str.hash + # Hash value of string "Hello 算法" is -4075943250025831763 + + tup = [12836, '小哈'] + hash_tup = tup.hash + # Hash value of tuple (12836, '小哈') is 1999544809202288822 + + obj = ListNode.new(0) + hash_obj = obj.hash + # Hash value of ListNode object # is 4302940560806366381 ``` -??? pythontutor "Code Visualization" +??? pythontutor "Visualized Execution" -
- + https://pythontutor.com/render.html#code=class%20ListNode%3A%0A%20%20%20%20%22%22%22%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.next%3A%20ListNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%90%8E%E7%BB%A7%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20num%20%3D%203%0A%20%20%20%20hash_num%20%3D%20hash%28num%29%0A%20%20%20%20%23%20%E6%95%B4%E6%95%B0%203%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%203%0A%0A%20%20%20%20bol%20%3D%20True%0A%20%20%20%20hash_bol%20%3D%20hash%28bol%29%0A%20%20%20%20%23%20%E5%B8%83%E5%B0%94%E9%87%8F%20True%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201%0A%0A%20%20%20%20dec%20%3D%203.14159%0A%20%20%20%20hash_dec%20%3D%20hash%28dec%29%0A%20%20%20%20%23%20%E5%B0%8F%E6%95%B0%203.14159%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20326484311674566659%0A%0A%20%20%20%20str%20%3D%20%22Hello%20%E7%AE%97%E6%B3%95%22%0A%20%20%20%20hash_str%20%3D%20hash%28str%29%0A%20%20%20%20%23%20%E5%AD%97%E7%AC%A6%E4%B8%B2%E2%80%9CHello%20%E7%AE%97%E6%B3%95%E2%80%9D%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%204617003410720528961%0A%0A%20%20%20%20tup%20%3D%20%2812836,%20%22%E5%B0%8F%E5%93%88%22%29%0A%20%20%20%20hash_tup%20%3D%20hash%28tup%29%0A%20%20%20%20%23%20%E5%85%83%E7%BB%84%20%2812836,%20'%E5%B0%8F%E5%93%88'%29%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%201029005403108185979%0A%0A%20%20%20%20obj%20%3D%20ListNode%280%29%0A%20%20%20%20hash_obj%20%3D%20hash%28obj%29%0A%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%AF%B9%E8%B1%A1%20%3CListNode%20object%20at%200x1058fd810%3E%20%E7%9A%84%E5%93%88%E5%B8%8C%E5%80%BC%E4%B8%BA%20274267521&cumulative=false&curInstr=19&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false In many programming languages, **only immutable objects can serve as the `key` in a hash table**. If we use a list (dynamic array) as a `key`, when the contents of the list change, its hash value also changes, and we would no longer be able to find the original `value` in the hash table. diff --git a/en/docs/chapter_hashing/hash_collision.md b/en/docs/chapter_hashing/hash_collision.md index 90d9f4973..fb3a41b6c 100644 --- a/en/docs/chapter_hashing/hash_collision.md +++ b/en/docs/chapter_hashing/hash_collision.md @@ -2,20 +2,20 @@ comments: true --- -# 6.2   Hash collision +# 6.2   Hash Collision -The previous section mentioned that, **in most cases, the input space of a hash function is much larger than the output space**, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the size of the array capacity, then multiple integers will inevitably be mapped to the same bucket index. +The previous section mentioned that, **in most cases, the input space of a hash function is much larger than the output space**, so theoretically, hash collisions are inevitable. For example, if the input space is all integers and the output space is the array capacity size, then multiple integers will inevitably be mapped to the same bucket index. -Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we perform hash table resizing until the collision disappears. This approach is pretty simple, straightforward, and working well. However, it appears to be pretty inefficient as the table expansion involves a lot of data migration as well as recalculation of hash code, which are expansive. To improve efficiency, we can adopt the following strategies: +Hash collisions can lead to incorrect query results, severely impacting the usability of the hash table. To address this issue, whenever a hash collision occurs, we can perform hash table expansion until the collision disappears. This approach is simple, straightforward, and effective, but it is very inefficient because hash table expansion involves a large amount of data migration and hash value recalculation. To improve efficiency, we can adopt the following strategies: -1. Improve the hash table data structure in a way that **locating target element is still functioning well in the event of a hash collision**. -2. Expansion is the last resort before it becomes necessary, when severe collisions are observed. +1. Improve the hash table data structure so that **the hash table can function normally when hash collisions occur**. +2. Only expand when necessary, that is, only when hash collisions are severe. -There are mainly two methods for improving the structure of hash tables: "Separate Chaining" and "Open Addressing". +The main methods for improving the structure of hash tables include "separate chaining" and "open addressing". -## 6.2.1   Separate chaining +## 6.2.1   Separate Chaining -In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as list nodes, storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a hash table with separate chaining. +In the original hash table, each bucket can store only one key-value pair. Separate chaining converts a single element into a linked list, treating key-value pairs as linked list nodes and storing all colliding key-value pairs in the same linked list. Figure 6-5 shows an example of a separate chaining hash table. ![Separate chaining hash table](hash_collision.assets/hash_table_chaining.png){ class="animation-figure" } @@ -23,9 +23,9 @@ In the original hash table, each bucket can store only one key-value pair. Se The operations of a hash table implemented with separate chaining have changed as follows: -- **Querying Elements**: Input `key`, obtain the bucket index through the hash function, then access the head node of the linked list. Traverse the linked list and compare key to find the target key-value pair. -- **Adding Elements**: Access the head node of the linked list via the hash function, then append the node (key-value pair) to the list. -- **Deleting Elements**: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it. +- **Querying elements**: Input `key`, obtain the bucket index through the hash function, then access the head node of the linked list, then traverse the linked list and compare `key` to find the target key-value pair. +- **Adding elements**: First access the linked list head node through the hash function, then append the node (key-value pair) to the linked list. +- **Deleting elements**: Access the head of the linked list based on the result of the hash function, then traverse the linked list to find the target node and delete it. Separate chaining has the following limitations: @@ -34,14 +34,14 @@ Separate chaining has the following limitations: The code below provides a simple implementation of a separate chaining hash table, with two things to note: -- Lists (dynamic arrays) are used instead of linked lists for simplicity. In this setup, the hash table (array) contains multiple buckets, each of which is a list. -- This implementation includes a hash table resizing method. When the load factor exceeds $\frac{2}{3}$, we expand the hash table to twice its original size. +- Lists (dynamic arrays) are used instead of linked lists to simplify the code. In this setup, the hash table (array) contains multiple buckets, each of which is a list. +- This implementation includes a hash table expansion method. When the load factor exceeds $\frac{2}{3}$, we expand the hash table to $2$ times its original size. === "Python" ```python title="hash_map_chaining.py" class HashMapChaining: - """Chained address hash table""" + """Hash table with separate chaining""" def __init__(self): """Constructor""" @@ -63,26 +63,26 @@ The code below provides a simple implementation of a separate chaining hash tabl """Query operation""" index = self.hash_func(key) bucket = self.buckets[index] - # Traverse the bucket, if the key is found, return the corresponding val + # Traverse bucket, if key is found, return corresponding val for pair in bucket: if pair.key == key: return pair.val - # If the key is not found, return None + # If key is not found, return None return None def put(self, key: int, val: str): """Add operation""" - # When the load factor exceeds the threshold, perform expansion + # When load factor exceeds threshold, perform expansion if self.load_factor() > self.load_thres: self.extend() index = self.hash_func(key) bucket = self.buckets[index] - # Traverse the bucket, if the specified key is encountered, update the corresponding val and return + # Traverse bucket, if specified key is encountered, update corresponding val and return for pair in bucket: if pair.key == key: pair.val = val return - # If the key is not found, add the key-value pair to the end + # If key does not exist, append key-value pair to the end pair = Pair(key, val) bucket.append(pair) self.size += 1 @@ -91,7 +91,7 @@ The code below provides a simple implementation of a separate chaining hash tabl """Remove operation""" index = self.hash_func(key) bucket = self.buckets[index] - # Traverse the bucket, remove the key-value pair from it + # Traverse bucket and remove key-value pair from it for pair in bucket: if pair.key == key: bucket.remove(pair) @@ -99,14 +99,14 @@ The code below provides a simple implementation of a separate chaining hash tabl break def extend(self): - """Extend hash table""" + """Expand hash table""" # Temporarily store the original hash table buckets = self.buckets - # Initialize the extended new hash table + # Initialize expanded new hash table self.capacity *= self.extend_ratio self.buckets = [[] for _ in range(self.capacity)] self.size = 0 - # Move key-value pairs from the original hash table to the new hash table + # Move key-value pairs from original hash table to new hash table for bucket in buckets: for pair in bucket: self.put(pair.key, pair.val) @@ -123,7 +123,7 @@ The code below provides a simple implementation of a separate chaining hash tabl === "C++" ```cpp title="hash_map_chaining.cpp" - /* Chained address hash table */ + /* Hash table with separate chaining */ class HashMapChaining { private: int size; // Number of key-value pairs @@ -161,31 +161,31 @@ The code below provides a simple implementation of a separate chaining hash tabl /* Query operation */ string get(int key) { int index = hashFunc(key); - // Traverse the bucket, if the key is found, return the corresponding val + // Traverse bucket, if key is found, return corresponding val for (Pair *pair : buckets[index]) { if (pair->key == key) { return pair->val; } } - // If key not found, return an empty string + // Return empty string if key not found return ""; } /* Add operation */ void put(int key, string val) { - // When the load factor exceeds the threshold, perform expansion + // When load factor exceeds threshold, perform expansion if (loadFactor() > loadThres) { extend(); } int index = hashFunc(key); - // Traverse the bucket, if the specified key is encountered, update the corresponding val and return + // Traverse bucket, if specified key is encountered, update corresponding val and return for (Pair *pair : buckets[index]) { if (pair->key == key) { pair->val = val; return; } } - // If the key is not found, add the key-value pair to the end + // If key does not exist, append key-value pair to the end buckets[index].push_back(new Pair(key, val)); size++; } @@ -194,11 +194,11 @@ The code below provides a simple implementation of a separate chaining hash tabl void remove(int key) { int index = hashFunc(key); auto &bucket = buckets[index]; - // Traverse the bucket, remove the key-value pair from it + // Traverse bucket and remove key-value pair from it for (int i = 0; i < bucket.size(); i++) { if (bucket[i]->key == key) { Pair *tmp = bucket[i]; - bucket.erase(bucket.begin() + i); // Remove key-value pair + bucket.erase(bucket.begin() + i); // Remove key-value pair from it delete tmp; // Free memory size--; return; @@ -206,16 +206,16 @@ The code below provides a simple implementation of a separate chaining hash tabl } } - /* Extend hash table */ + /* Expand hash table */ void extend() { // Temporarily store the original hash table vector> bucketsTmp = buckets; - // Initialize the extended new hash table + // Initialize expanded new hash table capacity *= extendRatio; buckets.clear(); buckets.resize(capacity); size = 0; - // Move key-value pairs from the original hash table to the new hash table + // Move key-value pairs from original hash table to new hash table for (auto &bucket : bucketsTmp) { for (Pair *pair : bucket) { put(pair->key, pair->val); @@ -241,7 +241,7 @@ The code below provides a simple implementation of a separate chaining hash tabl === "Java" ```java title="hash_map_chaining.java" - /* Chained address hash table */ + /* Hash table with separate chaining */ class HashMapChaining { int size; // Number of key-value pairs int capacity; // Hash table capacity @@ -275,7 +275,7 @@ The code below provides a simple implementation of a separate chaining hash tabl String get(int key) { int index = hashFunc(key); List bucket = buckets.get(index); - // Traverse the bucket, if the key is found, return the corresponding val + // Traverse bucket, if key is found, return corresponding val for (Pair pair : bucket) { if (pair.key == key) { return pair.val; @@ -287,20 +287,20 @@ The code below provides a simple implementation of a separate chaining hash tabl /* Add operation */ void put(int key, String val) { - // When the load factor exceeds the threshold, perform expansion + // When load factor exceeds threshold, perform expansion if (loadFactor() > loadThres) { extend(); } int index = hashFunc(key); List bucket = buckets.get(index); - // Traverse the bucket, if the specified key is encountered, update the corresponding val and return + // Traverse bucket, if specified key is encountered, update corresponding val and return for (Pair pair : bucket) { if (pair.key == key) { pair.val = val; return; } } - // If the key is not found, add the key-value pair to the end + // If key does not exist, append key-value pair to the end Pair pair = new Pair(key, val); bucket.add(pair); size++; @@ -310,7 +310,7 @@ The code below provides a simple implementation of a separate chaining hash tabl void remove(int key) { int index = hashFunc(key); List bucket = buckets.get(index); - // Traverse the bucket, remove the key-value pair from it + // Traverse bucket and remove key-value pair from it for (Pair pair : bucket) { if (pair.key == key) { bucket.remove(pair); @@ -320,18 +320,18 @@ The code below provides a simple implementation of a separate chaining hash tabl } } - /* Extend hash table */ + /* Expand hash table */ void extend() { // Temporarily store the original hash table List> bucketsTmp = buckets; - // Initialize the extended new hash table + // Initialize expanded new hash table capacity *= extendRatio; buckets = new ArrayList<>(capacity); for (int i = 0; i < capacity; i++) { buckets.add(new ArrayList<>()); } size = 0; - // Move key-value pairs from the original hash table to the new hash table + // Move key-value pairs from original hash table to new hash table for (List bucket : bucketsTmp) { for (Pair pair : bucket) { put(pair.key, pair.val); @@ -355,85 +355,1182 @@ The code below provides a simple implementation of a separate chaining hash tabl === "C#" ```csharp title="hash_map_chaining.cs" - [class]{HashMapChaining}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + int size; // Number of key-value pairs + int capacity; // Hash table capacity + double loadThres; // Load factor threshold for triggering expansion + int extendRatio; // Expansion multiplier + List> buckets; // Bucket array + + /* Constructor */ + 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([]); + } + } + + /* Hash function */ + int HashFunc(int key) { + return key % capacity; + } + + /* Load factor */ + double LoadFactor() { + return (double)size / capacity; + } + + /* Query operation */ + public string? Get(int key) { + int index = HashFunc(key); + // Traverse bucket, if key is found, return corresponding val + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + return pair.val; + } + } + // If key is not found, return null + return null; + } + + /* Add operation */ + public void Put(int key, string val) { + // When load factor exceeds threshold, perform expansion + if (LoadFactor() > loadThres) { + Extend(); + } + int index = HashFunc(key); + // Traverse bucket, if specified key is encountered, update corresponding val and return + foreach (Pair pair in buckets[index]) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // If key does not exist, append key-value pair to the end + buckets[index].Add(new Pair(key, val)); + size++; + } + + /* Remove operation */ + public void Remove(int key) { + int index = HashFunc(key); + // Traverse bucket and remove key-value pair from it + foreach (Pair pair in buckets[index].ToList()) { + if (pair.key == key) { + buckets[index].Remove(pair); + size--; + break; + } + } + } + + /* Expand hash table */ + void Extend() { + // Temporarily store the original hash table + List> bucketsTmp = buckets; + // Initialize expanded new hash table + capacity *= extendRatio; + buckets = new List>(capacity); + for (int i = 0; i < capacity; i++) { + buckets.Add([]); + } + size = 0; + // Move key-value pairs from original hash table to new hash table + foreach (List bucket in bucketsTmp) { + foreach (Pair pair in bucket) { + Put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + public void Print() { + foreach (List bucket in buckets) { + List res = []; + foreach (Pair pair in bucket) { + res.Add(pair.key + " -> " + pair.val); + } + foreach (string kv in res) { + Console.WriteLine(kv); + } + } + } + } ``` === "Go" ```go title="hash_map_chaining.go" - [class]{hashMapChaining}-[func]{} + /* Hash table with separate chaining */ + type hashMapChaining struct { + size int // Number of key-value pairs + capacity int // Hash table capacity + loadThres float64 // Load factor threshold for triggering expansion + extendRatio int // Expansion multiplier + buckets [][]pair // Bucket array + } + + /* Constructor */ + 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, + } + } + + /* Hash function */ + func (m *hashMapChaining) hashFunc(key int) int { + return key % m.capacity + } + + /* Load factor */ + func (m *hashMapChaining) loadFactor() float64 { + return float64(m.size) / float64(m.capacity) + } + + /* Query operation */ + func (m *hashMapChaining) get(key int) string { + idx := m.hashFunc(key) + bucket := m.buckets[idx] + // Traverse bucket, if key is found, return corresponding val + for _, p := range bucket { + if p.key == key { + return p.val + } + } + // Return empty string if key not found + return "" + } + + /* Add operation */ + func (m *hashMapChaining) put(key int, val string) { + // When load factor exceeds threshold, perform expansion + if m.loadFactor() > m.loadThres { + m.extend() + } + idx := m.hashFunc(key) + // Traverse bucket, if specified key is encountered, update corresponding val and return + for i := range m.buckets[idx] { + if m.buckets[idx][i].key == key { + m.buckets[idx][i].val = val + return + } + } + // If key does not exist, append key-value pair to the end + p := pair{ + key: key, + val: val, + } + m.buckets[idx] = append(m.buckets[idx], p) + m.size += 1 + } + + /* Remove operation */ + func (m *hashMapChaining) remove(key int) { + idx := m.hashFunc(key) + // Traverse bucket and remove key-value pair from it + for i, p := range m.buckets[idx] { + if p.key == key { + // Slice deletion + m.buckets[idx] = append(m.buckets[idx][:i], m.buckets[idx][i+1:]...) + m.size -= 1 + break + } + } + } + + /* Expand hash table */ + func (m *hashMapChaining) extend() { + // Temporarily store the original hash table + 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]) + } + // Initialize expanded new hash table + 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 + // Move key-value pairs from original hash table to new hash table + for _, bucket := range tmpBuckets { + for _, p := range bucket { + m.put(p.key, p.val) + } + } + } + + /* Print hash table */ + 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}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + var size: Int // Number of key-value pairs + var capacity: Int // Hash table capacity + var loadThres: Double // Load factor threshold for triggering expansion + var extendRatio: Int // Expansion multiplier + var buckets: [[Pair]] // Bucket array + + /* Constructor */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: [], count: capacity) + } + + /* Hash function */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* Load factor */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* Query operation */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let bucket = buckets[index] + // Traverse bucket, if key is found, return corresponding val + for pair in bucket { + if pair.key == key { + return pair.val + } + } + // Return nil if key not found + return nil + } + + /* Add operation */ + func put(key: Int, val: String) { + // When load factor exceeds threshold, perform expansion + if loadFactor() > loadThres { + extend() + } + let index = hashFunc(key: key) + let bucket = buckets[index] + // Traverse bucket, if specified key is encountered, update corresponding val and return + for pair in bucket { + if pair.key == key { + pair.val = val + return + } + } + // If key does not exist, append key-value pair to the end + let pair = Pair(key: key, val: val) + buckets[index].append(pair) + size += 1 + } + + /* Remove operation */ + func remove(key: Int) { + let index = hashFunc(key: key) + let bucket = buckets[index] + // Traverse bucket and remove key-value pair from it + for (pairIndex, pair) in bucket.enumerated() { + if pair.key == key { + buckets[index].remove(at: pairIndex) + size -= 1 + break + } + } + } + + /* Expand hash table */ + func extend() { + // Temporarily store the original hash table + let bucketsTmp = buckets + // Initialize expanded new hash table + capacity *= extendRatio + buckets = Array(repeating: [], count: capacity) + size = 0 + // Move key-value pairs from original hash table to new hash table + for bucket in bucketsTmp { + for pair in bucket { + put(key: pair.key, val: pair.val) + } + } + } + + /* Print hash table */ + 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}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + #size; // Number of key-value pairs + #capacity; // Hash table capacity + #loadThres; // Load factor threshold for triggering expansion + #extendRatio; // Expansion multiplier + #buckets; // Bucket array + + /* Constructor */ + 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) => []); + } + + /* Hash function */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* Load factor */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* Query operation */ + get(key) { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // Traverse bucket, if key is found, return corresponding val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // If key is not found, return null + return null; + } + + /* Add operation */ + put(key, val) { + // When load factor exceeds threshold, perform expansion + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // Traverse bucket, if specified key is encountered, update corresponding val and return + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // If key does not exist, append key-value pair to the end + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* Remove operation */ + remove(key) { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // Traverse bucket and remove key-value pair from it + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* Expand hash table */ + #extend() { + // Temporarily store the original hash table + const bucketsTmp = this.#buckets; + // Initialize expanded new hash table + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // Move key-value pairs from original hash table to new hash table + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + #size: number; // Number of key-value pairs + #capacity: number; // Hash table capacity + #loadThres: number; // Load factor threshold for triggering expansion + #extendRatio: number; // Expansion multiplier + #buckets: Pair[][]; // Bucket array + + /* Constructor */ + 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) => []); + } + + /* Hash function */ + #hashFunc(key: number): number { + return key % this.#capacity; + } + + /* Load factor */ + #loadFactor(): number { + return this.#size / this.#capacity; + } + + /* Query operation */ + get(key: number): string | null { + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // Traverse bucket, if key is found, return corresponding val + for (const pair of bucket) { + if (pair.key === key) { + return pair.val; + } + } + // If key is not found, return null + return null; + } + + /* Add operation */ + put(key: number, val: string): void { + // When load factor exceeds threshold, perform expansion + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + const index = this.#hashFunc(key); + const bucket = this.#buckets[index]; + // Traverse bucket, if specified key is encountered, update corresponding val and return + for (const pair of bucket) { + if (pair.key === key) { + pair.val = val; + return; + } + } + // If key does not exist, append key-value pair to the end + const pair = new Pair(key, val); + bucket.push(pair); + this.#size++; + } + + /* Remove operation */ + remove(key: number): void { + const index = this.#hashFunc(key); + let bucket = this.#buckets[index]; + // Traverse bucket and remove key-value pair from it + for (let i = 0; i < bucket.length; i++) { + if (bucket[i].key === key) { + bucket.splice(i, 1); + this.#size--; + break; + } + } + } + + /* Expand hash table */ + #extend(): void { + // Temporarily store the original hash table + const bucketsTmp = this.#buckets; + // Initialize expanded new hash table + this.#capacity *= this.#extendRatio; + this.#buckets = new Array(this.#capacity).fill(null).map((x) => []); + this.#size = 0; + // Move key-value pairs from original hash table to new hash table + for (const bucket of bucketsTmp) { + for (const pair of bucket) { + this.put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + late int size; // Number of key-value pairs + late int capacity; // Hash table capacity + late double loadThres; // Load factor threshold for triggering expansion + late int extendRatio; // Expansion multiplier + late List> buckets; // Bucket array + + /* Constructor */ + HashMapChaining() { + size = 0; + capacity = 4; + loadThres = 2.0 / 3.0; + extendRatio = 2; + buckets = List.generate(capacity, (_) => []); + } + + /* Hash function */ + int hashFunc(int key) { + return key % capacity; + } + + /* Load factor */ + double loadFactor() { + return size / capacity; + } + + /* Query operation */ + String? get(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // Traverse bucket, if key is found, return corresponding val + for (Pair pair in bucket) { + if (pair.key == key) { + return pair.val; + } + } + // If key is not found, return null + return null; + } + + /* Add operation */ + void put(int key, String val) { + // When load factor exceeds threshold, perform expansion + if (loadFactor() > loadThres) { + extend(); + } + int index = hashFunc(key); + List bucket = buckets[index]; + // Traverse bucket, if specified key is encountered, update corresponding val and return + for (Pair pair in bucket) { + if (pair.key == key) { + pair.val = val; + return; + } + } + // If key does not exist, append key-value pair to the end + Pair pair = Pair(key, val); + bucket.add(pair); + size++; + } + + /* Remove operation */ + void remove(int key) { + int index = hashFunc(key); + List bucket = buckets[index]; + // Traverse bucket and remove key-value pair from it + for (Pair pair in bucket) { + if (pair.key == key) { + bucket.remove(pair); + size--; + break; + } + } + } + + /* Expand hash table */ + void extend() { + // Temporarily store the original hash table + List> bucketsTmp = buckets; + // Initialize expanded new hash table + capacity *= extendRatio; + buckets = List.generate(capacity, (_) => []); + size = 0; + // Move key-value pairs from original hash table to new hash table + for (List bucket in bucketsTmp) { + for (Pair pair in bucket) { + put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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" - [class]{HashMapChaining}-[func]{} + /* Hash table with separate chaining */ + struct HashMapChaining { + size: usize, + capacity: usize, + load_thres: f32, + extend_ratio: usize, + buckets: Vec>, + } + + impl HashMapChaining { + /* Constructor */ + fn new() -> Self { + Self { + size: 0, + capacity: 4, + load_thres: 2.0 / 3.0, + extend_ratio: 2, + buckets: vec![vec![]; 4], + } + } + + /* Hash function */ + fn hash_func(&self, key: i32) -> usize { + key as usize % self.capacity + } + + /* Load factor */ + fn load_factor(&self) -> f32 { + self.size as f32 / self.capacity as f32 + } + + /* Remove operation */ + fn remove(&mut self, key: i32) -> Option { + let index = self.hash_func(key); + + // Traverse bucket and remove key-value pair from it + for (i, p) in self.buckets[index].iter_mut().enumerate() { + if p.key == key { + let pair = self.buckets[index].remove(i); + self.size -= 1; + return Some(pair.val); + } + } + + // If key is not found, return None + None + } + + /* Expand hash table */ + fn extend(&mut self) { + // Temporarily store the original hash table + let buckets_tmp = std::mem::take(&mut self.buckets); + + // Initialize expanded new hash table + self.capacity *= self.extend_ratio; + self.buckets = vec![Vec::new(); self.capacity as usize]; + self.size = 0; + + // Move key-value pairs from original hash table to new hash table + for bucket in buckets_tmp { + for pair in bucket { + self.put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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); + } + } + + /* Add operation */ + fn put(&mut self, key: i32, val: String) { + // When load factor exceeds threshold, perform expansion + if self.load_factor() > self.load_thres { + self.extend(); + } + + let index = self.hash_func(key); + + // Traverse bucket, if specified key is encountered, update corresponding val and return + for pair in self.buckets[index].iter_mut() { + if pair.key == key { + pair.val = val; + return; + } + } + + // If key does not exist, append key-value pair to the end + let pair = Pair { key, val }; + self.buckets[index].push(pair); + self.size += 1; + } + + /* Query operation */ + fn get(&self, key: i32) -> Option<&str> { + let index = self.hash_func(key); + + // Traverse bucket, if key is found, return corresponding val + for pair in self.buckets[index].iter() { + if pair.key == key { + return Some(&pair.val); + } + } + + // If key is not found, return None + None + } + } ``` === "C" ```c title="hash_map_chaining.c" - [class]{Node}-[func]{} + /* Linked list node */ + typedef struct Node { + Pair *pair; + struct Node *next; + } Node; - [class]{HashMapChaining}-[func]{} + /* Hash table with separate chaining */ + typedef struct { + int size; // Number of key-value pairs + int capacity; // Hash table capacity + double loadThres; // Load factor threshold for triggering expansion + int extendRatio; // Expansion multiplier + Node **buckets; // Bucket array + } HashMapChaining; + + /* Constructor */ + HashMapChaining *newHashMapChaining() { + HashMapChaining *hashMap = (HashMapChaining *)malloc(sizeof(HashMapChaining)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + return hashMap; + } + + /* Destructor */ + void delHashMapChaining(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + while (cur) { + Node *tmp = cur; + cur = cur->next; + free(tmp->pair); + free(tmp); + } + } + free(hashMap->buckets); + free(hashMap); + } + + /* Hash function */ + int hashFunc(HashMapChaining *hashMap, int key) { + return key % hashMap->capacity; + } + + /* Load factor */ + double loadFactor(HashMapChaining *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* Query operation */ + char *get(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + // Traverse bucket, if key is found, return corresponding val + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + return cur->pair->val; + } + cur = cur->next; + } + return ""; // Return empty string if key not found + } + + /* Add operation */ + void put(HashMapChaining *hashMap, int key, const char *val) { + // When load factor exceeds threshold, perform expansion + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + int index = hashFunc(hashMap, key); + // Traverse bucket, if specified key is encountered, update corresponding val and return + Node *cur = hashMap->buckets[index]; + while (cur) { + if (cur->pair->key == key) { + strcpy(cur->pair->val, val); // If specified key is found, update corresponding val and return + return; + } + cur = cur->next; + } + // If key not found, add key-value pair to list head + Pair *newPair = (Pair *)malloc(sizeof(Pair)); + newPair->key = key; + strcpy(newPair->val, val); + Node *newNode = (Node *)malloc(sizeof(Node)); + newNode->pair = newPair; + newNode->next = hashMap->buckets[index]; + hashMap->buckets[index] = newNode; + hashMap->size++; + } + + /* Expand hash table */ + void extend(HashMapChaining *hashMap) { + // Temporarily store the original hash table + int oldCapacity = hashMap->capacity; + Node **oldBuckets = hashMap->buckets; + // Initialize expanded new hash table + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Node **)malloc(hashMap->capacity * sizeof(Node *)); + for (int i = 0; i < hashMap->capacity; i++) { + hashMap->buckets[i] = NULL; + } + hashMap->size = 0; + // Move key-value pairs from original hash table to new hash table + for (int i = 0; i < oldCapacity; i++) { + Node *cur = oldBuckets[i]; + while (cur) { + put(hashMap, cur->pair->key, cur->pair->val); + Node *temp = cur; + cur = cur->next; + // Free memory + free(temp->pair); + free(temp); + } + } + + free(oldBuckets); + } + + /* Remove operation */ + void removeItem(HashMapChaining *hashMap, int key) { + int index = hashFunc(hashMap, key); + Node *cur = hashMap->buckets[index]; + Node *pre = NULL; + while (cur) { + if (cur->pair->key == key) { + // Remove key-value pair from it + if (pre) { + pre->next = cur->next; + } else { + hashMap->buckets[index] = cur->next; + } + // Free memory + free(cur->pair); + free(cur); + hashMap->size--; + return; + } + pre = cur; + cur = cur->next; + } + } + + /* Print hash table */ + void print(HashMapChaining *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Node *cur = hashMap->buckets[i]; + printf("["); + while (cur) { + printf("%d -> %s, ", cur->pair->key, cur->pair->val); + cur = cur->next; + } + printf("]\n"); + } + } ``` === "Kotlin" ```kotlin title="hash_map_chaining.kt" - [class]{HashMapChaining}-[func]{} + /* Hash table with separate chaining */ + class HashMapChaining { + var size: Int // Number of key-value pairs + var capacity: Int // Hash table capacity + val loadThres: Double // Load factor threshold for triggering expansion + val extendRatio: Int // Expansion multiplier + var buckets: MutableList> // Bucket array + + /* Constructor */ + init { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = mutableListOf() + for (i in 0.. loadThres) { + extend() + } + val index = hashFunc(key) + val bucket = buckets[index] + // Traverse bucket, if specified key is encountered, update corresponding val and return + for (pair in bucket) { + if (pair.key == key) { + pair._val = _val + return + } + } + // If key does not exist, append key-value pair to the end + val pair = Pair(key, _val) + bucket.add(pair) + size++ + } + + /* Remove operation */ + fun remove(key: Int) { + val index = hashFunc(key) + val bucket = buckets[index] + // Traverse bucket and remove key-value pair from it + for (pair in bucket) { + if (pair.key == key) { + bucket.remove(pair) + size-- + break + } + } + } + + /* Expand hash table */ + fun extend() { + // Temporarily store the original hash table + val bucketsTmp = buckets + // Initialize expanded new hash table + capacity *= extendRatio + // mutablelist has no fixed size + buckets = mutableListOf() + for (i in 0..() + for (pair in bucket) { + val k = pair.key + val v = pair._val + res.add("$k -> $v") + } + println(res) + } + } + } ``` === "Ruby" ```ruby title="hash_map_chaining.rb" - [class]{HashMapChaining}-[func]{} - ``` + ### Hash map with chaining ### + class HashMapChaining + ### Constructor ### + def initialize + @size = 0 # Number of key-value pairs + @capacity = 4 # Hash table capacity + @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion + @extend_ratio = 2 # Expansion multiplier + @buckets = Array.new(@capacity) { [] } # Bucket array + end -=== "Zig" + ### Hash function ### + def hash_func(key) + key % @capacity + end - ```zig title="hash_map_chaining.zig" - [class]{HashMapChaining}-[func]{} + ### Load factor ### + def load_factor + @size / @capacity + end + + ### Query operation ### + def get(key) + index = hash_func(key) + bucket = @buckets[index] + # Traverse bucket, if key is found, return corresponding val + for pair in bucket + return pair.val if pair.key == key + end + # Return nil if key not found + nil + end + + ### Add operation ### + def put(key, val) + # When load factor exceeds threshold, perform expansion + extend if load_factor > @load_thres + index = hash_func(key) + bucket = @buckets[index] + # Traverse bucket, if specified key is encountered, update corresponding val and return + for pair in bucket + if pair.key == key + pair.val = val + return + end + end + # If key does not exist, append key-value pair to the end + pair = Pair.new(key, val) + bucket << pair + @size += 1 + end + + ### Delete operation ### + def remove(key) + index = hash_func(key) + bucket = @buckets[index] + # Traverse bucket and remove key-value pair from it + for pair in bucket + if pair.key == key + bucket.delete(pair) + @size -= 1 + break + end + end + end + + ### Expand hash table ### + def extend + # Temporarily store original hash table + buckets = @buckets + # Initialize expanded new hash table + @capacity *= @extend_ratio + @buckets = Array.new(@capacity) { [] } + @size = 0 + # Move key-value pairs from original hash table to new hash table + for bucket in buckets + for pair in bucket + put(pair.key, pair.val) + end + end + end + + ### Print hash table ### + def print + for bucket in @buckets + res = [] + for pair in bucket + res << "#{pair.key} -> #{pair.val}" + end + pp res + end + end + end ``` It's worth noting that when the linked list is very long, the query efficiency $O(n)$ is poor. **In this case, the list can be converted to an "AVL tree" or "Red-Black tree"** to optimize the time complexity of the query operation to $O(\log n)$. -## 6.2.2   Open addressing +## 6.2.2   Open Addressing -Open addressing does not introduce additional data structures but instead handles hash collisions through "multiple probing". The probing methods mainly include linear probing, quadratic probing, and double hashing. +Open addressing does not introduce additional data structures but instead handles hash collisions through "multiple probes". The probing methods mainly include linear probing, quadratic probing, and double hashing. Let's use linear probing as an example to introduce the mechanism of open addressing hash tables. -### 1.   Linear probing +### 1.   Linear Probing -Linear probing uses a fixed-step linear search for probing, differing from ordinary hash tables. +Linear probing uses a fixed-step linear search for probing, and its operation method differs from ordinary hash tables. -- **Inserting Elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element. -- **Searching for Elements**: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`. +- **Inserting elements**: Calculate the bucket index using the hash function. If the bucket already contains an element, linearly traverse forward from the conflict position (usually with a step size of $1$) until an empty bucket is found, then insert the element. +- **Searching for elements**: If a hash collision is encountered, use the same step size to linearly traverse forward until the corresponding element is found and return `value`; if an empty bucket is encountered, it means the target element is not in the hash table, so return `None`. Figure 6-6 shows the distribution of key-value pairs in an open addressing (linear probing) hash table. According to this hash function, keys with the same last two digits will be mapped to the same bucket. Through linear probing, they are stored sequentially in that bucket and the buckets below it. @@ -441,9 +1538,9 @@ Figure 6-6 shows the distribution of key-value pairs in an open addressing (line

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

-However, **linear probing is prone to create "clustering"**. Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting the growth of clustering at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations. +However, **linear probing is prone to create "clustering"**. Specifically, the longer the continuously occupied positions in the array, the greater the probability of hash collisions occurring in these continuous positions, further promoting clustering growth at that position, forming a vicious cycle, and ultimately leading to degraded efficiency of insertion, deletion, query, and update operations. -It's important to note that **we cannot directly delete elements in an open addressing hash table**. Deleting an element creates an empty bucket `None` in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7. +It's important to note that **we cannot directly delete elements in an open addressing hash table**. Deleting an element creates an empty bucket `None` in the array. When searching for elements, if linear probing encounters this empty bucket, it will return, making the elements below this empty bucket inaccessible. The program may incorrectly assume these elements do not exist, as shown in Figure 6-7. ![Query issues caused by deletion in open addressing](hash_collision.assets/hash_table_open_addressing_deletion.png){ class="animation-figure" } @@ -451,17 +1548,17 @@ It's important to note that **we cannot directly delete elements in an open addr To solve this problem, we can adopt the lazy deletion mechanism: instead of directly removing elements from the hash table, **use a constant `TOMBSTONE` to mark the bucket**. In this mechanism, both `None` and `TOMBSTONE` represent empty buckets and can hold key-value pairs. However, when linear probing encounters `TOMBSTONE`, it should continue traversing since there may still be key-value pairs below it. -However, **lazy deletion may accelerate the performance degradation of the hash table**. Every deletion operation produces a delete mark, and as `TOMBSTONE` increases, the search time will also increase because linear probing may need to skip multiple `TOMBSTONE` to find the target element. +However, **lazy deletion may accelerate the performance degradation of the hash table**. Every deletion operation produces a deletion mark, and as `TOMBSTONE` increases, the search time will also increase because linear probing may need to skip multiple `TOMBSTONE` to find the target element. -To address this, consider recording the index of the first encountered `TOMBSTONE` during linear probing and swapping the positions of the searched target element with that `TOMBSTONE`. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency. +To address this, consider recording the index of the first encountered `TOMBSTONE` during linear probing and swapping the searched target element with that `TOMBSTONE`. The benefit of doing this is that each time an element is queried or added, the element will be moved to a bucket closer to its ideal position (the starting point of probing), thereby optimizing query efficiency. -The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a "circular array,". When going beyond the end of the array, we return to the beginning and continue traversing. +The code below implements an open addressing (linear probing) hash table with lazy deletion. To make better use of the hash table space, we treat the hash table as a "circular array". When going beyond the end of the array, we return to the beginning and continue traversing. === "Python" ```python title="hash_map_open_addressing.py" class HashMapOpenAddressing: - """Open addressing hash table""" + """Hash table with open addressing""" def __init__(self): """Constructor""" @@ -470,7 +1567,7 @@ The code below implements an open addressing (linear probing) hash table with la self.load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion self.extend_ratio = 2 # Expansion multiplier self.buckets: list[Pair | None] = [None] * self.capacity # Bucket array - self.TOMBSTONE = Pair(-1, "-1") # Removal mark + self.TOMBSTONE = Pair(-1, "-1") # Removal marker def hash_func(self, key: int) -> int: """Hash function""" @@ -481,70 +1578,70 @@ The code below implements an open addressing (linear probing) hash table with la return self.size / self.capacity def find_bucket(self, key: int) -> int: - """Search for the bucket index corresponding to key""" + """Search for bucket index corresponding to key""" index = self.hash_func(key) first_tombstone = -1 # Linear probing, break when encountering an empty bucket while self.buckets[index] is not None: - # If the key is encountered, return the corresponding bucket index + # If key is encountered, return the corresponding bucket index if self.buckets[index].key == key: - # If a removal mark was encountered earlier, move the key-value pair to that index + # If a removal marker was encountered before, move the key-value pair to that index if first_tombstone != -1: self.buckets[first_tombstone] = self.buckets[index] self.buckets[index] = self.TOMBSTONE return first_tombstone # Return the moved bucket index return index # Return bucket index - # Record the first encountered removal mark + # Record the first removal marker encountered if first_tombstone == -1 and self.buckets[index] is self.TOMBSTONE: first_tombstone = index - # Calculate the bucket index, return to the head if exceeding the tail + # Calculate bucket index, wrap around to the head if past the tail index = (index + 1) % self.capacity - # If the key does not exist, return the index of the insertion point + # If key does not exist, return the index for insertion return index if first_tombstone == -1 else first_tombstone def get(self, key: int) -> str: """Query operation""" - # Search for the bucket index corresponding to key + # Search for bucket index corresponding to key index = self.find_bucket(key) - # If the key-value pair is found, return the corresponding val + # If key-value pair is found, return corresponding val if self.buckets[index] not in [None, self.TOMBSTONE]: return self.buckets[index].val - # If the key-value pair does not exist, return None + # If key-value pair does not exist, return None return None def put(self, key: int, val: str): """Add operation""" - # When the load factor exceeds the threshold, perform expansion + # When load factor exceeds threshold, perform expansion if self.load_factor() > self.load_thres: self.extend() - # Search for the bucket index corresponding to key + # Search for bucket index corresponding to key index = self.find_bucket(key) - # If the key-value pair is found, overwrite val and return + # If key-value pair is found, overwrite val and return if self.buckets[index] not in [None, self.TOMBSTONE]: self.buckets[index].val = val return - # If the key-value pair does not exist, add the key-value pair + # If key-value pair does not exist, add the key-value pair self.buckets[index] = Pair(key, val) self.size += 1 def remove(self, key: int): """Remove operation""" - # Search for the bucket index corresponding to key + # Search for bucket index corresponding to key index = self.find_bucket(key) - # If the key-value pair is found, cover it with a removal mark + # If key-value pair is found, overwrite it with removal marker if self.buckets[index] not in [None, self.TOMBSTONE]: self.buckets[index] = self.TOMBSTONE self.size -= 1 def extend(self): - """Extend hash table""" + """Expand hash table""" # Temporarily store the original hash table buckets_tmp = self.buckets - # Initialize the extended new hash table + # Initialize expanded new hash table self.capacity *= self.extend_ratio self.buckets = [None] * self.capacity self.size = 0 - # Move key-value pairs from the original hash table to the new hash table + # Move key-value pairs from original hash table to new hash table for pair in buckets_tmp: if pair not in [None, self.TOMBSTONE]: self.put(pair.key, pair.val) @@ -563,7 +1660,7 @@ The code below implements an open addressing (linear probing) hash table with la === "C++" ```cpp title="hash_map_open_addressing.cpp" - /* Open addressing hash table */ + /* Hash table with open addressing */ class HashMapOpenAddressing { private: int size; // Number of key-value pairs @@ -571,7 +1668,7 @@ The code below implements an open addressing (linear probing) hash table with la const double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion const int extendRatio = 2; // Expansion multiplier vector buckets; // Bucket array - Pair *TOMBSTONE = new Pair(-1, "-1"); // Removal mark + Pair *TOMBSTONE = new Pair(-1, "-1"); // Removal marker public: /* Constructor */ @@ -598,15 +1695,15 @@ The code below implements an open addressing (linear probing) hash table with la return (double)size / capacity; } - /* Search for the bucket index corresponding to key */ + /* Search for bucket index corresponding to key */ int findBucket(int key) { int index = hashFunc(key); int firstTombstone = -1; // Linear probing, break when encountering an empty bucket while (buckets[index] != nullptr) { - // If the key is encountered, return the corresponding bucket index + // If key is encountered, return the corresponding bucket index if (buckets[index]->key == key) { - // If a removal mark was encountered earlier, move the key-value pair to that index + // If a removal marker was encountered before, move the key-value pair to that index if (firstTombstone != -1) { buckets[firstTombstone] = buckets[index]; buckets[index] = TOMBSTONE; @@ -614,52 +1711,52 @@ The code below implements an open addressing (linear probing) hash table with la } return index; // Return bucket index } - // Record the first encountered removal mark + // Record the first removal marker encountered if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { firstTombstone = index; } - // Calculate the bucket index, return to the head if exceeding the tail + // Calculate bucket index, wrap around to the head if past the tail index = (index + 1) % capacity; } - // If the key does not exist, return the index of the insertion point + // If key does not exist, return the index for insertion return firstTombstone == -1 ? index : firstTombstone; } /* Query operation */ string get(int key) { - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, return the corresponding val + // If key-value pair is found, return corresponding val if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { return buckets[index]->val; } - // If key-value pair does not exist, return an empty string + // Return empty string if key-value pair does not exist return ""; } /* Add operation */ void put(int key, string val) { - // When the load factor exceeds the threshold, perform expansion + // When load factor exceeds threshold, perform expansion if (loadFactor() > loadThres) { extend(); } - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, overwrite val and return + // If key-value pair is found, overwrite val and return if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { buckets[index]->val = val; return; } - // If the key-value pair does not exist, add the key-value pair + // If key-value pair does not exist, add the key-value pair buckets[index] = new Pair(key, val); size++; } /* Remove operation */ void remove(int key) { - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, cover it with a removal mark + // If key-value pair is found, overwrite it with removal marker if (buckets[index] != nullptr && buckets[index] != TOMBSTONE) { delete buckets[index]; buckets[index] = TOMBSTONE; @@ -667,15 +1764,15 @@ The code below implements an open addressing (linear probing) hash table with la } } - /* Extend hash table */ + /* Expand hash table */ void extend() { // Temporarily store the original hash table vector bucketsTmp = buckets; - // Initialize the extended new hash table + // Initialize expanded new hash table capacity *= extendRatio; buckets = vector(capacity, nullptr); size = 0; - // Move key-value pairs from the original hash table to the new hash table + // Move key-value pairs from original hash table to new hash table for (Pair *pair : bucketsTmp) { if (pair != nullptr && pair != TOMBSTONE) { put(pair->key, pair->val); @@ -702,14 +1799,14 @@ The code below implements an open addressing (linear probing) hash table with la === "Java" ```java title="hash_map_open_addressing.java" - /* Open addressing hash table */ + /* Hash table with open addressing */ class HashMapOpenAddressing { private int size; // Number of key-value pairs private int capacity = 4; // Hash table capacity private final double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion private final int extendRatio = 2; // Expansion multiplier private Pair[] buckets; // Bucket array - private final Pair TOMBSTONE = new Pair(-1, "-1"); // Removal mark + private final Pair TOMBSTONE = new Pair(-1, "-1"); // Removal marker /* Constructor */ public HashMapOpenAddressing() { @@ -727,15 +1824,15 @@ The code below implements an open addressing (linear probing) hash table with la return (double) size / capacity; } - /* Search for the bucket index corresponding to key */ + /* Search for bucket index corresponding to key */ private int findBucket(int key) { int index = hashFunc(key); int firstTombstone = -1; // Linear probing, break when encountering an empty bucket while (buckets[index] != null) { - // If the key is encountered, return the corresponding bucket index + // If key is encountered, return the corresponding bucket index if (buckets[index].key == key) { - // If a removal mark was encountered earlier, move the key-value pair to that index + // If a removal marker was encountered before, move the key-value pair to that index if (firstTombstone != -1) { buckets[firstTombstone] = buckets[index]; buckets[index] = TOMBSTONE; @@ -743,67 +1840,67 @@ The code below implements an open addressing (linear probing) hash table with la } return index; // Return bucket index } - // Record the first encountered removal mark + // Record the first removal marker encountered if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { firstTombstone = index; } - // Calculate the bucket index, return to the head if exceeding the tail + // Calculate bucket index, wrap around to the head if past the tail index = (index + 1) % capacity; } - // If the key does not exist, return the index of the insertion point + // If key does not exist, return the index for insertion return firstTombstone == -1 ? index : firstTombstone; } /* Query operation */ public String get(int key) { - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, return the corresponding val + // If key-value pair is found, return corresponding val if (buckets[index] != null && buckets[index] != TOMBSTONE) { return buckets[index].val; } - // If the key-value pair does not exist, return null + // If key-value pair does not exist, return null return null; } /* Add operation */ public void put(int key, String val) { - // When the load factor exceeds the threshold, perform expansion + // When load factor exceeds threshold, perform expansion if (loadFactor() > loadThres) { extend(); } - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, overwrite val and return + // If key-value pair is found, overwrite val and return if (buckets[index] != null && buckets[index] != TOMBSTONE) { buckets[index].val = val; return; } - // If the key-value pair does not exist, add the key-value pair + // If key-value pair does not exist, add the key-value pair buckets[index] = new Pair(key, val); size++; } /* Remove operation */ public void remove(int key) { - // Search for the bucket index corresponding to key + // Search for bucket index corresponding to key int index = findBucket(key); - // If the key-value pair is found, cover it with a removal mark + // If key-value pair is found, overwrite it with removal marker if (buckets[index] != null && buckets[index] != TOMBSTONE) { buckets[index] = TOMBSTONE; size--; } } - /* Extend hash table */ + /* Expand hash table */ private void extend() { // Temporarily store the original hash table Pair[] bucketsTmp = buckets; - // Initialize the extended new hash table + // Initialize expanded new hash table capacity *= extendRatio; buckets = new Pair[capacity]; size = 0; - // Move key-value pairs from the original hash table to the new hash table + // Move key-value pairs from original hash table to new hash table for (Pair pair : bucketsTmp) { if (pair != null && pair != TOMBSTONE) { put(pair.key, pair.val); @@ -829,76 +1926,1364 @@ The code below implements an open addressing (linear probing) hash table with la === "C#" ```csharp title="hash_map_open_addressing.cs" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + int size; // Number of key-value pairs + int capacity = 4; // Hash table capacity + double loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion + int extendRatio = 2; // Expansion multiplier + Pair[] buckets; // Bucket array + Pair TOMBSTONE = new(-1, "-1"); // Removal marker + + /* Constructor */ + public HashMapOpenAddressing() { + size = 0; + buckets = new Pair[capacity]; + } + + /* Hash function */ + int HashFunc(int key) { + return key % capacity; + } + + /* Load factor */ + double LoadFactor() { + return (double)size / capacity; + } + + /* Search for bucket index corresponding to key */ + int FindBucket(int key) { + int index = HashFunc(key); + int firstTombstone = -1; + // Linear probing, break when encountering an empty bucket + while (buckets[index] != null) { + // If key is encountered, return the corresponding bucket index + if (buckets[index].key == key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index]; + buckets[index] = TOMBSTONE; + return firstTombstone; // Return the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % capacity; + } + // If key does not exist, return the index for insertion + return firstTombstone == -1 ? index : firstTombstone; + } + + /* Query operation */ + public string? Get(int key) { + // Search for bucket index corresponding to key + int index = FindBucket(key); + // If key-value pair is found, return corresponding val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index].val; + } + // If key-value pair does not exist, return null + return null; + } + + /* Add operation */ + public void Put(int key, string val) { + // When load factor exceeds threshold, perform expansion + if (LoadFactor() > loadThres) { + Extend(); + } + // Search for bucket index corresponding to key + int index = FindBucket(key); + // If key-value pair is found, overwrite val and return + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index].val = val; + return; + } + // If key-value pair does not exist, add the key-value pair + buckets[index] = new Pair(key, val); + size++; + } + + /* Remove operation */ + public void Remove(int key) { + // Search for bucket index corresponding to key + int index = FindBucket(key); + // If key-value pair is found, overwrite it with removal marker + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE; + size--; + } + } + + /* Expand hash table */ + void Extend() { + // Temporarily store the original hash table + Pair[] bucketsTmp = buckets; + // Initialize expanded new hash table + capacity *= extendRatio; + buckets = new Pair[capacity]; + size = 0; + // Move key-value pairs from original hash table to new hash table + foreach (Pair pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + Put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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" - [class]{hashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + type hashMapOpenAddressing struct { + size int // Number of key-value pairs + capacity int // Hash table capacity + loadThres float64 // Load factor threshold for triggering expansion + extendRatio int // Expansion multiplier + buckets []*pair // Bucket array + TOMBSTONE *pair // Removal marker + } + + /* Constructor */ + func newHashMapOpenAddressing() *hashMapOpenAddressing { + return &hashMapOpenAddressing{ + size: 0, + capacity: 4, + loadThres: 2.0 / 3.0, + extendRatio: 2, + buckets: make([]*pair, 4), + TOMBSTONE: &pair{-1, "-1"}, + } + } + + /* Hash function */ + func (h *hashMapOpenAddressing) hashFunc(key int) int { + return key % h.capacity // Calculate hash value based on key + } + + /* Load factor */ + func (h *hashMapOpenAddressing) loadFactor() float64 { + return float64(h.size) / float64(h.capacity) // Calculate current load factor + } + + /* Search for bucket index corresponding to key */ + func (h *hashMapOpenAddressing) findBucket(key int) int { + index := h.hashFunc(key) // Get initial index + firstTombstone := -1 // Record position of first TOMBSTONE encountered + for h.buckets[index] != nil { + if h.buckets[index].key == key { + if firstTombstone != -1 { + // If a removal marker was encountered before, move the key-value pair to that index + h.buckets[firstTombstone] = h.buckets[index] + h.buckets[index] = h.TOMBSTONE + return firstTombstone // Return the moved bucket index + } + return index // Return found index + } + if firstTombstone == -1 && h.buckets[index] == h.TOMBSTONE { + firstTombstone = index // Record position of first deletion marker encountered + } + index = (index + 1) % h.capacity // Linear probing, wrap around to head if past tail + } + // If key does not exist, return the index for insertion + if firstTombstone != -1 { + return firstTombstone + } + return index + } + + /* Query operation */ + func (h *hashMapOpenAddressing) get(key int) string { + index := h.findBucket(key) // Search for bucket index corresponding to key + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + return h.buckets[index].val // If key-value pair is found, return corresponding val + } + return "" // Return "" if key-value pair does not exist + } + + /* Add operation */ + func (h *hashMapOpenAddressing) put(key int, val string) { + if h.loadFactor() > h.loadThres { + h.extend() // When load factor exceeds threshold, perform expansion + } + index := h.findBucket(key) // Search for bucket index corresponding to key + if h.buckets[index] == nil || h.buckets[index] == h.TOMBSTONE { + h.buckets[index] = &pair{key, val} // If key-value pair does not exist, add the key-value pair + h.size++ + } else { + h.buckets[index].val = val // If key-value pair found, overwrite val + } + } + + /* Remove operation */ + func (h *hashMapOpenAddressing) remove(key int) { + index := h.findBucket(key) // Search for bucket index corresponding to key + if h.buckets[index] != nil && h.buckets[index] != h.TOMBSTONE { + h.buckets[index] = h.TOMBSTONE // If key-value pair is found, overwrite it with removal marker + h.size-- + } + } + + /* Expand hash table */ + func (h *hashMapOpenAddressing) extend() { + oldBuckets := h.buckets // Temporarily store the original hash table + h.capacity *= h.extendRatio // Update capacity + h.buckets = make([]*pair, h.capacity) // Initialize expanded new hash table + h.size = 0 // Reset size + // Move key-value pairs from original hash table to new hash table + for _, pair := range oldBuckets { + if pair != nil && pair != h.TOMBSTONE { + h.put(pair.key, pair.val) + } + } + } + + /* Print hash table */ + func (h *hashMapOpenAddressing) print() { + for _, pair := range h.buckets { + if pair == nil { + fmt.Println("nil") + } else if pair == h.TOMBSTONE { + fmt.Println("TOMBSTONE") + } else { + fmt.Printf("%d -> %s\n", pair.key, pair.val) + } + } + } ``` === "Swift" ```swift title="hash_map_open_addressing.swift" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + var size: Int // Number of key-value pairs + var capacity: Int // Hash table capacity + var loadThres: Double // Load factor threshold for triggering expansion + var extendRatio: Int // Expansion multiplier + var buckets: [Pair?] // Bucket array + var TOMBSTONE: Pair // Removal marker + + /* Constructor */ + init() { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = Array(repeating: nil, count: capacity) + TOMBSTONE = Pair(key: -1, val: "-1") + } + + /* Hash function */ + func hashFunc(key: Int) -> Int { + key % capacity + } + + /* Load factor */ + func loadFactor() -> Double { + Double(size) / Double(capacity) + } + + /* Search for bucket index corresponding to key */ + func findBucket(key: Int) -> Int { + var index = hashFunc(key: key) + var firstTombstone = -1 + // Linear probing, break when encountering an empty bucket + while buckets[index] != nil { + // If key is encountered, return the corresponding bucket index + if buckets[index]!.key == key { + // If a removal marker was encountered before, move the key-value pair to that index + if firstTombstone != -1 { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // Return the moved bucket index + } + return index // Return bucket index + } + // Record the first removal marker encountered + if firstTombstone == -1 && buckets[index] == TOMBSTONE { + firstTombstone = index + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % capacity + } + // If key does not exist, return the index for insertion + return firstTombstone == -1 ? index : firstTombstone + } + + /* Query operation */ + func get(key: Int) -> String? { + // Search for bucket index corresponding to key + let index = findBucket(key: key) + // If key-value pair is found, return corresponding val + if buckets[index] != nil, buckets[index] != TOMBSTONE { + return buckets[index]!.val + } + // If key-value pair does not exist, return null + return nil + } + + /* Add operation */ + func put(key: Int, val: String) { + // When load factor exceeds threshold, perform expansion + if loadFactor() > loadThres { + extend() + } + // Search for bucket index corresponding to key + let index = findBucket(key: key) + // If key-value pair is found, overwrite val and return + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index]!.val = val + return + } + // If key-value pair does not exist, add the key-value pair + buckets[index] = Pair(key: key, val: val) + size += 1 + } + + /* Remove operation */ + func remove(key: Int) { + // Search for bucket index corresponding to key + let index = findBucket(key: key) + // If key-value pair is found, overwrite it with removal marker + if buckets[index] != nil, buckets[index] != TOMBSTONE { + buckets[index] = TOMBSTONE + size -= 1 + } + } + + /* Expand hash table */ + func extend() { + // Temporarily store the original hash table + let bucketsTmp = buckets + // Initialize expanded new hash table + capacity *= extendRatio + buckets = Array(repeating: nil, count: capacity) + size = 0 + // Move key-value pairs from original hash table to new hash table + for pair in bucketsTmp { + if let pair, pair != TOMBSTONE { + put(key: pair.key, val: pair.val) + } + } + } + + /* Print hash table */ + 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}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + #size; // Number of key-value pairs + #capacity; // Hash table capacity + #loadThres; // Load factor threshold for triggering expansion + #extendRatio; // Expansion multiplier + #buckets; // Bucket array + #TOMBSTONE; // Removal marker + + /* Constructor */ + constructor() { + this.#size = 0; // Number of key-value pairs + this.#capacity = 4; // Hash table capacity + this.#loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion + this.#extendRatio = 2; // Expansion multiplier + this.#buckets = Array(this.#capacity).fill(null); // Bucket array + this.#TOMBSTONE = new Pair(-1, '-1'); // Removal marker + } + + /* Hash function */ + #hashFunc(key) { + return key % this.#capacity; + } + + /* Load factor */ + #loadFactor() { + return this.#size / this.#capacity; + } + + /* Search for bucket index corresponding to key */ + #findBucket(key) { + let index = this.#hashFunc(key); + let firstTombstone = -1; + // Linear probing, break when encountering an empty bucket + while (this.#buckets[index] !== null) { + // If key is encountered, return the corresponding bucket index + if (this.#buckets[index].key === key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone !== -1) { + this.#buckets[firstTombstone] = this.#buckets[index]; + this.#buckets[index] = this.#TOMBSTONE; + return firstTombstone; // Return the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if ( + firstTombstone === -1 && + this.#buckets[index] === this.#TOMBSTONE + ) { + firstTombstone = index; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % this.#capacity; + } + // If key does not exist, return the index for insertion + return firstTombstone === -1 ? index : firstTombstone; + } + + /* Query operation */ + get(key) { + // Search for bucket index corresponding to key + const index = this.#findBucket(key); + // If key-value pair is found, return corresponding val + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + return this.#buckets[index].val; + } + // If key-value pair does not exist, return null + return null; + } + + /* Add operation */ + put(key, val) { + // When load factor exceeds threshold, perform expansion + if (this.#loadFactor() > this.#loadThres) { + this.#extend(); + } + // Search for bucket index corresponding to key + const index = this.#findBucket(key); + // If key-value pair is found, overwrite val and return + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index].val = val; + return; + } + // If key-value pair does not exist, add the key-value pair + this.#buckets[index] = new Pair(key, val); + this.#size++; + } + + /* Remove operation */ + remove(key) { + // Search for bucket index corresponding to key + const index = this.#findBucket(key); + // If key-value pair is found, overwrite it with removal marker + if ( + this.#buckets[index] !== null && + this.#buckets[index] !== this.#TOMBSTONE + ) { + this.#buckets[index] = this.#TOMBSTONE; + this.#size--; + } + } + + /* Expand hash table */ + #extend() { + // Temporarily store the original hash table + const bucketsTmp = this.#buckets; + // Initialize expanded new hash table + this.#capacity *= this.#extendRatio; + this.#buckets = Array(this.#capacity).fill(null); + this.#size = 0; + // Move key-value pairs from original hash table to new hash table + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.#TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + print() { + for (const pair of this.#buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.#TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } ``` === "TS" ```typescript title="hash_map_open_addressing.ts" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + private size: number; // Number of key-value pairs + private capacity: number; // Hash table capacity + private loadThres: number; // Load factor threshold for triggering expansion + private extendRatio: number; // Expansion multiplier + private buckets: Array; // Bucket array + private TOMBSTONE: Pair; // Removal marker + + /* Constructor */ + constructor() { + this.size = 0; // Number of key-value pairs + this.capacity = 4; // Hash table capacity + this.loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion + this.extendRatio = 2; // Expansion multiplier + this.buckets = Array(this.capacity).fill(null); // Bucket array + this.TOMBSTONE = new Pair(-1, '-1'); // Removal marker + } + + /* Hash function */ + private hashFunc(key: number): number { + return key % this.capacity; + } + + /* Load factor */ + private loadFactor(): number { + return this.size / this.capacity; + } + + /* Search for bucket index corresponding to key */ + private findBucket(key: number): number { + let index = this.hashFunc(key); + let firstTombstone = -1; + // Linear probing, break when encountering an empty bucket + while (this.buckets[index] !== null) { + // If key is encountered, return the corresponding bucket index + if (this.buckets[index]!.key === key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone !== -1) { + this.buckets[firstTombstone] = this.buckets[index]; + this.buckets[index] = this.TOMBSTONE; + return firstTombstone; // Return the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if ( + firstTombstone === -1 && + this.buckets[index] === this.TOMBSTONE + ) { + firstTombstone = index; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % this.capacity; + } + // If key does not exist, return the index for insertion + return firstTombstone === -1 ? index : firstTombstone; + } + + /* Query operation */ + get(key: number): string | null { + // Search for bucket index corresponding to key + const index = this.findBucket(key); + // If key-value pair is found, return corresponding val + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + return this.buckets[index]!.val; + } + // If key-value pair does not exist, return null + return null; + } + + /* Add operation */ + put(key: number, val: string): void { + // When load factor exceeds threshold, perform expansion + if (this.loadFactor() > this.loadThres) { + this.extend(); + } + // Search for bucket index corresponding to key + const index = this.findBucket(key); + // If key-value pair is found, overwrite val and return + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index]!.val = val; + return; + } + // If key-value pair does not exist, add the key-value pair + this.buckets[index] = new Pair(key, val); + this.size++; + } + + /* Remove operation */ + remove(key: number): void { + // Search for bucket index corresponding to key + const index = this.findBucket(key); + // If key-value pair is found, overwrite it with removal marker + if ( + this.buckets[index] !== null && + this.buckets[index] !== this.TOMBSTONE + ) { + this.buckets[index] = this.TOMBSTONE; + this.size--; + } + } + + /* Expand hash table */ + private extend(): void { + // Temporarily store the original hash table + const bucketsTmp = this.buckets; + // Initialize expanded new hash table + this.capacity *= this.extendRatio; + this.buckets = Array(this.capacity).fill(null); + this.size = 0; + // Move key-value pairs from original hash table to new hash table + for (const pair of bucketsTmp) { + if (pair !== null && pair !== this.TOMBSTONE) { + this.put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + print(): void { + for (const pair of this.buckets) { + if (pair === null) { + console.log('null'); + } else if (pair === this.TOMBSTONE) { + console.log('TOMBSTONE'); + } else { + console.log(pair.key + ' -> ' + pair.val); + } + } + } + } ``` === "Dart" ```dart title="hash_map_open_addressing.dart" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + late int _size; // Number of key-value pairs + int _capacity = 4; // Hash table capacity + double _loadThres = 2.0 / 3.0; // Load factor threshold for triggering expansion + int _extendRatio = 2; // Expansion multiplier + late List _buckets; // Bucket array + Pair _TOMBSTONE = Pair(-1, "-1"); // Removal marker + + /* Constructor */ + HashMapOpenAddressing() { + _size = 0; + _buckets = List.generate(_capacity, (index) => null); + } + + /* Hash function */ + int hashFunc(int key) { + return key % _capacity; + } + + /* Load factor */ + double loadFactor() { + return _size / _capacity; + } + + /* Search for bucket index corresponding to key */ + int findBucket(int key) { + int index = hashFunc(key); + int firstTombstone = -1; + // Linear probing, break when encountering an empty bucket + while (_buckets[index] != null) { + // If key is encountered, return the corresponding bucket index + if (_buckets[index]!.key == key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone != -1) { + _buckets[firstTombstone] = _buckets[index]; + _buckets[index] = _TOMBSTONE; + return firstTombstone; // Return the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if (firstTombstone == -1 && _buckets[index] == _TOMBSTONE) { + firstTombstone = index; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % _capacity; + } + // If key does not exist, return the index for insertion + return firstTombstone == -1 ? index : firstTombstone; + } + + /* Query operation */ + String? get(int key) { + // Search for bucket index corresponding to key + int index = findBucket(key); + // If key-value pair is found, return corresponding val + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + return _buckets[index]!.val; + } + // If key-value pair does not exist, return null + return null; + } + + /* Add operation */ + void put(int key, String val) { + // When load factor exceeds threshold, perform expansion + if (loadFactor() > _loadThres) { + extend(); + } + // Search for bucket index corresponding to key + int index = findBucket(key); + // If key-value pair is found, overwrite val and return + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index]!.val = val; + return; + } + // If key-value pair does not exist, add the key-value pair + _buckets[index] = new Pair(key, val); + _size++; + } + + /* Remove operation */ + void remove(int key) { + // Search for bucket index corresponding to key + int index = findBucket(key); + // If key-value pair is found, overwrite it with removal marker + if (_buckets[index] != null && _buckets[index] != _TOMBSTONE) { + _buckets[index] = _TOMBSTONE; + _size--; + } + } + + /* Expand hash table */ + void extend() { + // Temporarily store the original hash table + List bucketsTmp = _buckets; + // Initialize expanded new hash table + _capacity *= _extendRatio; + _buckets = List.generate(_capacity, (index) => null); + _size = 0; + // Move key-value pairs from original hash table to new hash table + for (Pair? pair in bucketsTmp) { + if (pair != null && pair != _TOMBSTONE) { + put(pair.key, pair.val); + } + } + } + + /* Print hash table */ + 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" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + struct HashMapOpenAddressing { + size: usize, // Number of key-value pairs + capacity: usize, // Hash table capacity + load_thres: f64, // Load factor threshold for triggering expansion + extend_ratio: usize, // Expansion multiplier + buckets: Vec>, // Bucket array + TOMBSTONE: Option, // Removal marker + } + + impl HashMapOpenAddressing { + /* Constructor */ + 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(), + }), + } + } + + /* Hash function */ + fn hash_func(&self, key: i32) -> usize { + (key % self.capacity as i32) as usize + } + + /* Load factor */ + fn load_factor(&self) -> f64 { + self.size as f64 / self.capacity as f64 + } + + /* Search for bucket index corresponding to key */ + fn find_bucket(&mut self, key: i32) -> usize { + let mut index = self.hash_func(key); + let mut first_tombstone = -1; + // Linear probing, break when encountering an empty bucket + while self.buckets[index].is_some() { + // If key is found, return corresponding bucket index + if self.buckets[index].as_ref().unwrap().key == key { + // If deletion marker was encountered before, move key-value pair to that index + 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 the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if first_tombstone == -1 && self.buckets[index] == self.TOMBSTONE { + first_tombstone = index as i32; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % self.capacity; + } + // If key does not exist, return the index for insertion + if first_tombstone == -1 { + index + } else { + first_tombstone as usize + } + } + + /* Query operation */ + fn get(&mut self, key: i32) -> Option<&str> { + // Search for bucket index corresponding to key + let index = self.find_bucket(key); + // If key-value pair is found, return corresponding val + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + return self.buckets[index].as_ref().map(|pair| &pair.val as &str); + } + // If key-value pair does not exist, return null + None + } + + /* Add operation */ + fn put(&mut self, key: i32, val: String) { + // When load factor exceeds threshold, perform expansion + if self.load_factor() > self.load_thres { + self.extend(); + } + // Search for bucket index corresponding to key + let index = self.find_bucket(key); + // If key-value pair is found, overwrite val and return + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index].as_mut().unwrap().val = val; + return; + } + // If key-value pair does not exist, add the key-value pair + self.buckets[index] = Some(Pair { key, val }); + self.size += 1; + } + + /* Remove operation */ + fn remove(&mut self, key: i32) { + // Search for bucket index corresponding to key + let index = self.find_bucket(key); + // If key-value pair is found, overwrite it with removal marker + if self.buckets[index].is_some() && self.buckets[index] != self.TOMBSTONE { + self.buckets[index] = self.TOMBSTONE.clone(); + self.size -= 1; + } + } + + /* Expand hash table */ + fn extend(&mut self) { + // Temporarily store the original hash table + let buckets_tmp = self.buckets.clone(); + // Initialize expanded new hash table + self.capacity *= self.extend_ratio; + self.buckets = vec![None; self.capacity]; + self.size = 0; + + // Move key-value pairs from original hash table to new hash table + for pair in buckets_tmp { + if pair.is_none() || pair == self.TOMBSTONE { + continue; + } + let pair = pair.unwrap(); + + self.put(pair.key, pair.val); + } + } + /* Print hash table */ + 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]{} + /* Hash table with open addressing */ + typedef struct { + int size; // Number of key-value pairs + int capacity; // Hash table capacity + double loadThres; // Load factor threshold for triggering expansion + int extendRatio; // Expansion multiplier + Pair **buckets; // Bucket array + Pair *TOMBSTONE; // Removal marker + } HashMapOpenAddressing; + + /* Constructor */ + HashMapOpenAddressing *newHashMapOpenAddressing() { + HashMapOpenAddressing *hashMap = (HashMapOpenAddressing *)malloc(sizeof(HashMapOpenAddressing)); + hashMap->size = 0; + hashMap->capacity = 4; + hashMap->loadThres = 2.0 / 3.0; + hashMap->extendRatio = 2; + hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *)); + hashMap->TOMBSTONE = (Pair *)malloc(sizeof(Pair)); + hashMap->TOMBSTONE->key = -1; + hashMap->TOMBSTONE->val = "-1"; + + return hashMap; + } + + /* Destructor */ + void delHashMapOpenAddressing(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + free(pair->val); + free(pair); + } + } + free(hashMap->buckets); + free(hashMap->TOMBSTONE); + free(hashMap); + } + + /* Hash function */ + int hashFunc(HashMapOpenAddressing *hashMap, int key) { + return key % hashMap->capacity; + } + + /* Load factor */ + double loadFactor(HashMapOpenAddressing *hashMap) { + return (double)hashMap->size / (double)hashMap->capacity; + } + + /* Search for bucket index corresponding to key */ + int findBucket(HashMapOpenAddressing *hashMap, int key) { + int index = hashFunc(hashMap, key); + int firstTombstone = -1; + // Linear probing, break when encountering an empty bucket + while (hashMap->buckets[index] != NULL) { + // If key is encountered, return the corresponding bucket index + if (hashMap->buckets[index]->key == key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone != -1) { + hashMap->buckets[firstTombstone] = hashMap->buckets[index]; + hashMap->buckets[index] = hashMap->TOMBSTONE; + return firstTombstone; // Return the moved bucket index + } + return index; // Return bucket index + } + // Record the first removal marker encountered + if (firstTombstone == -1 && hashMap->buckets[index] == hashMap->TOMBSTONE) { + firstTombstone = index; + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % hashMap->capacity; + } + // If key does not exist, return the index for insertion + return firstTombstone == -1 ? index : firstTombstone; + } + + /* Query operation */ + char *get(HashMapOpenAddressing *hashMap, int key) { + // Search for bucket index corresponding to key + int index = findBucket(hashMap, key); + // If key-value pair is found, return corresponding val + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + return hashMap->buckets[index]->val; + } + // Return empty string if key-value pair does not exist + return ""; + } + + /* Add operation */ + void put(HashMapOpenAddressing *hashMap, int key, char *val) { + // When load factor exceeds threshold, perform expansion + if (loadFactor(hashMap) > hashMap->loadThres) { + extend(hashMap); + } + // Search for bucket index corresponding to key + int index = findBucket(hashMap, key); + // If key-value pair is found, overwrite val and return + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + free(hashMap->buckets[index]->val); + hashMap->buckets[index]->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(hashMap->buckets[index]->val, val); + hashMap->buckets[index]->val[strlen(val)] = '\0'; + return; + } + // If key-value pair does not exist, add the key-value pair + Pair *pair = (Pair *)malloc(sizeof(Pair)); + pair->key = key; + pair->val = (char *)malloc(sizeof(strlen(val) + 1)); + strcpy(pair->val, val); + pair->val[strlen(val)] = '\0'; + + hashMap->buckets[index] = pair; + hashMap->size++; + } + + /* Remove operation */ + void removeItem(HashMapOpenAddressing *hashMap, int key) { + // Search for bucket index corresponding to key + int index = findBucket(hashMap, key); + // If key-value pair is found, overwrite it with removal marker + if (hashMap->buckets[index] != NULL && hashMap->buckets[index] != hashMap->TOMBSTONE) { + Pair *pair = hashMap->buckets[index]; + free(pair->val); + free(pair); + hashMap->buckets[index] = hashMap->TOMBSTONE; + hashMap->size--; + } + } + + /* Expand hash table */ + void extend(HashMapOpenAddressing *hashMap) { + // Temporarily store the original hash table + Pair **bucketsTmp = hashMap->buckets; + int oldCapacity = hashMap->capacity; + // Initialize expanded new hash table + hashMap->capacity *= hashMap->extendRatio; + hashMap->buckets = (Pair **)calloc(hashMap->capacity, sizeof(Pair *)); + hashMap->size = 0; + // Move key-value pairs from original hash table to new hash table + for (int i = 0; i < oldCapacity; i++) { + Pair *pair = bucketsTmp[i]; + if (pair != NULL && pair != hashMap->TOMBSTONE) { + put(hashMap, pair->key, pair->val); + free(pair->val); + free(pair); + } + } + free(bucketsTmp); + } + + /* Print hash table */ + void print(HashMapOpenAddressing *hashMap) { + for (int i = 0; i < hashMap->capacity; i++) { + Pair *pair = hashMap->buckets[i]; + if (pair == NULL) { + printf("NULL\n"); + } else if (pair == hashMap->TOMBSTONE) { + printf("TOMBSTONE\n"); + } else { + printf("%d -> %s\n", pair->key, pair->val); + } + } + } ``` === "Kotlin" ```kotlin title="hash_map_open_addressing.kt" - [class]{HashMapOpenAddressing}-[func]{} + /* Hash table with open addressing */ + class HashMapOpenAddressing { + private var size: Int // Number of key-value pairs + private var capacity: Int // Hash table capacity + private val loadThres: Double // Load factor threshold for triggering expansion + private val extendRatio: Int // Expansion multiplier + private var buckets: Array // Bucket array + private val TOMBSTONE: Pair // Removal marker + + /* Constructor */ + init { + size = 0 + capacity = 4 + loadThres = 2.0 / 3.0 + extendRatio = 2 + buckets = arrayOfNulls(capacity) + TOMBSTONE = Pair(-1, "-1") + } + + /* Hash function */ + fun hashFunc(key: Int): Int { + return key % capacity + } + + /* Load factor */ + fun loadFactor(): Double { + return (size / capacity).toDouble() + } + + /* Search for bucket index corresponding to key */ + fun findBucket(key: Int): Int { + var index = hashFunc(key) + var firstTombstone = -1 + // Linear probing, break when encountering an empty bucket + while (buckets[index] != null) { + // If key is encountered, return the corresponding bucket index + if (buckets[index]?.key == key) { + // If a removal marker was encountered before, move the key-value pair to that index + if (firstTombstone != -1) { + buckets[firstTombstone] = buckets[index] + buckets[index] = TOMBSTONE + return firstTombstone // Return the moved bucket index + } + return index // Return bucket index + } + // Record the first removal marker encountered + if (firstTombstone == -1 && buckets[index] == TOMBSTONE) { + firstTombstone = index + } + // Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % capacity + } + // If key does not exist, return the index for insertion + return if (firstTombstone == -1) index else firstTombstone + } + + /* Query operation */ + fun get(key: Int): String? { + // Search for bucket index corresponding to key + val index = findBucket(key) + // If key-value pair is found, return corresponding val + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + return buckets[index]?._val + } + // If key-value pair does not exist, return null + return null + } + + /* Add operation */ + fun put(key: Int, _val: String) { + // When load factor exceeds threshold, perform expansion + if (loadFactor() > loadThres) { + extend() + } + // Search for bucket index corresponding to key + val index = findBucket(key) + // If key-value pair is found, overwrite val and return + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index]!!._val = _val + return + } + // If key-value pair does not exist, add the key-value pair + buckets[index] = Pair(key, _val) + size++ + } + + /* Remove operation */ + fun remove(key: Int) { + // Search for bucket index corresponding to key + val index = findBucket(key) + // If key-value pair is found, overwrite it with removal marker + if (buckets[index] != null && buckets[index] != TOMBSTONE) { + buckets[index] = TOMBSTONE + size-- + } + } + + /* Expand hash table */ + fun extend() { + // Temporarily store the original hash table + val bucketsTmp = buckets + // Initialize expanded new hash table + capacity *= extendRatio + buckets = arrayOfNulls(capacity) + size = 0 + // Move key-value pairs from original hash table to new hash table + for (pair in bucketsTmp) { + if (pair != null && pair != TOMBSTONE) { + put(pair.key, pair._val) + } + } + } + + /* Print hash table */ + fun print() { + for (pair in buckets) { + if (pair == null) { + println("null") + } else if (pair == TOMBSTONE) { + println("TOMESTOME") + } else { + println("${pair.key} -> ${pair._val}") + } + } + } + } ``` === "Ruby" ```ruby title="hash_map_open_addressing.rb" - [class]{HashMapOpenAddressing}-[func]{} + ### Hash map with open addressing ### + class HashMapOpenAddressing + TOMBSTONE = Pair.new(-1, '-1') # Removal marker + + ### Constructor ### + def initialize + @size = 0 # Number of key-value pairs + @capacity = 4 # Hash table capacity + @load_thres = 2.0 / 3.0 # Load factor threshold for triggering expansion + @extend_ratio = 2 # Expansion multiplier + @buckets = Array.new(@capacity) # Bucket array + end + + ### Hash function ### + def hash_func(key) + key % @capacity + end + + ### Load factor ### + def load_factor + @size / @capacity + end + + ### Search bucket index for key ### + def find_bucket(key) + index = hash_func(key) + first_tombstone = -1 + # Linear probing, break when encountering an empty bucket + while !@buckets[index].nil? + # If key is encountered, return the corresponding bucket index + if @buckets[index].key == key + # If a removal marker was encountered before, move the key-value pair to that index + if first_tombstone != -1 + @buckets[first_tombstone] = @buckets[index] + @buckets[index] = TOMBSTONE + return first_tombstone # Return the moved bucket index + end + return index # Return bucket index + end + # Record the first removal marker encountered + first_tombstone = index if first_tombstone == -1 && @buckets[index] == TOMBSTONE + # Calculate bucket index, wrap around to the head if past the tail + index = (index + 1) % @capacity + end + # If key does not exist, return the index for insertion + first_tombstone == -1 ? index : first_tombstone + end + + ### Query operation ### + def get(key) + # Search for bucket index corresponding to key + index = find_bucket(key) + # If key-value pair is found, return corresponding val + return @buckets[index].val unless [nil, TOMBSTONE].include?(@buckets[index]) + # Return nil if key-value pair does not exist + nil + end + + ### Add operation ### + def put(key, val) + # When load factor exceeds threshold, perform expansion + extend if load_factor > @load_thres + # Search for bucket index corresponding to key + index = find_bucket(key) + # If key-value pair found, overwrite val and return + unless [nil, TOMBSTONE].include?(@buckets[index]) + @buckets[index].val = val + return + end + # If key-value pair does not exist, add the key-value pair + @buckets[index] = Pair.new(key, val) + @size += 1 + end + + ### Delete operation ### + def remove(key) + # Search for bucket index corresponding to key + index = find_bucket(key) + # If key-value pair is found, overwrite it with removal marker + unless [nil, TOMBSTONE].include?(@buckets[index]) + @buckets[index] = TOMBSTONE + @size -= 1 + end + end + + ### Expand hash table ### + def extend + # Temporarily store the original hash table + buckets_tmp = @buckets + # Initialize expanded new hash table + @capacity *= @extend_ratio + @buckets = Array.new(@capacity) + @size = 0 + # Move key-value pairs from original hash table to new hash table + for pair in buckets_tmp + put(pair.key, pair.val) unless [nil, TOMBSTONE].include?(pair) + end + end + + ### Print hash table ### + def print + for pair in @buckets + if pair.nil? + puts "Nil" + elsif pair == TOMBSTONE + puts "TOMBSTONE" + else + puts "#{pair.key} -> #{pair.val}" + end + end + end + end ``` -=== "Zig" +### 2.   Quadratic Probing - ```zig title="hash_map_open_addressing.zig" - [class]{HashMapOpenAddressing}-[func]{} - ``` - -### 2.   Quadratic probing - -Quadratic probing is similar to linear probing and is one of the common strategies of open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the "square of the number of probes", i.e., $1, 4, 9, \dots$ steps. +Quadratic probing is similar to linear probing and is one of the common strategies for open addressing. When a collision occurs, quadratic probing does not simply skip a fixed number of steps but skips a number of steps equal to the "square of the number of probes", i.e., $1, 4, 9, \dots$ steps. Quadratic probing has the following advantages: -- Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping the distance of the square of the number of probes. +- Quadratic probing attempts to alleviate the clustering effect of linear probing by skipping distances equal to the square of the probe count. - Quadratic probing skips larger distances to find empty positions, which helps to distribute data more evenly. However, quadratic probing is not perfect: @@ -906,23 +3291,23 @@ However, quadratic probing is not perfect: - Clustering still exists, i.e., some positions are more likely to be occupied than others. - Due to the growth of squares, quadratic probing may not probe the entire hash table, meaning that even if there are empty buckets in the hash table, quadratic probing may not be able to access them. -### 3.   Double hashing +### 3.   Double Hashing As the name suggests, the double hashing method uses multiple hash functions $f_1(x)$, $f_2(x)$, $f_3(x)$, $\dots$ for probing. -- **Inserting Elements**: If hash function $f_1(x)$ encounters a conflict, it tries $f_2(x)$, and so on, until an empty position is found and the element is inserted. -- **Searching for Elements**: Search in the same order of hash functions until the target element is found and returned; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`. +- **Inserting elements**: If hash function $f_1(x)$ encounters a conflict, try $f_2(x)$, and so on, until an empty position is found and the element is inserted. +- **Searching for elements**: Search in the same order of hash functions until the target element is found and return it; if an empty position is encountered or all hash functions have been tried, it indicates the element is not in the hash table, then return `None`. Compared to linear probing, the double hashing method is less prone to clustering, but multiple hash functions introduce additional computational overhead. !!! tip - Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "can not directly delete elements." + Please note that open addressing (linear probing, quadratic probing, and double hashing) hash tables all have the problem of "cannot directly delete elements". -## 6.2.3   Choice of programming languages +## 6.2.3   Choice of Programming Languages Different programming languages adopt different hash table implementation strategies. Here are a few examples: - Python uses open addressing. The `dict` dictionary uses pseudo-random numbers for probing. - Java uses separate chaining. Since JDK 1.8, when the array length in `HashMap` reaches 64 and the length of a linked list reaches 8, the linked list is converted to a red-black tree to improve search performance. -- Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity resizing operation is performed to ensure performance. +- Go uses separate chaining. Go stipulates that each bucket can store up to 8 key-value pairs, and if the capacity is exceeded, an overflow bucket is linked; when there are too many overflow buckets, a special equal-capacity expansion operation is performed to ensure performance. diff --git a/en/docs/chapter_hashing/hash_map.md b/en/docs/chapter_hashing/hash_map.md index fa9154f19..acc10f989 100755 --- a/en/docs/chapter_hashing/hash_map.md +++ b/en/docs/chapter_hashing/hash_map.md @@ -2,39 +2,39 @@ comments: true --- -# 6.1   Hash table +# 6.1   Hash Table -A hash table, also known as a hash map, is a data structure that establishes a mapping between keys and values, enabling efficient element retrieval. Specifically, when we input a `key` into the hash table, we can retrieve the corresponding `value` in $O(1)$ time complexity. +A hash table, also known as a hash map, establishes a mapping between keys `key` and values `value`, enabling efficient element retrieval. Specifically, when we input a key `key` into a hash table, we can retrieve the corresponding value `value` in $O(1)$ time. -As shown in Figure 6-1, given $n$ students, each student has two data fields: "Name" and "Student ID". If we want to implement a query function that takes a student ID as input and returns the corresponding name, we can use the hash table shown in Figure 6-1. +As shown in Figure 6-1, given $n$ students, each with two pieces of data: "name" and "student ID". If we want to implement a query function that "inputs a student ID and returns the corresponding name", we can use the hash table shown below. ![Abstract representation of a hash table](hash_map.assets/hash_table_lookup.png){ class="animation-figure" }

Figure 6-1   Abstract representation of a hash table

-In addition to hash tables, arrays and linked lists can also be used to implement query functionality, but the time complexity is different. Their efficiency is compared in Table 6-1: +In addition to hash tables, arrays and linked lists can also implement query functionality. Their efficiency comparison is shown in the following table. -- **Inserting an element**: Simply append the element to the tail of the array (or linked list). The time complexity of this operation is $O(1)$. -- **Searching for an element**: As the array (or linked list) is unsorted, searching for an element requires traversing through all of the elements. The time complexity of this operation is $O(n)$. -- **Deleting an element**: To remove an element, we first need to locate it. Then, we delete it from the array (or linked list). The time complexity of this operation is $O(n)$. +- **Adding elements**: Simply add elements to the end of the array (linked list), using $O(1)$ time. +- **Querying elements**: Since the array (linked list) is unordered, all elements need to be traversed, using $O(n)$ time. +- **Deleting elements**: The element must first be located, then deleted from the array (linked list), using $O(n)$ time. -

Table 6-1   Comparison of time efficiency for common operations

+

Table 6-1   Comparison of element query efficiency

-| | Array | Linked List | Hash Table | -| -------------- | ------ | ----------- | ---------- | -| Search Elements | $O(n)$ | $O(n)$ | $O(1)$ | -| Insert Elements | $O(1)$ | $O(1)$ | $O(1)$ | -| Delete Elements | $O(n)$ | $O(n)$ | $O(1)$ | +| | Array | Linked List | Hash Table | +| --------------- | ------ | ----------- | ---------- | +| Find element | $O(n)$ | $O(n)$ | $O(1)$ | +| Add element | $O(1)$ | $O(1)$ | $O(1)$ | +| Delete element | $O(n)$ | $O(n)$ | $O(1)$ |
-As observed, **the time complexity for operations (insertion, deletion, searching, and modification) in a hash table is $O(1)$**, which is highly efficient. +As observed, **the time complexity for insertion, deletion, search, and modification operations in a hash table is $O(1)$**, which is very efficient. -## 6.1.1   Common operations of hash table +## 6.1.1   Common Hash Table Operations -Common operations of a hash table include: initialization, querying, adding key-value pairs, and deleting key-value pairs. Here is an example code: +Common operations on hash tables include: initialization, query operations, adding key-value pairs, and deleting key-value pairs. Example code is as follows: === "Python" @@ -43,15 +43,15 @@ Common operations of a hash table include: initialization, querying, adding key- hmap: dict = {} # Add operation - # Add key-value pair (key, value) to the hash table - hmap[12836] = "Xiao Ha" - hmap[15937] = "Xiao Luo" - hmap[16750] = "Xiao Suan" - hmap[13276] = "Xiao Fa" - hmap[10583] = "Xiao Ya" + # Add key-value pair (key, value) to hash table + hmap[12836] = "XiaoHa" + hmap[15937] = "XiaoLuo" + hmap[16750] = "XiaoSuan" + hmap[13276] = "XiaoFa" + hmap[10583] = "XiaoYa" # Query operation - # Input key into hash table, get value + # Input key into hash table to get value name: str = hmap[15937] # Delete operation @@ -67,14 +67,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - map[12836] = "Xiao Ha"; - map[15937] = "Xiao Luo"; - map[16750] = "Xiao Suan"; - map[13276] = "Xiao Fa"; - map[10583] = "Xiao Ya"; + map[12836] = "XiaoHa"; + map[15937] = "XiaoLuo"; + map[16750] = "XiaoSuan"; + map[13276] = "XiaoFa"; + map[10583] = "XiaoYa"; /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value string name = map[15937]; /* Delete operation */ @@ -90,14 +90,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - map.put(12836, "Xiao Ha"); - map.put(15937, "Xiao Luo"); - map.put(16750, "Xiao Suan"); - map.put(13276, "Xiao Fa"); - map.put(10583, "Xiao Ya"); + map.put(12836, "XiaoHa"); + map.put(15937, "XiaoLuo"); + map.put(16750, "XiaoSuan"); + map.put(13276, "XiaoFa"); + map.put(10583, "XiaoYa"); /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value String name = map.get(15937); /* Delete operation */ @@ -112,15 +112,15 @@ Common operations of a hash table include: initialization, querying, adding key- Dictionary map = new() { /* Add operation */ // Add key-value pair (key, value) to hash table - { 12836, "Xiao Ha" }, - { 15937, "Xiao Luo" }, - { 16750, "Xiao Suan" }, - { 13276, "Xiao Fa" }, - { 10583, "Xiao Ya" } + { 12836, "XiaoHa" }, + { 15937, "XiaoLuo" }, + { 16750, "XiaoSuan" }, + { 13276, "XiaoFa" }, + { 10583, "XiaoYa" } }; /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value string name = map[15937]; /* Delete operation */ @@ -136,14 +136,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - hmap[12836] = "Xiao Ha" - hmap[15937] = "Xiao Luo" - hmap[16750] = "Xiao Suan" - hmap[13276] = "Xiao Fa" - hmap[10583] = "Xiao Ya" + hmap[12836] = "XiaoHa" + hmap[15937] = "XiaoLuo" + hmap[16750] = "XiaoSuan" + hmap[13276] = "XiaoFa" + hmap[10583] = "XiaoYa" /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value name := hmap[15937] /* Delete operation */ @@ -159,14 +159,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - map[12836] = "Xiao Ha" - map[15937] = "Xiao Luo" - map[16750] = "Xiao Suan" - map[13276] = "Xiao Fa" - map[10583] = "Xiao Ya" + map[12836] = "XiaoHa" + map[15937] = "XiaoLuo" + map[16750] = "XiaoSuan" + map[13276] = "XiaoFa" + map[10583] = "XiaoYa" /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value let name = map[15937]! /* Delete operation */ @@ -180,15 +180,15 @@ Common operations of a hash table include: initialization, querying, adding key- /* Initialize hash table */ const map = new Map(); /* Add operation */ - // Add key-value pair (key, value) to the hash table - map.set(12836, 'Xiao Ha'); - map.set(15937, 'Xiao Luo'); - map.set(16750, 'Xiao Suan'); - map.set(13276, 'Xiao Fa'); - map.set(10583, 'Xiao Ya'); + // Add key-value pair (key, value) to hash table + map.set(12836, 'XiaoHa'); + map.set(15937, 'XiaoLuo'); + map.set(16750, 'XiaoSuan'); + map.set(13276, 'XiaoFa'); + map.set(10583, 'XiaoYa'); /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value let name = map.get(15937); /* Delete operation */ @@ -203,23 +203,23 @@ Common operations of a hash table include: initialization, querying, adding key- const map = new Map(); /* Add operation */ // Add key-value pair (key, value) to hash table - map.set(12836, 'Xiao Ha'); - map.set(15937, 'Xiao Luo'); - map.set(16750, 'Xiao Suan'); - map.set(13276, 'Xiao Fa'); - map.set(10583, 'Xiao Ya'); - console.info('\nAfter adding, the hash table is\nKey -> Value'); + map.set(12836, 'XiaoHa'); + map.set(15937, 'XiaoLuo'); + map.set(16750, 'XiaoSuan'); + map.set(13276, 'XiaoFa'); + map.set(10583, 'XiaoYa'); + console.info('\nAfter adding, hash table is\nKey -> Value'); console.info(map); /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value let name = map.get(15937); - console.info('\nInput student number 15937, query name ' + name); + console.info('\nInput student ID 15937, queried name ' + name); /* Delete operation */ // Delete key-value pair (key, value) from hash table map.delete(10583); - console.info('\nAfter deleting 10583, the hash table is\nKey -> Value'); + console.info('\nAfter deleting 10583, hash table is\nKey -> Value'); console.info(map); ``` @@ -231,14 +231,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - map[12836] = "Xiao Ha"; - map[15937] = "Xiao Luo"; - map[16750] = "Xiao Suan"; - map[13276] = "Xiao Fa"; - map[10583] = "Xiao Ya"; + map[12836] = "XiaoHa"; + map[15937] = "XiaoLuo"; + map[16750] = "XiaoSuan"; + map[13276] = "XiaoFa"; + map[10583] = "XiaoYa"; /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value String name = map[15937]; /* Delete operation */ @@ -256,14 +256,14 @@ Common operations of a hash table include: initialization, querying, adding key- /* Add operation */ // Add key-value pair (key, value) to hash table - map.insert(12836, "Xiao Ha".to_string()); - map.insert(15937, "Xiao Luo".to_string()); - map.insert(16750, "Xiao Suan".to_string()); - map.insert(13279, "Xiao Fa".to_string()); - map.insert(10583, "Xiao Ya".to_string()); + map.insert(12836, "XiaoHa".to_string()); + map.insert(15937, "XiaoLuo".to_string()); + map.insert(16750, "XiaoSuan".to_string()); + map.insert(13279, "XiaoFa".to_string()); + map.insert(10583, "XiaoYa".to_string()); /* Query operation */ - // Input key into hash table, get value + // Input key into hash table to get value let _name: Option<&String> = map.get(&15937); /* Delete operation */ @@ -280,21 +280,54 @@ Common operations of a hash table include: initialization, querying, adding key- === "Kotlin" ```kotlin title="hash_map.kt" + /* Initialize hash table */ + val map = HashMap() + /* Add operation */ + // Add key-value pair (key, value) to hash table + map[12836] = "XiaoHa" + map[15937] = "XiaoLuo" + map[16750] = "XiaoSuan" + map[13276] = "XiaoFa" + map[10583] = "XiaoYa" + + /* Query operation */ + // Input key into hash table to get value + val name = map[15937] + + /* Delete operation */ + // Delete key-value pair (key, value) from hash table + map.remove(10583) ``` -=== "Zig" +=== "Ruby" - ```zig title="hash_map.zig" + ```ruby title="hash_map.rb" + # Initialize hash table + hmap = {} + # Add operation + # Add key-value pair (key, value) to hash table + hmap[12836] = "XiaoHa" + hmap[15937] = "XiaoLuo" + hmap[16750] = "XiaoSuan" + hmap[13276] = "XiaoFa" + hmap[10583] = "XiaoYa" + + # Query operation + # Input key into hash table to get value + name = hmap[15937] + + # Delete operation + # Delete key-value pair (key, value) from hash table + hmap.delete(10583) ``` -??? pythontutor "Code Visualization" +??? pythontutor "Visualized Execution" -
- + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%9F%A5%E8%AF%A2%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%90%91%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E8%BE%93%E5%85%A5%E9%94%AE%20key%20%EF%BC%8C%E5%BE%97%E5%88%B0%E5%80%BC%20value%0A%20%20%20%20name%20%3D%20hmap%5B15937%5D%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E5%88%A0%E9%99%A4%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap.pop%2810583%29&cumulative=false&curInstr=2&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false -There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Here is an example code: +There are three common ways to traverse a hash table: traversing key-value pairs, traversing keys, and traversing values. Example code is as follows: === "Python" @@ -439,17 +472,17 @@ There are three common ways to traverse a hash table: traversing key-value pairs /* Traverse hash table */ // Traverse key-value pairs Key->Value map.forEach((key, value) { - print('$key -> $value'); + print('$key -> $value'); }); - // Traverse keys only Key + // Traverse keys only map.keys.forEach((key) { - print(key); + print(key); }); - // Traverse values only Value + // Traverse values only map.values.forEach((value) { - print(value); + print(value); }); ``` @@ -462,12 +495,12 @@ There are three common ways to traverse a hash table: traversing key-value pairs println!("{key} -> {value}"); } - // Traverse keys only Key + // Traverse keys only for key in map.keys() { - println!("{key}"); + println!("{key}"); } - // Traverse values only Value + // Traverse values only for value in map.values() { println!("{value}"); } @@ -482,44 +515,63 @@ There are three common ways to traverse a hash table: traversing key-value pairs === "Kotlin" ```kotlin title="hash_map.kt" - + /* Traverse hash table */ + // Traverse key-value pairs key->value + for ((key, value) in map) { + println("$key -> $value") + } + // Traverse keys only + for (key in map.keys) { + println(key) + } + // Traverse values only + for (_val in map.values) { + println(_val) + } ``` -=== "Zig" +=== "Ruby" - ```zig title="hash_map.zig" - // Zig example is not provided + ```ruby title="hash_map.rb" + # Traverse hash table + # Traverse key-value pairs key->value + hmap.entries.each { |key, value| puts "#{key} -> #{value}" } + + # Traverse keys only + hmap.keys.each { |key| puts key } + + # Traverse values only + hmap.values.each { |val| puts val } ``` -??? pythontutor "Code Visualization" +??? pythontutor "Visualized Execution" -
- + https://pythontutor.com/render.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20hmap%20%3D%20%7B%7D%0A%20%20%20%20%0A%20%20%20%20%23%20%E6%B7%BB%E5%8A%A0%E6%93%8D%E4%BD%9C%0A%20%20%20%20%23%20%E5%9C%A8%E5%93%88%E5%B8%8C%E8%A1%A8%E4%B8%AD%E6%B7%BB%E5%8A%A0%E9%94%AE%E5%80%BC%E5%AF%B9%20%28key,%20value%29%0A%20%20%20%20hmap%5B12836%5D%20%3D%20%22%E5%B0%8F%E5%93%88%22%0A%20%20%20%20hmap%5B15937%5D%20%3D%20%22%E5%B0%8F%E5%95%B0%22%0A%20%20%20%20hmap%5B16750%5D%20%3D%20%22%E5%B0%8F%E7%AE%97%22%0A%20%20%20%20hmap%5B13276%5D%20%3D%20%22%E5%B0%8F%E6%B3%95%22%0A%20%20%20%20hmap%5B10583%5D%20%3D%20%22%E5%B0%8F%E9%B8%AD%22%0A%20%20%20%20%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E5%93%88%E5%B8%8C%E8%A1%A8%0A%20%20%20%20%23%20%E9%81%8D%E5%8E%86%E9%94%AE%E5%80%BC%E5%AF%B9%20key-%3Evalue%0A%20%20%20%20for%20key,%20value%20in%20hmap.items%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key,%20%22-%3E%22,%20value%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E9%94%AE%20key%0A%20%20%20%20for%20key%20in%20hmap.keys%28%29%3A%0A%20%20%20%20%20%20%20%20print%28key%29%0A%20%20%20%20%23%20%E5%8D%95%E7%8B%AC%E9%81%8D%E5%8E%86%E5%80%BC%20value%0A%20%20%20%20for%20value%20in%20hmap.values%28%29%3A%0A%20%20%20%20%20%20%20%20print%28value%29&cumulative=false&curInstr=8&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false -## 6.1.2   Simple implementation of a hash table +## 6.1.2   Simple Hash Table Implementation -First, let's consider the simplest case: **implementing a hash table using only one array**. In the hash table, each empty slot in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation involves finding the bucket corresponding to the `key` and retrieving the `value` from it. +Let's first consider the simplest case: **implementing a hash table using only an array**. In a hash table, each empty position in the array is called a bucket, and each bucket can store a key-value pair. Therefore, the query operation is to find the bucket corresponding to `key` and retrieve the `value` from the bucket. -So, how do we locate the corresponding bucket based on the `key`? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space consists of all the keys, and the output space consists of all the buckets (array indices). In other words, given a `key`, **we can use the hash function to determine the storage location of the corresponding key-value pair in the array**. +So how do we locate the corresponding bucket based on `key`? This is achieved through a hash function. The role of the hash function is to map a larger input space to a smaller output space. In a hash table, the input space is all `key`s, and the output space is all buckets (array indices). In other words, given a `key`, **we can use the hash function to obtain the storage location of the key-value pair corresponding to that `key` in the array**. -With a given `key`, the calculation of the hash function consists of two steps: +When inputting a `key`, the hash function's calculation process consists of the following two steps: -1. Calculate the hash value by using a certain hash algorithm `hash()`. -2. Take the modulus of the hash value with the bucket count (array length) `capacity` to obtain the array `index` corresponding to the key. +1. Calculate the hash value through a hash algorithm `hash()`. +2. Take the modulo of the hash value by the number of buckets (array length) `capacity` to obtain the bucket (array index) `index` corresponding to that `key`. ```shell index = hash(key) % capacity ``` -Afterward, we can use the `index` to access the corresponding bucket in the hash table and thereby retrieve the `value`. +Subsequently, we can use `index` to access the corresponding bucket in the hash table and retrieve the `value`. -Let's assume that the array length is `capacity = 100`, and the hash algorithm is defined as `hash(key) = key`. Therefore, the hash function can be expressed as `key % 100`. The following figure illustrates the working principle of the hash function using `key` as student ID and `value` as name. +Assuming the array length is `capacity = 100` and the hash algorithm is `hash(key) = key`, the hash function becomes `key % 100`. Figure 6-2 shows the working principle of the hash function using `key` as student ID and `value` as name. ![Working principle of hash function](hash_map.assets/hash_function.png){ class="animation-figure" }

Figure 6-2   Working principle of hash function

-The following code implements a simple hash table. Here, we encapsulate `key` and `value` into a class `Pair` to represent the key-value pair. +The following code implements a simple hash table. Here, we encapsulate `key` and `value` into a class `Pair` to represent a key-value pair. === "Python" @@ -536,7 +588,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an def __init__(self): """Constructor""" - # Initialize an array, containing 100 buckets + # Initialize array with 100 buckets self.buckets: list[Pair | None] = [None] * 100 def hash_func(self, key: int) -> int: @@ -544,7 +596,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an index = key % 100 return index - def get(self, key: int) -> str: + def get(self, key: int) -> str | None: """Query operation""" index: int = self.hash_func(key) pair: Pair = self.buckets[index] @@ -553,7 +605,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an return pair.val def put(self, key: int, val: str): - """Add operation""" + """Add and update operation""" pair = Pair(key, val) index: int = self.hash_func(key) self.buckets[index] = pair @@ -561,7 +613,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an def remove(self, key: int): """Remove operation""" index: int = self.hash_func(key) - # Set to None, representing removal + # Set to None to represent removal self.buckets[index] = None def entry_set(self) -> list[Pair]: @@ -616,7 +668,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an public: ArrayHashMap() { - // Initialize an array, containing 100 buckets + // Initialize array with 100 buckets buckets = vector(100); } @@ -719,7 +771,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an private List buckets; public ArrayHashMap() { - // Initialize an array, containing 100 buckets + // Initialize array with 100 buckets buckets = new ArrayList<>(); for (int i = 0; i < 100; i++) { buckets.add(null); @@ -751,7 +803,7 @@ The following code implements a simple hash table. Here, we encapsulate `key` an /* Remove operation */ public void remove(int key) { int index = hashFunc(key); - // Set to null, indicating removal + // Set to null to represent deletion buckets.set(index, null); } @@ -797,116 +849,933 @@ The following code implements a simple hash table. Here, we encapsulate `key` an === "C#" ```csharp title="array_hash_map.cs" - [class]{Pair}-[func]{} + /* Key-value pair int->string */ + class Pair(int key, string val) { + public int key = key; + public string val = val; + } - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + class ArrayHashMap { + List buckets; + public ArrayHashMap() { + // Initialize array with 100 buckets + buckets = []; + for (int i = 0; i < 100; i++) { + buckets.Add(null); + } + } + + /* Hash function */ + int HashFunc(int key) { + int index = key % 100; + return index; + } + + /* Query operation */ + public string? Get(int key) { + int index = HashFunc(key); + Pair? pair = buckets[index]; + if (pair == null) return null; + return pair.val; + } + + /* Add operation */ + public void Put(int key, string val) { + Pair pair = new(key, val); + int index = HashFunc(key); + buckets[index] = pair; + } + + /* Remove operation */ + public void Remove(int key) { + int index = HashFunc(key); + // Set to null to represent deletion + buckets[index] = null; + } + + /* Get all key-value pairs */ + public List PairSet() { + List pairSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + pairSet.Add(pair); + } + return pairSet; + } + + /* Get all keys */ + public List KeySet() { + List keySet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + keySet.Add(pair.key); + } + return keySet; + } + + /* Get all values */ + public List ValueSet() { + List valueSet = []; + foreach (Pair? pair in buckets) { + if (pair != null) + valueSet.Add(pair.val); + } + return valueSet; + } + + /* Print hash table */ + public void Print() { + foreach (Pair kv in PairSet()) { + Console.WriteLine(kv.key + " -> " + kv.val); + } + } + } ``` === "Go" ```go title="array_hash_map.go" - [class]{pair}-[func]{} + /* Key-value pair */ + type pair struct { + key int + val string + } - [class]{arrayHashMap}-[func]{} + /* Hash table based on array implementation */ + type arrayHashMap struct { + buckets []*pair + } + + /* Initialize hash table */ + func newArrayHashMap() *arrayHashMap { + // Initialize array with 100 buckets + buckets := make([]*pair, 100) + return &arrayHashMap{buckets: buckets} + } + + /* Hash function */ + func (a *arrayHashMap) hashFunc(key int) int { + index := key % 100 + return index + } + + /* Query operation */ + func (a *arrayHashMap) get(key int) string { + index := a.hashFunc(key) + pair := a.buckets[index] + if pair == nil { + return "Not Found" + } + return pair.val + } + + /* Add operation */ + func (a *arrayHashMap) put(key int, val string) { + pair := &pair{key: key, val: val} + index := a.hashFunc(key) + a.buckets[index] = pair + } + + /* Remove operation */ + func (a *arrayHashMap) remove(key int) { + index := a.hashFunc(key) + // Set to nil to delete + a.buckets[index] = nil + } + + /* Get all key pairs */ + func (a *arrayHashMap) pairSet() []*pair { + var pairs []*pair + for _, pair := range a.buckets { + if pair != nil { + pairs = append(pairs, pair) + } + } + return pairs + } + + /* Get all keys */ + func (a *arrayHashMap) keySet() []int { + var keys []int + for _, pair := range a.buckets { + if pair != nil { + keys = append(keys, pair.key) + } + } + return keys + } + + /* Get all values */ + func (a *arrayHashMap) valueSet() []string { + var values []string + for _, pair := range a.buckets { + if pair != nil { + values = append(values, pair.val) + } + } + return values + } + + /* Print hash table */ + 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" - [file]{utils/pair.swift}-[class]{Pair}-[func]{} + /* Key-value pair */ + class Pair: Equatable { + public var key: Int + public var val: String - [class]{ArrayHashMap}-[func]{} + public init(key: Int, val: String) { + self.key = key + self.val = val + } + + public static func == (lhs: Pair, rhs: Pair) -> Bool { + lhs.key == rhs.key && lhs.val == rhs.val + } + } + + /* Hash table based on array implementation */ + class ArrayHashMap { + private var buckets: [Pair?] + + init() { + // Initialize array with 100 buckets + buckets = Array(repeating: nil, count: 100) + } + + /* Hash function */ + private func hashFunc(key: Int) -> Int { + let index = key % 100 + return index + } + + /* Query operation */ + func get(key: Int) -> String? { + let index = hashFunc(key: key) + let pair = buckets[index] + return pair?.val + } + + /* Add operation */ + func put(key: Int, val: String) { + let pair = Pair(key: key, val: val) + let index = hashFunc(key: key) + buckets[index] = pair + } + + /* Remove operation */ + func remove(key: Int) { + let index = hashFunc(key: key) + // Set to nil to delete + buckets[index] = nil + } + + /* Get all key-value pairs */ + func pairSet() -> [Pair] { + buckets.compactMap { $0 } + } + + /* Get all keys */ + func keySet() -> [Int] { + buckets.compactMap { $0?.key } + } + + /* Get all values */ + func valueSet() -> [String] { + buckets.compactMap { $0?.val } + } + + /* Print hash table */ + func print() { + for pair in pairSet() { + Swift.print("\(pair.key) -> \(pair.val)") + } + } + } ``` === "JS" ```javascript title="array_hash_map.js" - [class]{Pair}-[func]{} + /* Key-value pair Number -> String */ + class Pair { + constructor(key, val) { + this.key = key; + this.val = val; + } + } - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + class ArrayHashMap { + #buckets; + constructor() { + // Initialize array with 100 buckets + this.#buckets = new Array(100).fill(null); + } + + /* Hash function */ + #hashFunc(key) { + return key % 100; + } + + /* Query operation */ + get(key) { + let index = this.#hashFunc(key); + let pair = this.#buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* Add operation */ + set(key, val) { + let index = this.#hashFunc(key); + this.#buckets[index] = new Pair(key, val); + } + + /* Remove operation */ + delete(key) { + let index = this.#hashFunc(key); + // Set to null to represent deletion + this.#buckets[index] = null; + } + + /* Get all key-value pairs */ + entries() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i]); + } + } + return arr; + } + + /* Get all keys */ + keys() { + let arr = []; + for (let i = 0; i < this.#buckets.length; i++) { + if (this.#buckets[i]) { + arr.push(this.#buckets[i].key); + } + } + return arr; + } + + /* Get all values */ + 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 hash table */ + print() { + let pairSet = this.entries(); + for (const pair of pairSet) { + console.info(`${pair.key} -> ${pair.val}`); + } + } + } ``` === "TS" ```typescript title="array_hash_map.ts" - [class]{Pair}-[func]{} + /* Key-value pair Number -> String */ + class Pair { + public key: number; + public val: string; - [class]{ArrayHashMap}-[func]{} + constructor(key: number, val: string) { + this.key = key; + this.val = val; + } + } + + /* Hash table based on array implementation */ + class ArrayHashMap { + private readonly buckets: (Pair | null)[]; + + constructor() { + // Initialize array with 100 buckets + this.buckets = new Array(100).fill(null); + } + + /* Hash function */ + private hashFunc(key: number): number { + return key % 100; + } + + /* Query operation */ + public get(key: number): string | null { + let index = this.hashFunc(key); + let pair = this.buckets[index]; + if (pair === null) return null; + return pair.val; + } + + /* Add operation */ + public set(key: number, val: string) { + let index = this.hashFunc(key); + this.buckets[index] = new Pair(key, val); + } + + /* Remove operation */ + public delete(key: number) { + let index = this.hashFunc(key); + // Set to null to represent deletion + this.buckets[index] = null; + } + + /* Get all key-value pairs */ + 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; + } + + /* Get all keys */ + 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; + } + + /* Get all values */ + 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; + } + + /* Print hash table */ + 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}-[func]{} + /* Key-value pair */ + class Pair { + int key; + String val; + Pair(this.key, this.val); + } - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + class ArrayHashMap { + late List _buckets; + + ArrayHashMap() { + // Initialize array with 100 buckets + _buckets = List.filled(100, null); + } + + /* Hash function */ + int _hashFunc(int key) { + final int index = key % 100; + return index; + } + + /* Query operation */ + String? get(int key) { + final int index = _hashFunc(key); + final Pair? pair = _buckets[index]; + if (pair == null) { + return null; + } + return pair.val; + } + + /* Add operation */ + void put(int key, String val) { + final Pair pair = Pair(key, val); + final int index = _hashFunc(key); + _buckets[index] = pair; + } + + /* Remove operation */ + void remove(int key) { + final int index = _hashFunc(key); + _buckets[index] = null; + } + + /* Get all key-value pairs */ + List pairSet() { + List pairSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + pairSet.add(pair); + } + } + return pairSet; + } + + /* Get all keys */ + List keySet() { + List keySet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + keySet.add(pair.key); + } + } + return keySet; + } + + /* Get all values */ + List values() { + List valueSet = []; + for (final Pair? pair in _buckets) { + if (pair != null) { + valueSet.add(pair.val); + } + } + return valueSet; + } + + /* Print hash table */ + void printHashMap() { + for (final Pair kv in pairSet()) { + print("${kv.key} -> ${kv.val}"); + } + } + } ``` === "Rust" ```rust title="array_hash_map.rs" - [class]{Pair}-[func]{} + /* Key-value pair */ + #[derive(Debug, Clone, PartialEq)] + pub struct Pair { + pub key: i32, + pub val: String, + } - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + pub struct ArrayHashMap { + buckets: Vec>, + } + + impl ArrayHashMap { + pub fn new() -> ArrayHashMap { + // Initialize array with 100 buckets + Self { + buckets: vec![None; 100], + } + } + + /* Hash function */ + fn hash_func(&self, key: i32) -> usize { + key as usize % 100 + } + + /* Query operation */ + pub fn get(&self, key: i32) -> Option<&String> { + let index = self.hash_func(key); + self.buckets[index].as_ref().map(|pair| &pair.val) + } + + /* Add operation */ + 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(), + }); + } + + /* Remove operation */ + pub fn remove(&mut self, key: i32) { + let index = self.hash_func(key); + // Set to None to represent removal + self.buckets[index] = None; + } + + /* Get all key-value pairs */ + pub fn entry_set(&self) -> Vec<&Pair> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref()) + .collect() + } + + /* Get all keys */ + pub fn key_set(&self) -> Vec<&i32> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.key)) + .collect() + } + + /* Get all values */ + pub fn value_set(&self) -> Vec<&String> { + self.buckets + .iter() + .filter_map(|pair| pair.as_ref().map(|pair| &pair.val)) + .collect() + } + + /* Print hash table */ + pub fn print(&self) { + for pair in self.entry_set() { + println!("{} -> {}", pair.key, pair.val); + } + } + } ``` === "C" ```c title="array_hash_map.c" - [class]{Pair}-[func]{} + /* Key-value pair int->string */ + typedef struct { + int key; + char *val; + } Pair; - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + typedef struct { + Pair *buckets[MAX_SIZE]; + } ArrayHashMap; + + /* Constructor */ + ArrayHashMap *newArrayHashMap() { + ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap)); + for (int i=0; i < MAX_SIZE; i++) { + hmap->buckets[i] = NULL; + } + return hmap; + } + + /* Destructor */ + void delArrayHashMap(ArrayHashMap *hmap) { + for (int i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + free(hmap->buckets[i]->val); + free(hmap->buckets[i]); + } + } + free(hmap); + } + + /* Add operation */ + void put(ArrayHashMap *hmap, const int key, const char *val) { + Pair *Pair = malloc(sizeof(Pair)); + Pair->key = key; + Pair->val = malloc(strlen(val) + 1); + strcpy(Pair->val, val); + + int index = hashFunc(key); + hmap->buckets[index] = Pair; + } + + /* Remove operation */ + void removeItem(ArrayHashMap *hmap, const int key) { + int index = hashFunc(key); + free(hmap->buckets[index]->val); + free(hmap->buckets[index]); + hmap->buckets[index] = NULL; + } + + /* Get all key-value pairs */ + void pairSet(ArrayHashMap *hmap, MapSet *set) { + Pair *entries; + int i = 0, index = 0; + int total = 0; + /* Count valid key-value pairs */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + entries = malloc(sizeof(Pair) * total); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + entries[index].key = hmap->buckets[i]->key; + entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1); + strcpy(entries[index].val, hmap->buckets[i]->val); + index++; + } + } + set->set = entries; + set->len = total; + } + + /* Get all keys */ + void keySet(ArrayHashMap *hmap, MapSet *set) { + int *keys; + int i = 0, index = 0; + int total = 0; + /* Count valid key-value pairs */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + keys = malloc(total * sizeof(int)); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + keys[index] = hmap->buckets[i]->key; + index++; + } + } + set->set = keys; + set->len = total; + } + + /* Get all values */ + void valueSet(ArrayHashMap *hmap, MapSet *set) { + char **vals; + int i = 0, index = 0; + int total = 0; + /* Count valid key-value pairs */ + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + total++; + } + } + vals = malloc(total * sizeof(char *)); + for (i = 0; i < MAX_SIZE; i++) { + if (hmap->buckets[i] != NULL) { + vals[index] = hmap->buckets[i]->val; + index++; + } + } + set->set = vals; + set->len = total; + } + + /* Print hash table */ + void print(ArrayHashMap *hmap) { + int i; + MapSet set; + pairSet(hmap, &set); + Pair *entries = (Pair *)set.set; + for (i = 0; i < set.len; i++) { + printf("%d -> %s\n", entries[i].key, entries[i].val); + } + free(set.set); + } ``` === "Kotlin" ```kotlin title="array_hash_map.kt" - [class]{Pair}-[func]{} + /* Key-value pair */ + class Pair( + var key: Int, + var _val: String + ) - [class]{ArrayHashMap}-[func]{} + /* Hash table based on array implementation */ + class ArrayHashMap { + // Initialize array with 100 buckets + private val buckets = arrayOfNulls(100) + + /* Hash function */ + fun hashFunc(key: Int): Int { + val index = key % 100 + return index + } + + /* Query operation */ + fun get(key: Int): String? { + val index = hashFunc(key) + val pair = buckets[index] ?: return null + return pair._val + } + + /* Add operation */ + fun put(key: Int, _val: String) { + val pair = Pair(key, _val) + val index = hashFunc(key) + buckets[index] = pair + } + + /* Remove operation */ + fun remove(key: Int) { + val index = hashFunc(key) + // Set to null to represent deletion + buckets[index] = null + } + + /* Get all key-value pairs */ + fun pairSet(): MutableList { + val pairSet = mutableListOf() + for (pair in buckets) { + if (pair != null) + pairSet.add(pair) + } + return pairSet + } + + /* Get all keys */ + fun keySet(): MutableList { + val keySet = mutableListOf() + for (pair in buckets) { + if (pair != null) + keySet.add(pair.key) + } + return keySet + } + + /* Get all values */ + fun valueSet(): MutableList { + val valueSet = mutableListOf() + for (pair in buckets) { + if (pair != null) + valueSet.add(pair._val) + } + return valueSet + } + + /* Print hash table */ + fun print() { + for (kv in pairSet()) { + val key = kv.key + val _val = kv._val + println("$key -> $_val") + } + } + } ``` === "Ruby" ```ruby title="array_hash_map.rb" - [class]{Pair}-[func]{} + ### Key-value pair ### + class Pair + attr_accessor :key, :val - [class]{ArrayHashMap}-[func]{} + def initialize(key, val) + @key = key + @val = val + end + end + + ### Hash map based on array ### + class ArrayHashMap + ### Constructor ### + def initialize + # Initialize array with 100 buckets + @buckets = Array.new(100) + end + + ### Hash function ### + def hash_func(key) + index = key % 100 + end + + ### Query operation ### + def get(key) + index = hash_func(key) + pair = @buckets[index] + + return if pair.nil? + pair.val + end + + ### Add operation ### + def put(key, val) + pair = Pair.new(key, val) + index = hash_func(key) + @buckets[index] = pair + end + + ### Delete operation ### + def remove(key) + index = hash_func(key) + # Set to nil to delete + @buckets[index] = nil + end + + ### Get all key-value pairs ### + def entry_set + result = [] + @buckets.each { |pair| result << pair unless pair.nil? } + result + end + + ### Get all keys ### + def key_set + result = [] + @buckets.each { |pair| result << pair.key unless pair.nil? } + result + end + + ### Get all values ### + def value_set + result = [] + @buckets.each { |pair| result << pair.val unless pair.nil? } + result + end + + ### Print hash table ### + def print + @buckets.each { |pair| puts "#{pair.key} -> #{pair.val}" unless pair.nil? } + end + end ``` -=== "Zig" +## 6.1.3   Hash Collision and Resizing - ```zig title="array_hash_map.zig" - [class]{Pair}-[func]{} +Fundamentally, the role of a hash function is to map the input space consisting of all `key`s to the output space consisting of all array indices, and the input space is often much larger than the output space. Therefore, **theoretically there must be cases where "multiple inputs correspond to the same output"**. - [class]{ArrayHashMap}-[func]{} - ``` - -## 6.1.3   Hash collision and resizing - -Essentially, the role of the hash function is to map the entire input space of all keys to the output space of all array indices. However, the input space is often much larger than the output space. Therefore, **theoretically, there will always be cases where "multiple inputs correspond to the same output"**. - -In the example above, with the given hash function, when the last two digits of the input `key` are the same, the hash function produces the same output. For instance, when querying two students with student IDs 12836 and 20336, we find: +For the hash function in the above example, when the input `key`s have the same last two digits, the hash function produces the same output. For example, when querying two students with IDs 12836 and 20336, we get: ```shell 12836 % 100 = 36 20336 % 100 = 36 ``` -As shown in Figure 6-3, both student IDs point to the same name, which is obviously incorrect. This situation where multiple inputs correspond to the same output is called hash collision. +As shown in Figure 6-3, two student IDs point to the same name, which is obviously incorrect. We call this situation where multiple inputs correspond to the same output a hash collision. -![Example of hash collision](hash_map.assets/hash_collision.png){ class="animation-figure" } +![Hash collision example](hash_map.assets/hash_collision.png){ class="animation-figure" } -

Figure 6-3   Example of hash collision

+

Figure 6-3   Hash collision example

-It is easy to understand that as the capacity $n$ of the hash table increases, the probability of multiple keys being assigned to the same bucket decreases, resulting in fewer collisions. Therefore, **we can reduce hash collisions by resizing the hash table**. +It's easy to see that the larger the hash table capacity $n$, the lower the probability that multiple `key`s will be assigned to the same bucket, and the fewer collisions. Therefore, **we can reduce hash collisions by expanding the hash table**. -As shown in Figure 6-4, before resizing, the key-value pairs `(136, A)` and `(236, D)` collide. However, after resizing, the collision is resolved. +As shown in Figure 6-4, before expansion, the key-value pairs `(136, A)` and `(236, D)` collided, but after expansion, the collision disappears. ![Hash table resizing](hash_map.assets/hash_table_reshash.png){ class="animation-figure" }

Figure 6-4   Hash table resizing

-Similar to array expansion, resizing a hash table requires migrating all key-value pairs from the original hash table to the new one, which is time-consuming. Furthermore, since the `capacity` of the hash table changes, we need to recalculate the storage positions of all key-value pairs using the hash function, further increasing the computational overhead of the resizing process. Therefore, programming languages often allocate a sufficiently large capacity for the hash table to prevent frequent resizing. +Similar to array expansion, hash table expansion requires migrating all key-value pairs from the original hash table to the new hash table, which is very time-consuming. Moreover, since the hash table capacity `capacity` changes, we need to recalculate the storage locations of all key-value pairs through the hash function, further increasing the computational overhead of the expansion process. For this reason, programming languages typically reserve a sufficiently large hash table capacity to prevent frequent expansion. -The load factor is an important concept in hash tables. It is defined as the ratio of the number of elements in the hash table to the number of buckets. It is used to measure the severity of hash collisions and **often serves as a trigger for hash table resizing**. For example, in Java, when the load factor exceeds $0.75$, the system will resize the hash table to twice its original size. +The load factor is an important concept for hash tables. It is defined as the number of elements in the hash table divided by the number of buckets, and is used to measure the severity of hash collisions. **It is also commonly used as a trigger condition for hash table expansion**. For example, in Java, when the load factor exceeds $0.75$, the system will expand the hash table to $2$ times its original size. diff --git a/en/docs/chapter_hashing/index.md b/en/docs/chapter_hashing/index.md index 42bb5a431..50cc48b95 100644 --- a/en/docs/chapter_hashing/index.md +++ b/en/docs/chapter_hashing/index.md @@ -3,19 +3,19 @@ comments: true icon: material/table-search --- -# Chapter 6.   Hash table +# Chapter 6.   Hashing -![Hash table](../assets/covers/chapter_hashing.jpg){ class="cover-image" } +![Hashing](../assets/covers/chapter_hashing.jpg){ class="cover-image" } !!! abstract - In the world of computing, a hash table is akin to an intelligent librarian. - - It understands how to compute index numbers, enabling swift retrieval of the desired book. + In the world of computing, a hash table is like a clever librarian. + + They know how to calculate call numbers, enabling them to quickly locate the target book. ## Chapter contents -- [6.1   Hash table](hash_map.md) -- [6.2   Hash collision](hash_collision.md) -- [6.3   Hash algorithm](hash_algorithm.md) +- [6.1   Hash Table](hash_map.md) +- [6.2   Hash Collision](hash_collision.md) +- [6.3   Hash Algorithm](hash_algorithm.md) - [6.4   Summary](summary.md) diff --git a/en/docs/chapter_hashing/summary.md b/en/docs/chapter_hashing/summary.md index e1053c86b..2d3f8154f 100644 --- a/en/docs/chapter_hashing/summary.md +++ b/en/docs/chapter_hashing/summary.md @@ -4,19 +4,19 @@ comments: true # 6.4   Summary -### 1.   Key review +### 1.   Key Review - Given an input `key`, a hash table can retrieve the corresponding `value` in $O(1)$ time, which is highly efficient. - Common hash table operations include querying, adding key-value pairs, deleting key-value pairs, and traversing the hash table. - The hash function maps a `key` to an array index, allowing access to the corresponding bucket and retrieval of the `value`. - Two different keys may end up with the same array index after hashing, leading to erroneous query results. This phenomenon is known as hash collision. -- The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table resizing can mitigate hash collisions. Similar to array resizing, hash table resizing is costly. -- The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table resizing. -- Chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same list. However, excessively long lists can reduce query efficiency, which can be improved by converting the lists into red-black trees. -- Open addressing handles hash collisions through multiple probes. Linear probing uses a fixed step size but it cannot delete elements and is prone to clustering. Multiple hashing uses several hash functions for probing which reduces clustering compared to linear probing but increases computational overhead. -- Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses chaining, while Python's `dict` employs open addressing. +- The larger the capacity of the hash table, the lower the probability of hash collisions. Therefore, hash table expansion can mitigate hash collisions. Similar to array expansion, hash table expansion is costly. +- The load factor, defined as the number of elements divided by the number of buckets, reflects the severity of hash collisions and is often used as a condition to trigger hash table expansion. +- Separate chaining addresses hash collisions by converting each element into a linked list, storing all colliding elements in the same linked list. However, excessively long linked lists can reduce query efficiency, which can be improved by converting the linked lists into red-black trees. +- Open addressing handles hash collisions through multiple probing. Linear probing uses a fixed step size but cannot delete elements and is prone to clustering. Double hashing uses multiple hash functions for probing, which reduces clustering compared to linear probing but increases computational overhead. +- Different programming languages adopt various hash table implementations. For example, Java's `HashMap` uses separate chaining, while Python's `dict` employs open addressing. - In hash tables, we desire hash algorithms with determinism, high efficiency, and uniform distribution. In cryptography, hash algorithms should also possess collision resistance and the avalanche effect. -- Hash algorithms typically use large prime numbers as moduli to ensure uniform distribution of hash values and reduce hash collisions. +- Hash algorithms typically use large prime numbers as moduli to maximize the uniform distribution of hash values and reduce hash collisions. - Common hash algorithms include MD5, SHA-1, SHA-2, and SHA-3. MD5 is often used for file integrity checks, while SHA-2 is commonly used in secure applications and protocols. - Programming languages usually provide built-in hash algorithms for data types to calculate bucket indices in hash tables. Generally, only immutable objects are hashable. @@ -36,16 +36,16 @@ Firstly, hash tables have higher time efficiency but lower space efficiency. A s Secondly, hash tables are only more time-efficient in specific use cases. If a feature can be implemented with the same time complexity using an array or a linked list, it's usually faster than using a hash table. This is because the computation of the hash function incurs overhead, making the constant factor in the time complexity larger. -Lastly, the time complexity of hash tables can degrade. For example, in chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to $O(n)$ time. +Lastly, the time complexity of hash tables can degrade. For example, in separate chaining, we perform search operations in a linked list or red-black tree, which still risks degrading to $O(n)$ time. -**Q**: Does multiple hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused? +**Q**: Does double hashing also have the flaw of not being able to delete elements directly? Can space marked as deleted be reused? -Multiple hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space. +Double hashing is a form of open addressing, and all open addressing methods have the drawback of not being able to delete elements directly; they require marking elements as deleted. Marked spaces can be reused. When inserting new elements into the hash table, and the hash function points to a position marked as deleted, that position can be used by the new element. This maintains the probing sequence of the hash table while ensuring efficient use of space. **Q**: Why do hash collisions occur during the search process in linear probing? -During the search process, the hash function points to the corresponding bucket and key-value pair. If the `key` doesn't match, it indicates a hash collision. Therefore, linear probing will search downwards at a predetermined step size until the correct key-value pair is found or the search fails. +During the search process, the hash function points to the corresponding bucket and key-value pair. If the `key` doesn't match, it indicates a hash collision. Therefore, linear probing will search downward at a predetermined step size until the correct key-value pair is found or the search fails. -**Q**: Why can resizing a hash table alleviate hash collisions? +**Q**: Why can expanding a hash table alleviate hash collisions? -The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When resizing, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after resizing, thereby mitigating hash collisions. +The last step of a hash function often involves taking the modulo of the array length $n$, to keep the output within the array index range. When expanding, the array length $n$ changes, and the indices corresponding to the keys may also change. Keys that were previously mapped to the same bucket might be distributed across multiple buckets after expansion, thereby mitigating hash collisions. diff --git a/en/docs/chapter_heap/build_heap.md b/en/docs/chapter_heap/build_heap.md index 131d73437..f1f7b837e 100644 --- a/en/docs/chapter_heap/build_heap.md +++ b/en/docs/chapter_heap/build_heap.md @@ -2,39 +2,39 @@ comments: true --- -# 8.2   Heap construction operation +# 8.2   Heap Construction Operation -In some cases, we want to build a heap using all elements of a list, and this process is known as "heap construction operation." +In some cases, we want to build a heap using all elements of a list, and this process is called "heap construction operation." -## 8.2.1   Implementing with heap insertion operation +## 8.2.1   Implementing with Element Insertion -First, we create an empty heap and then iterate through the list, performing the "heap insertion operation" on each element in turn. This means adding the element to the end of the heap and then "heapifying" it from bottom to top. +We first create an empty heap, then iterate through the list, performing the "element insertion operation" on each element in sequence. This means adding the element to the bottom of the heap and then performing "bottom-to-top" heapify on that element. -Each time an element is added to the heap, the length of the heap increases by one. Since nodes are added to the binary tree from top to bottom, the heap is constructed "from top to bottom." +Each time an element is inserted into the heap, the heap's length increases by one. Since nodes are added to the binary tree sequentially from top to bottom, the heap is constructed "from top to bottom." -Let the number of elements be $n$, and each element's insertion operation takes $O(\log{n})$ time, thus the time complexity of this heap construction method is $O(n \log n)$. +Given $n$ elements, each element's insertion operation takes $O(\log{n})$ time, so the time complexity of this heap construction method is $O(n \log n)$. -## 8.2.2   Implementing by heapifying through traversal +## 8.2.2   Implementing Through Heapify Traversal -In fact, we can implement a more efficient method of heap construction in two steps. +In fact, we can implement a more efficient heap construction method in two steps. -1. Add all elements of the list as they are into the heap, at this point the properties of the heap are not yet satisfied. -2. Traverse the heap in reverse order (reverse of level-order traversal), and perform "top to bottom heapify" on each non-leaf node. +1. Add all elements of the list as-is to the heap, at which point the heap property is not yet satisfied. +2. Traverse the heap in reverse order (reverse of level-order traversal), performing "top-to-bottom heapify" on each non-leaf node in sequence. -**After heapifying a node, the subtree with that node as the root becomes a valid sub-heap**. Since the traversal is in reverse order, the heap is built "from bottom to top." +**After heapifying a node, the subtree rooted at that node becomes a valid sub-heap**. Since we traverse in reverse order, the heap is constructed "from bottom to top." -The reason for choosing reverse traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective. +The reason for choosing reverse order traversal is that it ensures the subtree below the current node is already a valid sub-heap, making the heapification of the current node effective. -It's worth mentioning that **since leaf nodes have no children, they naturally form valid sub-heaps and do not need to be heapified**. As shown in the following code, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification: +It's worth noting that **since leaf nodes have no children, they are naturally valid sub-heaps and do not require heapification**. As shown in the code below, the last non-leaf node is the parent of the last node; we start from it and traverse in reverse order to perform heapification: === "Python" ```python title="my_heap.py" def __init__(self, nums: list[int]): """Constructor, build heap based on input list""" - # Add all list elements into the heap + # Add list elements to heap as is self.max_heap = nums - # Heapify all nodes except leaves + # Heapify all nodes except leaf nodes for i in range(self.parent(self.size() - 1), -1, -1): self.sift_down(i) ``` @@ -44,9 +44,9 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f ```cpp title="my_heap.cpp" /* Constructor, build heap based on input list */ MaxHeap(vector nums) { - // Add all list elements into the heap + // Add list elements to heap as is maxHeap = nums; - // Heapify all nodes except leaves + // Heapify all nodes except leaf nodes for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -58,9 +58,9 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f ```java title="my_heap.java" /* Constructor, build heap based on input list */ MaxHeap(List nums) { - // Add all list elements into the heap + // Add list elements to heap as is maxHeap = new ArrayList<>(nums); - // Heapify all nodes except leaves + // Heapify all nodes except leaf nodes for (int i = parent(size() - 1); i >= 0; i--) { siftDown(i); } @@ -70,106 +70,295 @@ It's worth mentioning that **since leaf nodes have no children, they naturally f === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{MaxHeap} + /* Constructor, build heap from input list */ + MaxHeap(IEnumerable nums) { + // Add list elements to heap as is + maxHeap = new List(nums); + // Heapify all nodes except leaf nodes + var size = Parent(this.Size() - 1); + for (int i = size; i >= 0; i--) { + SiftDown(i); + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{newMaxHeap} + /* Constructor, build heap from slice */ + func newMaxHeap(nums []any) *maxHeap { + // Add list elements to heap as is + h := &maxHeap{data: nums} + for i := h.parent(len(h.data) - 1); i >= 0; i-- { + // Heapify all nodes except leaf nodes + h.siftDown(i) + } + return h + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{init} + /* Constructor, build heap based on input list */ + init(nums: [Int]) { + // Add list elements to heap as is + maxHeap = nums + // Heapify all nodes except leaf nodes + for i in (0 ... parent(i: size() - 1)).reversed() { + siftDown(i: i) + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{constructor} + /* Constructor, build empty heap or build heap from input list */ + constructor(nums) { + // Add list elements to heap as is + this.#maxHeap = nums === undefined ? [] : [...nums]; + // Heapify all nodes except leaf nodes + for (let i = this.#parent(this.size() - 1); i >= 0; i--) { + this.#siftDown(i); + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{constructor} + /* Constructor, build empty heap or build heap from input list */ + constructor(nums?: number[]) { + // Add list elements to heap as is + this.maxHeap = nums === undefined ? [] : [...nums]; + // Heapify all nodes except leaf nodes + for (let i = this.parent(this.size() - 1); i >= 0; i--) { + this.siftDown(i); + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{MaxHeap} + /* Constructor, build heap based on input list */ + MaxHeap(List nums) { + // Add list elements to heap as is + _maxHeap = nums; + // Heapify all nodes except leaf nodes + for (int i = _parent(size() - 1); i >= 0; i--) { + siftDown(i); + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{new} + /* Constructor, build heap based on input list */ + fn new(nums: Vec) -> Self { + // Add list elements to heap as is + let mut heap = MaxHeap { max_heap: nums }; + // Heapify all nodes except leaf nodes + for i in (0..=Self::parent(heap.size() - 1)).rev() { + heap.sift_down(i); + } + heap + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{newMaxHeap} + /* Constructor, build heap from slice */ + MaxHeap *newMaxHeap(int nums[], int size) { + // Push all elements to heap + MaxHeap *maxHeap = (MaxHeap *)malloc(sizeof(MaxHeap)); + maxHeap->size = size; + memcpy(maxHeap->data, nums, size * sizeof(int)); + for (int i = parent(maxHeap, size - 1); i >= 0; i--) { + // Heapify all nodes except leaf nodes + siftDown(maxHeap, i); + } + return maxHeap; + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{} + /* Max heap */ + class MaxHeap(nums: MutableList?) { + // Use list instead of array, no need to consider capacity expansion + private val maxHeap = mutableListOf() + + /* Constructor, build heap based on input list */ + init { + // Add list elements to heap as is + maxHeap.addAll(nums!!) + // Heapify all nodes except leaf nodes + for (i in parent(size() - 1) downTo 0) { + siftDown(i) + } + } + + /* Get index of left child node */ + private fun left(i: Int): Int { + return 2 * i + 1 + } + + /* Get index of right child node */ + private fun right(i: Int): Int { + return 2 * i + 2 + } + + /* Get index of parent node */ + private fun parent(i: Int): Int { + return (i - 1) / 2 // Floor division + } + + /* Swap elements */ + private fun swap(i: Int, j: Int) { + val temp = maxHeap[i] + maxHeap[i] = maxHeap[j] + maxHeap[j] = temp + } + + /* Get heap size */ + fun size(): Int { + return maxHeap.size + } + + /* Check if heap is empty */ + fun isEmpty(): Boolean { + /* Check if heap is empty */ + return size() == 0 + } + + /* Access top element */ + fun peek(): Int { + return maxHeap[0] + } + + /* Element enters heap */ + fun push(_val: Int) { + // Add node + maxHeap.add(_val) + // Heapify from bottom to top + siftUp(size() - 1) + } + + /* Starting from node i, heapify from bottom to top */ + private fun siftUp(it: Int) { + // Kotlin function parameters are immutable, so create temporary variable + var i = it + while (true) { + // Get parent node of node i + val p = parent(i) + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || maxHeap[i] <= maxHeap[p]) break + // Swap two nodes + swap(i, p) + // Loop upward heapify + i = p + } + } + + /* Element exits heap */ + fun pop(): Int { + // Handle empty case + if (isEmpty()) throw IndexOutOfBoundsException() + // Delete node + swap(0, size() - 1) + // Remove node + val _val = maxHeap.removeAt(size() - 1) + // Return top element + siftDown(0) + // Return heap top element + return _val + } + + /* Starting from node i, heapify from top to bottom */ + private fun siftDown(it: Int) { + // Kotlin function parameters are immutable, so create temporary variable + var i = it + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + val l = left(i) + val r = right(i) + var ma = i + if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l + if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r + // Swap two nodes + if (ma == i) break + // Swap two nodes + swap(i, ma) + // Loop downwards heapification + i = ma + } + } + + /* Driver Code */ + fun print() { + val queue = PriorityQueue { a: Int, b: Int -> b - a } + queue.addAll(maxHeap) + printHeap(queue) + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{initialize} + ### Constructor, build heap from input list ### + def initialize(nums) + # Add list elements to heap as is + @max_heap = nums + # Heapify all nodes except leaf nodes + parent(size - 1).downto(0) do |i| + sift_down(i) + end + end ``` -=== "Zig" +## 8.2.3   Complexity Analysis - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{init} - ``` +Next, let's attempt to derive the time complexity of this second heap construction method. -## 8.2.3   Complexity analysis +- Assuming the complete binary tree has $n$ nodes, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is floor division. Therefore, the number of nodes that need heapification is $(n - 1) / 2$. +- In the top-to-bottom heapify process, each node is heapified at most to the leaf nodes, so the maximum number of iterations is the binary tree height $\log n$. -Next, let's attempt to calculate the time complexity of this second method of heap construction. +Multiplying these two together, we get a time complexity of $O(n \log n)$ for the heap construction process. **However, this estimate is not accurate because it doesn't account for the property that binary trees have far more nodes at lower levels than at upper levels**. -- Assuming the number of nodes in the complete binary tree is $n$, then the number of leaf nodes is $(n + 1) / 2$, where $/$ is integer division. Therefore, the number of nodes that need to be heapified is $(n - 1) / 2$. -- In the process of "top to bottom heapification," each node is heapified to the leaf nodes at most, so the maximum number of iterations is the height of the binary tree $\log n$. +Let's perform a more accurate calculation. To reduce calculation difficulty, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. -Multiplying the two, we get the time complexity of the heap construction process as $O(n \log n)$. **But this estimate is not accurate, because it does not take into account the nature of the binary tree having far more nodes at the lower levels than at the top.** +![Node count at each level of a perfect binary tree](build_heap.assets/heapify_operations_count.png){ class="animation-figure" } -Let's perform a more accurate calculation. To simplify the calculation, assume a "perfect binary tree" with $n$ nodes and height $h$; this assumption does not affect the correctness of the result. +

Figure 8-5   Node count at each level of a perfect binary tree

-![Node counts at each level of a perfect binary tree](build_heap.assets/heapify_operations_count.png){ class="animation-figure" } - -

Figure 8-5   Node counts at each level of a perfect binary tree

- -As shown in Figure 8-5, the maximum number of iterations for a node "to be heapified from top to bottom" is equal to the distance from that node to the leaf nodes, which is precisely "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level, **to get the total number of heapification iterations for all nodes**. +As shown in Figure 8-5, the maximum number of iterations for a node's "top-to-bottom heapify" equals the distance from that node to the leaf nodes, which is precisely the "node height." Therefore, we can sum the "number of nodes $\times$ node height" at each level to **obtain the total number of heapify iterations for all nodes**. $$ T(h) = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{(h-1)}\times1 $$ -To simplify the above equation, we need to use knowledge of sequences from high school, first multiply $T(h)$ by $2$, to get: +To simplify the above expression, we need to use sequence knowledge from high school. First, multiply $T(h)$ by $2$ to get: $$ \begin{aligned} T(h) & = 2^0h + 2^1(h-1) + 2^2(h-2) + \dots + 2^{h-1}\times1 \newline -2T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^h\times1 \newline +2 T(h) & = 2^1h + 2^2(h-1) + 2^3(h-2) + \dots + 2^{h}\times1 \newline \end{aligned} $$ -By subtracting $T(h)$ from $2T(h)$ using the method of displacement, we get: +Using the method of differences, subtract the first equation $T(h)$ from the second equation $2 T(h)$ to get: $$ 2T(h) - T(h) = T(h) = -2^0h + 2^1 + 2^2 + \dots + 2^{h-1} + 2^h $$ -Observing the equation, $T(h)$ is an geometric series, which can be directly calculated using the sum formula, resulting in a time complexity of: +Observing the above expression, we find that $T(h)$ is a geometric series, which can be calculated directly using the sum formula, yielding a time complexity of: $$ \begin{aligned} @@ -179,4 +368,4 @@ T(h) & = 2 \frac{1 - 2^h}{1 - 2} - h \newline \end{aligned} $$ -Further, a perfect binary tree with height $h$ has $n = 2^{h+1} - 1$ nodes, thus the complexity is $O(2^h) = O(n)$. This calculation shows that **the time complexity of inputting a list and constructing a heap is $O(n)$, which is very efficient**. +Furthermore, a perfect binary tree with height $h$ has $n = 2^{h+1} - 1$ nodes, so the complexity is $O(2^h) = O(n)$. This derivation shows that **the time complexity of building a heap from an input list is $O(n)$, which is highly efficient**. diff --git a/en/docs/chapter_heap/heap.md b/en/docs/chapter_heap/heap.md index 967d98e89..edf3e5dd7 100644 --- a/en/docs/chapter_heap/heap.md +++ b/en/docs/chapter_heap/heap.md @@ -13,37 +13,37 @@ A heap is a complete binary tree that satisfies specific conditions and c

Figure 8-1   Min heap and max heap

-As a special case of a complete binary tree, a heap has the following characteristics: +As a special case of a complete binary tree, heaps have the following characteristics. - The bottom layer nodes are filled from left to right, and nodes in other layers are fully filled. -- The root node of the binary tree is called the "top" of the heap, and the bottom-rightmost node is called the "bottom" of the heap. -- For max heaps (min heaps), the value of the top element (root) is the largest (smallest) among all elements. +- We call the root node of the binary tree the "heap top" and the bottom-rightmost node the "heap bottom." +- For max heaps (min heaps), the value of the heap top element (root node) is the largest (smallest). -## 8.1.1   Common heap operations +## 8.1.1   Common Heap Operations It should be noted that many programming languages provide a priority queue, which is an abstract data structure defined as a queue with priority sorting. -In practice, **heaps are often used to implement priority queues. A max heap corresponds to a priority queue where elements are dequeued in descending order**. From a usage perspective, we can consider "priority queue" and "heap" as equivalent data structures. Therefore, this book does not make a special distinction between the two, uniformly referring to them as "heap." +In fact, **heaps are typically used to implement priority queues, with max heaps corresponding to priority queues where elements are dequeued in descending order**. From a usage perspective, we can regard "priority queue" and "heap" as equivalent data structures. Therefore, this book does not make a special distinction between the two and uniformly refers to them as "heap." -Common operations on heaps are shown in Table 8-1, and the method names may vary based on the programming language. +Common heap operations are shown in Table 8-1, and method names need to be determined based on the programming language.

Table 8-1   Efficiency of Heap Operations

-| Method name | Description | Time complexity | -| ----------- | ------------------------------------------------------------ | --------------- | -| `push()` | Add an element to the heap | $O(\log n)$ | -| `pop()` | Remove the top element from the heap | $O(\log n)$ | -| `peek()` | Access the top element (for max/min heap, the max/min value) | $O(1)$ | -| `size()` | Get the number of elements in the heap | $O(1)$ | -| `isEmpty()` | Check if the heap is empty | $O(1)$ | +| Method name | Description | Time complexity | +| ----------- | ----------------------------------------------------------------- | --------------- | +| `push()` | Insert an element into the heap | $O(\log n)$ | +| `pop()` | Remove the heap top element | $O(\log n)$ | +| `peek()` | Access the heap top element (max/min value for max/min heap) | $O(1)$ | +| `size()` | Get the number of elements in the heap | $O(1)$ | +| `isEmpty()` | Check if the heap is empty | $O(1)$ |
-In practice, we can directly use the heap class (or priority queue class) provided by programming languages. +In practical applications, we can directly use the heap class (or priority queue class) provided by programming languages. -Similar to sorting algorithms where we have "ascending order" and "descending order", we can switch between "min heap" and "max heap" by setting a `flag` or modifying the `Comparator`. The code is as follows: +Similar to "ascending order" and "descending order" in sorting algorithms, we can implement conversion between "min heap" and "max heap" by setting a `flag` or modifying the `Comparator`. The code is as follows: === "Python" @@ -54,8 +54,8 @@ Similar to sorting algorithms where we have "ascending order" and "descending or max_heap, flag = [], -1 # Python's heapq module implements a min heap by default - # By negating the elements before pushing them to the heap, we invert the order and thus implement a max heap - # In this example, flag = 1 corresponds to a min heap, while flag = -1 corresponds to a max heap + # Consider negating elements before pushing them to the heap, which inverts the size relationship and thus implements a max heap + # In this example, flag = 1 corresponds to a min heap, flag = -1 corresponds to a max heap # Push elements into the heap heapq.heappush(max_heap, flag * 1) @@ -64,24 +64,24 @@ Similar to sorting algorithms where we have "ascending order" and "descending or heapq.heappush(max_heap, flag * 5) heapq.heappush(max_heap, flag * 4) - # Retrieve the top element of the heap + # Get the heap top element peek: int = flag * max_heap[0] # 5 - # Pop the top element of the heap - # The popped elements will form a sequence in descending order + # Remove the heap top element + # The removed elements will form a descending sequence 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 - # Get the size of the heap + # Get the heap size size: int = len(max_heap) # Check if the heap is empty is_empty: bool = not max_heap - # Create a heap from a list + # Build a heap from an input list min_heap: list[int] = [1, 3, 2, 5, 4] heapq.heapify(min_heap) ``` @@ -102,24 +102,24 @@ Similar to sorting algorithms where we have "ascending order" and "descending or maxHeap.push(5); maxHeap.push(4); - /* Retrieve the top element of the heap */ + /* Get the heap top element */ int peek = maxHeap.top(); // 5 - /* Pop the top element of the heap */ - // The popped elements will form a sequence in descending order + /* Remove the heap top element */ + // The removed elements will form a descending sequence maxHeap.pop(); // 5 maxHeap.pop(); // 4 maxHeap.pop(); // 3 maxHeap.pop(); // 2 maxHeap.pop(); // 1 - /* Get the size of the heap */ + /* Get the heap size */ int size = maxHeap.size(); /* Check if the heap is empty */ bool isEmpty = maxHeap.empty(); - /* Create a heap from a list */ + /* Build a heap from an input list */ vector input{1, 3, 2, 5, 4}; priority_queue, greater> minHeap(input.begin(), input.end()); ``` @@ -130,34 +130,34 @@ Similar to sorting algorithms where we have "ascending order" and "descending or /* Initialize a heap */ // Initialize a min heap Queue minHeap = new PriorityQueue<>(); - // Initialize a max heap (Simply modify the Comparator using a lambda expression) + // Initialize a max heap (use lambda expression to modify Comparator) Queue maxHeap = new PriorityQueue<>((a, b) -> b - a); - + /* Push elements into the heap */ maxHeap.offer(1); maxHeap.offer(3); maxHeap.offer(2); maxHeap.offer(5); maxHeap.offer(4); - - /* Retrieve the top element of the heap */ + + /* Get the heap top element */ int peek = maxHeap.peek(); // 5 - - /* Pop the top element of the heap */ - // The popped elements will form a sequence in descending order + + /* Remove the heap top element */ + // The removed elements will form a descending sequence peek = maxHeap.poll(); // 5 peek = maxHeap.poll(); // 4 peek = maxHeap.poll(); // 3 peek = maxHeap.poll(); // 2 peek = maxHeap.poll(); // 1 - - /* Get the size of the heap */ + + /* Get the heap size */ int size = maxHeap.size(); - + /* Check if the heap is empty */ boolean isEmpty = maxHeap.isEmpty(); - - /* Create a heap from a list */ + + /* Build a heap from an input list */ minHeap = new PriorityQueue<>(Arrays.asList(1, 3, 2, 5, 4)); ``` @@ -167,8 +167,8 @@ Similar to sorting algorithms where we have "ascending order" and "descending or /* Initialize a heap */ // Initialize a min heap PriorityQueue minHeap = new(); - // Initialize a max heap (Simply modify the Comparator using a lambda expression) - PriorityQueue maxHeap = new(Comparer.Create((x, y) => y - x)); + // Initialize a max heap (use lambda expression to modify Comparer) + PriorityQueue maxHeap = new(Comparer.Create((x, y) => y.CompareTo(x))); /* Push elements into the heap */ maxHeap.Enqueue(1, 1); @@ -177,24 +177,24 @@ Similar to sorting algorithms where we have "ascending order" and "descending or maxHeap.Enqueue(5, 5); maxHeap.Enqueue(4, 4); - /* Retrieve the top element of the heap */ + /* Get the heap top element */ int peek = maxHeap.Peek();//5 - /* Pop the top element of the heap */ - // The popped elements will form a sequence in descending order + /* Remove the heap top element */ + // The removed elements will form a descending sequence peek = maxHeap.Dequeue(); // 5 peek = maxHeap.Dequeue(); // 4 peek = maxHeap.Dequeue(); // 3 peek = maxHeap.Dequeue(); // 2 peek = maxHeap.Dequeue(); // 1 - /* Get the size of the heap */ + /* Get the heap size */ int size = maxHeap.Count; /* Check if the heap is empty */ bool isEmpty = maxHeap.Count == 0; - /* Create a heap from a list */ + /* Build a heap from an input list */ minHeap = new PriorityQueue([(1, 1), (3, 3), (2, 2), (5, 5), (4, 4)]); ``` @@ -202,41 +202,41 @@ Similar to sorting algorithms where we have "ascending order" and "descending or ```go title="heap.go" // In Go, we can construct a max heap of integers by implementing heap.Interface - // Note that implementing heap.Interface requires also implementing sort.Interface + // Implementing heap.Interface also requires implementing sort.Interface type intHeap []any - // Push method of heap.Interface, which pushes an element into the heap + // Push implements the heap.Interface method for pushing an element into the heap func (h *intHeap) Push(x any) { - // Both Push and Pop use a pointer receiver - // because they not only adjust the elements of the slice but also change its length + // Push and Pop use pointer receiver as parameters + // because they not only adjust the slice contents but also modify the slice length *h = append(*h, x.(int)) } - // Pop method of heap.Interface, which removes the top element of the heap + // Pop implements the heap.Interface method for popping the heap top element func (h *intHeap) Pop() any { - // The element to pop from the heap is stored at the end + // The element to be removed is stored at the end last := (*h)[len(*h)-1] *h = (*h)[:len(*h)-1] return last } - // Len method of sort.Interface + // Len is a sort.Interface method func (h *intHeap) Len() int { return len(*h) } - // Less method of sort.Interface + // Less is a sort.Interface method func (h *intHeap) Less(i, j int) bool { - // If you want to implement a min heap, you would change this to a less-than comparison + // To implement a min heap, change this to a less-than sign return (*h)[i].(int) > (*h)[j].(int) } - // Swap method of sort.Interface + // Swap is a sort.Interface method func (h *intHeap) Swap(i, j int) { (*h)[i], (*h)[j] = (*h)[j], (*h)[i] } - // Top Retrieve the top element of the heap + // Top gets the heap top element func (h *intHeap) Top() any { return (*h)[0] } @@ -248,28 +248,28 @@ Similar to sorting algorithms where we have "ascending order" and "descending or maxHeap := &intHeap{} heap.Init(maxHeap) /* Push elements into the heap */ - // Call the methods of heap.Interface to add elements + // Call heap.Interface methods to add elements heap.Push(maxHeap, 1) heap.Push(maxHeap, 3) heap.Push(maxHeap, 2) heap.Push(maxHeap, 4) heap.Push(maxHeap, 5) - /* Retrieve the top element of the heap */ + /* Get the heap top element */ top := maxHeap.Top() - fmt.Printf("The top element of the heap is %d\n", top) + fmt.Printf("Heap top element is %d\n", top) - /* Pop the top element of the heap */ - // Call the methods of heap.Interface to remove elements + /* Remove the heap top element */ + // Call heap.Interface methods to remove elements heap.Pop(maxHeap) // 5 heap.Pop(maxHeap) // 4 heap.Pop(maxHeap) // 3 heap.Pop(maxHeap) // 2 heap.Pop(maxHeap) // 1 - /* Get the size of the heap */ + /* Get the heap size */ size := len(*maxHeap) - fmt.Printf("The number of elements in the heap is %d\n", size) + fmt.Printf("Number of heap elements is %d\n", size) /* Check if the heap is empty */ isEmpty := len(*maxHeap) == 0 @@ -281,7 +281,7 @@ Similar to sorting algorithms where we have "ascending order" and "descending or ```swift title="heap.swift" /* Initialize a heap */ - // Swift’s Heap type supports both max heaps and min heaps, and need the swift-collections library + // Swift's Heap type supports both max heaps and min heaps, and requires importing swift-collections var heap = Heap() /* Push elements into the heap */ @@ -291,23 +291,23 @@ Similar to sorting algorithms where we have "ascending order" and "descending or heap.insert(5) heap.insert(4) - /* Retrieve the top element of the heap */ + /* Get the heap top element */ var peek = heap.max()! - /* Pop the top element of the heap */ + /* Remove the heap top element */ peek = heap.removeMax() // 5 peek = heap.removeMax() // 4 peek = heap.removeMax() // 3 peek = heap.removeMax() // 2 peek = heap.removeMax() // 1 - /* Get the size of the heap */ + /* Get the heap size */ let size = heap.count /* Check if the heap is empty */ let isEmpty = heap.isEmpty - /* Create a heap from a list */ + /* Build a heap from an input list */ let heap2 = Heap([1, 3, 2, 5, 4]) ``` @@ -347,25 +347,25 @@ Similar to sorting algorithms where we have "ascending order" and "descending or max_heap.push(2); max_heap.push(5); max_heap.push(4); - - /* Retrieve the top element of the heap */ + + /* Get the heap top element */ let peek = max_heap.peek().unwrap(); // 5 - /* Pop the top element of the heap */ - // The popped elements will form a sequence in descending order + /* Remove the heap top element */ + // The removed elements will form a descending sequence 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 - /* Get the size of the heap */ + /* Get the heap size */ let size = max_heap.len(); /* Check if the heap is empty */ let is_empty = max_heap.is_empty(); - /* Create a heap from a list */ + /* Build a heap from an input list */ let min_heap = BinaryHeap::from(vec![Reverse(1), Reverse(3), Reverse(2), Reverse(5), Reverse(4)]); ``` @@ -381,70 +381,65 @@ Similar to sorting algorithms where we have "ascending order" and "descending or /* Initialize a heap */ // Initialize a min heap var minHeap = PriorityQueue() - // Initialize a max heap (Simply modify the Comparator using a lambda expression) + // Initialize a max heap (use lambda expression to modify Comparator) val maxHeap = PriorityQueue { a: Int, b: Int -> b - a } - + /* Push elements into the heap */ maxHeap.offer(1) maxHeap.offer(3) maxHeap.offer(2) maxHeap.offer(5) maxHeap.offer(4) - - /* Retrieve the top element of the heap */ + + /* Get the heap top element */ var peek = maxHeap.peek() // 5 - - /* Pop the top element of the heap */ - // The popped elements will form a sequence in descending order + + /* Remove the heap top element */ + // The removed elements will form a descending sequence peek = maxHeap.poll() // 5 peek = maxHeap.poll() // 4 peek = maxHeap.poll() // 3 peek = maxHeap.poll() // 2 peek = maxHeap.poll() // 1 - - /* Get the size of the heap */ + + /* Get the heap size */ val size = maxHeap.size - + /* Check if the heap is empty */ val isEmpty = maxHeap.isEmpty() - - /* Create a heap from a list */ + + /* Build a heap from an input list */ minHeap = PriorityQueue(mutableListOf(1, 3, 2, 5, 4)) ``` === "Ruby" ```ruby title="heap.rb" - + # Ruby does not provide a built-in Heap class ``` -=== "Zig" +??? pythontutor "Code Visualization" - ```zig title="heap.zig" +
+ - ``` +## 8.1.2   Implementation of the Heap -??? pythontutor "Code visualization" +The following implementation is of a max heap. To convert it to a min heap, simply invert all size logic comparisons (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement this on their own. - https://pythontutor.com/render.html#code=import%20heapq%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20min_heap,%20flag%20%3D%20%5B%5D,%201%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20max_heap,%20flag%20%3D%20%5B%5D,%20-1%0A%20%20%20%20%0A%20%20%20%20%23%20Python%20%E7%9A%84%20heapq%20%E6%A8%A1%E5%9D%97%E9%BB%98%E8%AE%A4%E5%AE%9E%E7%8E%B0%E5%B0%8F%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E8%80%83%E8%99%91%E5%B0%86%E2%80%9C%E5%85%83%E7%B4%A0%E5%8F%96%E8%B4%9F%E2%80%9D%E5%90%8E%E5%86%8D%E5%85%A5%E5%A0%86%EF%BC%8C%E8%BF%99%E6%A0%B7%E5%B0%B1%E5%8F%AF%E4%BB%A5%E5%B0%86%E5%A4%A7%E5%B0%8F%E5%85%B3%E7%B3%BB%E9%A2%A0%E5%80%92%EF%BC%8C%E4%BB%8E%E8%80%8C%E5%AE%9E%E7%8E%B0%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%23%20%E5%9C%A8%E6%9C%AC%E7%A4%BA%E4%BE%8B%E4%B8%AD%EF%BC%8Cflag%20%3D%201%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%B0%8F%E9%A1%B6%E5%A0%86%EF%BC%8Cflag%20%3D%20-1%20%E6%97%B6%E5%AF%B9%E5%BA%94%E5%A4%A7%E9%A1%B6%E5%A0%86%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E5%A0%86%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%201%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%203%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%202%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%205%29%0A%20%20%20%20heapq.heappush%28max_heap,%20flag%20*%204%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20flag%20*%20max_heap%5B0%5D%20%23%205%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%A0%86%E9%A1%B6%E5%85%83%E7%B4%A0%E5%87%BA%E5%A0%86%0A%20%20%20%20%23%20%E5%87%BA%E5%A0%86%E5%85%83%E7%B4%A0%E4%BC%9A%E5%BD%A2%E6%88%90%E4%B8%80%E4%B8%AA%E4%BB%8E%E5%A4%A7%E5%88%B0%E5%B0%8F%E7%9A%84%E5%BA%8F%E5%88%97%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%205%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%204%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%203%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%202%0A%20%20%20%20val%20%3D%20flag%20*%20heapq.heappop%28max_heap%29%20%23%201%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%A0%86%E5%A4%A7%E5%B0%8F%0A%20%20%20%20size%20%3D%20len%28max_heap%29%0A%20%20%20%20%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%A0%86%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20not%20max_heap%0A%20%20%20%20%0A%20%20%20%20%23%20%E8%BE%93%E5%85%A5%E5%88%97%E8%A1%A8%E5%B9%B6%E5%BB%BA%E5%A0%86%0A%20%20%20%20min_heap%20%3D%20%5B1,%203,%202,%205,%204%5D%0A%20%20%20%20heapq.heapify%28min_heap%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +### 1.   Heap Storage and Representation -## 8.1.2   Implementation of the heap +As mentioned in the "Binary Tree" chapter, complete binary trees are well-suited for array representation. Since heaps are a type of complete binary tree, **we will use arrays to store heaps**. -The following implementation is of a max heap. To convert it into a min heap, simply invert all size logic comparisons (for example, replace $\geq$ with $\leq$). Interested readers are encouraged to implement it on their own. +When representing a binary tree with an array, elements represent node values, and indexes represent node positions in the binary tree. **Node pointers are implemented through index mapping formulas**. -### 1.   Heap storage and representation - -As mentioned in the "Binary Trees" section, complete binary trees are highly suitable for array representation. Since heaps are a type of complete binary tree, **we will use arrays to store heaps**. - -When using an array to represent a binary tree, elements represent node values, and indexes represent node positions in the binary tree. **Node pointers are implemented through an index mapping formula**. - -As shown in Figure 8-2, given an index $i$, the index of its left child is $2i + 1$, the index of its right child is $2i + 2$, and the index of its parent is $(i - 1) / 2$ (floor division). When the index is out of bounds, it signifies a null node or the node does not exist. +As shown in Figure 8-2, given an index $i$, the index of its left child is $2i + 1$, the index of its right child is $2i + 2$, and the index of its parent is $(i - 1) / 2$ (floor division). When an index is out of bounds, it indicates a null node or that the node does not exist. ![Representation and storage of heaps](heap.assets/representation_of_heap.png){ class="animation-figure" }

Figure 8-2   Representation and storage of heaps

-We can encapsulate the index mapping formula into functions for convenient later use: +We can encapsulate the index mapping formula into functions for convenient subsequent use: === "Python" @@ -459,7 +454,7 @@ We can encapsulate the index mapping formula into functions for convenient later def parent(self, i: int) -> int: """Get index of parent node""" - return (i - 1) // 2 # Integer division down + return (i - 1) // 2 # Floor division ``` === "C++" @@ -477,7 +472,7 @@ We can encapsulate the index mapping formula into functions for convenient later /* Get index of parent node */ int parent(int i) { - return (i - 1) / 2; // Integer division down + return (i - 1) / 2; // Floor division } ``` @@ -496,136 +491,217 @@ We can encapsulate the index mapping formula into functions for convenient later /* Get index of parent node */ int parent(int i) { - return (i - 1) / 2; // Integer division down + return (i - 1) / 2; // Floor division } ``` === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Left} + /* Get index of left child node */ + int Left(int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{Right} + /* Get index of right child node */ + int Right(int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{Parent} + /* Get index of parent node */ + int Parent(int i) { + return (i - 1) / 2; // Floor division + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{left} + /* Get index of left child node */ + func (h *maxHeap) left(i int) int { + return 2*i + 1 + } - [class]{maxHeap}-[func]{right} + /* Get index of right child node */ + func (h *maxHeap) right(i int) int { + return 2*i + 2 + } - [class]{maxHeap}-[func]{parent} + /* Get index of parent node */ + func (h *maxHeap) parent(i int) int { + // Floor division + return (i - 1) / 2 + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + func left(i: Int) -> Int { + 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + func right(i: Int) -> Int { + 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + func parent(i: Int) -> Int { + (i - 1) / 2 // Floor division + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + #left(i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + #right(i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + #parent(i) { + return Math.floor((i - 1) / 2); // Floor division + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + left(i: number): number { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + right(i: number): number { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // Floor division + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{_left} + /* Get index of left child node */ + int _left(int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{_right} + /* Get index of right child node */ + int _right(int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{_parent} + /* Get index of parent node */ + int _parent(int i) { + return (i - 1) ~/ 2; // Floor division + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + fn left(i: usize) -> usize { + 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + fn right(i: usize) -> usize { + 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + fn parent(i: usize) -> usize { + (i - 1) / 2 // Floor division + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + int left(MaxHeap *maxHeap, int i) { + return 2 * i + 1; + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + int right(MaxHeap *maxHeap, int i) { + return 2 * i + 2; + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + int parent(MaxHeap *maxHeap, int i) { + return (i - 1) / 2; // Round down + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{left} + /* Get index of left child node */ + fun left(i: Int): Int { + return 2 * i + 1 + } - [class]{MaxHeap}-[func]{right} + /* Get index of right child node */ + fun right(i: Int): Int { + return 2 * i + 2 + } - [class]{MaxHeap}-[func]{parent} + /* Get index of parent node */ + fun parent(i: Int): Int { + return (i - 1) / 2 // Floor division + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{left} + ### Get left child index ### + def left(i) + 2 * i + 1 + end - [class]{MaxHeap}-[func]{right} + ### Get right child index ### + def right(i) + 2 * i + 2 + end - [class]{MaxHeap}-[func]{parent} + ### Get parent node index ### + def parent(i) + (i - 1) / 2 # Floor division + end ``` -=== "Zig" +### 2.   Accessing the Heap Top Element - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{left} - - [class]{MaxHeap}-[func]{right} - - [class]{MaxHeap}-[func]{parent} - ``` - -### 2.   Accessing the top element of the heap - -The top element of the heap is the root node of the binary tree, which is also the first element of the list: +The heap top element is the root node of the binary tree, which is also the first element of the list: === "Python" ```python title="my_heap.py" def peek(self) -> int: - """Access heap top element""" + """Access top element""" return self.max_heap[0] ``` === "C++" ```cpp title="my_heap.cpp" - /* Access heap top element */ + /* Access top element */ int peek() { return maxHeap[0]; } @@ -634,7 +710,7 @@ The top element of the heap is the root node of the binary tree, which is also t === "Java" ```java title="my_heap.java" - /* Access heap top element */ + /* Access top element */ int peek() { return maxHeap.get(0); } @@ -643,77 +719,101 @@ The top element of the heap is the root node of the binary tree, which is also t === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Peek} + /* Access top element */ + int Peek() { + return maxHeap[0]; + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{peek} + /* Access top element */ + func (h *maxHeap) peek() any { + return h.data[0] + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + func peek() -> Int { + maxHeap[0] + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + peek() { + return this.#maxHeap[0]; + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + peek(): number { + return this.maxHeap[0]; + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + int peek() { + return _maxHeap[0]; + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + fn peek(&self) -> Option { + self.max_heap.first().copied() + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + int peek(MaxHeap *maxHeap) { + return maxHeap->data[0]; + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{peek} + /* Access top element */ + fun peek(): Int { + return maxHeap[0] + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{peek} + ### Access heap top element ### + def peek + @max_heap[0] + end ``` -=== "Zig" +### 3.   Inserting an Element Into the Heap - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{peek} - ``` +Given an element `val`, we first add it to the bottom of the heap. After addition, since `val` may be larger than other elements in the heap, the heap's property may be violated. **Therefore, it's necessary to repair the path from the inserted node to the root node**. This operation is called heapify. -### 3.   Inserting an element into the heap - -Given an element `val`, we first add it to the bottom of the heap. After addition, since `val` may be larger than other elements in the heap, the heap's integrity might be compromised, **thus it's necessary to repair the path from the inserted node to the root node**. This operation is called heapify. - -Considering starting from the node inserted, **perform heapify from bottom to top**. As shown in Figure 8-3, we compare the value of the inserted node with its parent node, and if the inserted node is larger, we swap them. Then continue this operation, repairing each node in the heap from bottom to top until reaching the root or a node that does not need swapping. +Starting from the inserted node, **perform heapify from bottom to top**. As shown in Figure 8-3, we compare the inserted node with its parent node, and if the inserted node is larger, swap them. Then continue this operation, repairing nodes in the heap from bottom to top until we pass the root node or encounter a node that does not need swapping. === "<1>" - ![Steps of element insertion into the heap](heap.assets/heap_push_step1.png){ class="animation-figure" } + ![Steps of inserting an element into the heap](heap.assets/heap_push_step1.png){ class="animation-figure" } === "<2>" ![heap_push_step2](heap.assets/heap_push_step2.png){ class="animation-figure" } @@ -739,38 +839,38 @@ Considering starting from the node inserted, **perform heapify from bottom to to === "<9>" ![heap_push_step9](heap.assets/heap_push_step9.png){ class="animation-figure" } -

Figure 8-3   Steps of element insertion into the heap

+

Figure 8-3   Steps of inserting an element into the heap

-Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the loop iterations for the heapify operation are at most $O(\log n)$, **making the time complexity of the element insertion operation $O(\log n)$**. The code is as shown: +Given a total of $n$ nodes, the tree height is $O(\log n)$. Thus, the number of loop iterations in the heapify operation is at most $O(\log n)$, **making the time complexity of the element insertion operation $O(\log n)$**. The code is as follows: === "Python" ```python title="my_heap.py" def push(self, val: int): - """Push the element into heap""" + """Element enters heap""" # Add node self.max_heap.append(val) # Heapify from bottom to top self.sift_up(self.size() - 1) def sift_up(self, i: int): - """Start heapifying node i, from bottom to top""" + """Starting from node i, heapify from bottom to top""" while True: # Get parent node of node i p = self.parent(i) - # When "crossing the root node" or "node does not need repair", end heapification + # When "crossing root node" or "node needs no repair", end heapify if p < 0 or self.max_heap[i] <= self.max_heap[p]: break # Swap two nodes self.swap(i, p) - # Loop upwards heapification + # Loop upward heapify i = p ``` === "C++" ```cpp title="my_heap.cpp" - /* Push the element into heap */ + /* Element enters heap */ void push(int val) { // Add node maxHeap.push_back(val); @@ -778,17 +878,17 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo siftUp(size() - 1); } - /* Start heapifying node i, from bottom to top */ + /* Starting from node i, heapify from bottom to top */ void siftUp(int i) { while (true) { // Get parent node of node i int p = parent(i); - // When "crossing the root node" or "node does not need repair", end heapification + // When "crossing root node" or "node needs no repair", end heapify if (p < 0 || maxHeap[i] <= maxHeap[p]) break; // Swap two nodes swap(maxHeap[i], maxHeap[p]); - // Loop upwards heapification + // Loop upward heapify i = p; } } @@ -797,7 +897,7 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo === "Java" ```java title="my_heap.java" - /* Push the element into heap */ + /* Element enters heap */ void push(int val) { // Add node maxHeap.add(val); @@ -805,17 +905,17 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo siftUp(size() - 1); } - /* Start heapifying node i, from bottom to top */ + /* Starting from node i, heapify from bottom to top */ void siftUp(int i) { while (true) { // Get parent node of node i int p = parent(i); - // When "crossing the root node" or "node does not need repair", end heapification + // When "crossing root node" or "node needs no repair", end heapify if (p < 0 || maxHeap.get(i) <= maxHeap.get(p)) break; // Swap two nodes swap(i, p); - // Loop upwards heapification + // Loop upward heapify i = p; } } @@ -824,103 +924,300 @@ Given a total of $n$ nodes, the height of the tree is $O(\log n)$. Hence, the lo === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Push} + /* Element enters heap */ + void Push(int val) { + // Add node + maxHeap.Add(val); + // Heapify from bottom to top + SiftUp(Size() - 1); + } - [class]{MaxHeap}-[func]{SiftUp} + /* Starting from node i, heapify from bottom to top */ + void SiftUp(int i) { + while (true) { + // Get parent node of node i + int p = Parent(i); + // If 'past root node' or 'node needs no repair', end heapify + if (p < 0 || maxHeap[i] <= maxHeap[p]) + break; + // Swap two nodes + Swap(i, p); + // Loop upward heapify + i = p; + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{push} + /* Element enters heap */ + func (h *maxHeap) push(val any) { + // Add node + h.data = append(h.data, val) + // Heapify from bottom to top + h.siftUp(len(h.data) - 1) + } - [class]{maxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + func (h *maxHeap) siftUp(i int) { + for true { + // Get parent node of node i + p := h.parent(i) + // When "crossing root node" or "node needs no repair", end heapify + if p < 0 || h.data[i].(int) <= h.data[p].(int) { + break + } + // Swap two nodes + h.swap(i, p) + // Loop upward heapify + i = p + } + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + func push(val: Int) { + // Add node + maxHeap.append(val) + // Heapify from bottom to top + siftUp(i: size() - 1) + } - [class]{MaxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + func siftUp(i: Int) { + var i = i + while true { + // Get parent node of node i + let p = parent(i: i) + // When "crossing root node" or "node needs no repair", end heapify + if p < 0 || maxHeap[i] <= maxHeap[p] { + break + } + // Swap two nodes + swap(i: i, j: p) + // Loop upward heapify + i = p + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + push(val) { + // Add node + this.#maxHeap.push(val); + // Heapify from bottom to top + this.#siftUp(this.size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + #siftUp(i) { + while (true) { + // Get parent node of node i + const p = this.#parent(i); + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || this.#maxHeap[i] <= this.#maxHeap[p]) break; + // Swap two nodes + this.#swap(i, p); + // Loop upward heapify + i = p; + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + push(val: number): void { + // Add node + this.maxHeap.push(val); + // Heapify from bottom to top + this.siftUp(this.size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + siftUp(i: number): void { + while (true) { + // Get parent node of node i + const p = this.parent(i); + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || this.maxHeap[i] <= this.maxHeap[p]) break; + // Swap two nodes + this.swap(i, p); + // Loop upward heapify + i = p; + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + void push(int val) { + // Add node + _maxHeap.add(val); + // Heapify from bottom to top + siftUp(size() - 1); + } - [class]{MaxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + void siftUp(int i) { + while (true) { + // Get parent node of node i + int p = _parent(i); + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || _maxHeap[i] <= _maxHeap[p]) { + break; + } + // Swap two nodes + _swap(i, p); + // Loop upward heapify + i = p; + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + fn push(&mut self, val: i32) { + // Add node + self.max_heap.push(val); + // Heapify from bottom to top + self.sift_up(self.size() - 1); + } - [class]{MaxHeap}-[func]{sift_up} + /* Starting from node i, heapify from bottom to top */ + fn sift_up(&mut self, mut i: usize) { + loop { + // Node i is already the heap root, end heapification + if i == 0 { + break; + } + // Get parent node of node i + let p = Self::parent(i); + // When "node needs no repair", end heapification + if self.max_heap[i] <= self.max_heap[p] { + break; + } + // Swap two nodes + self.swap(i, p); + // Loop upward heapify + i = p; + } + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + void push(MaxHeap *maxHeap, int val) { + // By default, should not add this many nodes + if (maxHeap->size == MAX_SIZE) { + printf("heap is full!"); + return; + } + // Add node + maxHeap->data[maxHeap->size] = val; + maxHeap->size++; - [class]{MaxHeap}-[func]{siftUp} + // Heapify from bottom to top + siftUp(maxHeap, maxHeap->size - 1); + } + + /* Starting from node i, heapify from bottom to top */ + void siftUp(MaxHeap *maxHeap, int i) { + while (true) { + // Get parent node of node i + int p = parent(maxHeap, i); + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || maxHeap->data[i] <= maxHeap->data[p]) { + break; + } + // Swap two nodes + swap(maxHeap, i, p); + // Loop upward heapify + i = p; + } + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{push} + /* Element enters heap */ + fun push(_val: Int) { + // Add node + maxHeap.add(_val) + // Heapify from bottom to top + siftUp(size() - 1) + } - [class]{MaxHeap}-[func]{siftUp} + /* Starting from node i, heapify from bottom to top */ + fun siftUp(it: Int) { + // Kotlin function parameters are immutable, so create temporary variable + var i = it + while (true) { + // Get parent node of node i + val p = parent(i) + // When "crossing root node" or "node needs no repair", end heapify + if (p < 0 || maxHeap[i] <= maxHeap[p]) break + // Swap two nodes + swap(i, p) + // Loop upward heapify + i = p + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{push} + ### Push element to heap ### + def push(val) + # Add node + @max_heap << val + # Heapify from bottom to top + sift_up(size - 1) + end - [class]{MaxHeap}-[func]{sift_up} + ### Heapify from node i, bottom to top ### + def sift_up(i) + loop do + # Get parent node of node i + p = parent(i) + # When "crossing root node" or "node needs no repair", end heapify + break if p < 0 || @max_heap[i] <= @max_heap[p] + # Swap two nodes + swap(i, p) + # Loop upward heapify + i = p + end + end ``` -=== "Zig" +### 4.   Removing the Heap Top Element - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{push} +The heap top element is the root node of the binary tree, which is the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree would change, making subsequent repair with heapify difficult. To minimize changes in element indexes, we use the following steps. - [class]{MaxHeap}-[func]{siftUp} - ``` - -### 4.   Removing the top element from the heap - -The top element of the heap is the root node of the binary tree, that is, the first element of the list. If we directly remove the first element from the list, all node indexes in the binary tree will change, making it difficult to use heapify for subsequent repairs. To minimize changes in element indexes, we use the following steps. - -1. Swap the top element with the bottom element of the heap (swap the root node with the rightmost leaf node). -2. After swapping, remove the bottom of the heap from the list (note that since it has been swapped, the original top element is actually being removed). +1. Swap the heap top element with the heap bottom element (swap the root node with the rightmost leaf node). +2. After swapping, remove the heap bottom from the list (note that since we've swapped, we're actually removing the original heap top element). 3. Starting from the root node, **perform heapify from top to bottom**. -As shown in Figure 8-4, **the direction of "heapify from top to bottom" is opposite to "heapify from bottom to top"**. We compare the value of the root node with its two children and swap it with the largest child. Then, repeat this operation until reaching the leaf node or encountering a node that does not need swapping. +As shown in Figure 8-4, **the direction of "top-to-bottom heapify" is opposite to "bottom-to-top heapify"**. We compare the root node's value with its two children and swap it with the largest child. Then loop this operation until we pass a leaf node or encounter a node that doesn't need swapping. === "<1>" - ![Steps of removing the top element from the heap](heap.assets/heap_pop_step1.png){ class="animation-figure" } + ![Steps of removing the heap top element](heap.assets/heap_pop_step1.png){ class="animation-figure" } === "<2>" ![heap_pop_step2](heap.assets/heap_pop_step2.png){ class="animation-figure" } @@ -949,42 +1246,42 @@ As shown in Figure 8-4, **the direction of "heapify from top to bottom" is oppos === "<10>" ![heap_pop_step10](heap.assets/heap_pop_step10.png){ class="animation-figure" } -

Figure 8-4   Steps of removing the top element from the heap

+

Figure 8-4   Steps of removing the heap top element

-Similar to the element insertion operation, the time complexity of the top element removal operation is also $O(\log n)$. The code is as follows: +Similar to the element insertion operation, the time complexity of the heap top element removal operation is also $O(\log n)$. The code is as follows: === "Python" ```python title="my_heap.py" def pop(self) -> int: """Element exits heap""" - # Empty handling + # Handle empty case if self.is_empty(): raise IndexError("Heap is empty") - # Swap the root node with the rightmost leaf node (swap the first element with the last element) + # Swap root node with rightmost leaf node (swap first element with last element) self.swap(0, self.size() - 1) - # Remove node + # Delete node val = self.max_heap.pop() # Heapify from top to bottom self.sift_down(0) - # Return heap top element + # Return top element return val def sift_down(self, i: int): - """Start heapifying node i, from top to bottom""" + """Starting from node i, heapify from top to bottom""" while True: - # Determine the largest node among i, l, r, noted as ma + # Find node with largest value among i, l, r, denoted as 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 - # If node i is the largest or indices l, r are out of bounds, no further heapification needed, break + # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break if ma == i: break # Swap two nodes self.swap(i, ma) - # Loop downwards heapification + # Loop downward heapify i = ma ``` @@ -993,28 +1290,28 @@ Similar to the element insertion operation, the time complexity of the top eleme ```cpp title="my_heap.cpp" /* Element exits heap */ void pop() { - // Empty handling + // Handle empty case if (isEmpty()) { throw out_of_range("Heap is empty"); } - // Swap the root node with the rightmost leaf node (swap the first element with the last element) + // Delete node swap(maxHeap[0], maxHeap[size() - 1]); // Remove node maxHeap.pop_back(); - // Heapify from top to bottom + // Return top element siftDown(0); } - /* Start heapifying node i, from top to bottom */ + /* Starting from node i, heapify from top to bottom */ void siftDown(int i) { while (true) { - // Determine the largest node among i, l, r, noted as ma + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break 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; - // If node i is the largest or indices l, r are out of bounds, no further heapification needed, break + // Swap two nodes if (ma == i) break; swap(maxHeap[i], maxHeap[ma]); @@ -1029,29 +1326,29 @@ Similar to the element insertion operation, the time complexity of the top eleme ```java title="my_heap.java" /* Element exits heap */ int pop() { - // Empty handling + // Handle empty case if (isEmpty()) throw new IndexOutOfBoundsException(); - // Swap the root node with the rightmost leaf node (swap the first element with the last element) + // Delete node swap(0, size() - 1); // Remove node int val = maxHeap.remove(size() - 1); - // Heapify from top to bottom + // Return top element siftDown(0); // Return heap top element return val; } - /* Start heapifying node i, from top to bottom */ + /* Starting from node i, heapify from top to bottom */ void siftDown(int i) { while (true) { - // Determine the largest node among i, l, r, noted as ma + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break 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; - // If node i is the largest or indices l, r are out of bounds, no further heapification needed, break + // Swap two nodes if (ma == i) break; // Swap two nodes @@ -1065,93 +1362,404 @@ Similar to the element insertion operation, the time complexity of the top eleme === "C#" ```csharp title="my_heap.cs" - [class]{MaxHeap}-[func]{Pop} + /* Element exits heap */ + int Pop() { + // Handle empty case + if (IsEmpty()) + throw new IndexOutOfRangeException(); + // Delete node + Swap(0, Size() - 1); + // Remove node + int val = maxHeap.Last(); + maxHeap.RemoveAt(Size() - 1); + // Return top element + SiftDown(0); + // Return heap top element + return val; + } - [class]{MaxHeap}-[func]{SiftDown} + /* Starting from node i, heapify from top to bottom */ + void SiftDown(int i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // If 'node i is largest' or 'past leaf node', end heapify + if (ma == i) break; + // Swap two nodes + Swap(i, ma); + // Loop downwards heapification + i = ma; + } + } ``` === "Go" ```go title="my_heap.go" - [class]{maxHeap}-[func]{pop} + /* Element exits heap */ + func (h *maxHeap) pop() any { + // Handle empty case + if h.isEmpty() { + fmt.Println("error") + return nil + } + // Delete node + h.swap(0, h.size()-1) + // Remove node + val := h.data[len(h.data)-1] + h.data = h.data[:len(h.data)-1] + // Return top element + h.siftDown(0) - [class]{maxHeap}-[func]{siftDown} + // Return heap top element + return val + } + + /* Starting from node i, heapify from top to bottom */ + func (h *maxHeap) siftDown(i int) { + for true { + // Find node with maximum value among nodes i, l, r, denoted as 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 + } + // Swap two nodes + if max == i { + break + } + // Swap two nodes + h.swap(i, max) + // Loop downwards heapification + i = max + } + } ``` === "Swift" ```swift title="my_heap.swift" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + func pop() -> Int { + // Handle empty case + if isEmpty() { + fatalError("Heap is empty") + } + // Delete node + swap(i: 0, j: size() - 1) + // Remove node + let val = maxHeap.remove(at: size() - 1) + // Return top element + siftDown(i: 0) + // Return heap top element + return val + } - [class]{MaxHeap}-[func]{siftDown} + /* Starting from node i, heapify from top to bottom */ + func siftDown(i: Int) { + var i = i + while true { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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 + } + // Swap two nodes + if ma == i { + break + } + // Swap two nodes + swap(i: i, j: ma) + // Loop downwards heapification + i = ma + } + } ``` === "JS" ```javascript title="my_heap.js" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + pop() { + // Handle empty case + if (this.isEmpty()) throw new Error('Heap is empty'); + // Delete node + this.#swap(0, this.size() - 1); + // Remove node + const val = this.#maxHeap.pop(); + // Return top element + this.#siftDown(0); + // Return heap top element + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* Starting from node i, heapify from top to bottom */ + #siftDown(i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma === i) break; + // Swap two nodes + this.#swap(i, ma); + // Loop downwards heapification + i = ma; + } + } ``` === "TS" ```typescript title="my_heap.ts" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + pop(): number { + // Handle empty case + if (this.isEmpty()) throw new RangeError('Heap is empty.'); + // Delete node + this.swap(0, this.size() - 1); + // Remove node + const val = this.maxHeap.pop(); + // Return top element + this.siftDown(0); + // Return heap top element + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* Starting from node i, heapify from top to bottom */ + siftDown(i: number): void { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma === i) break; + // Swap two nodes + this.swap(i, ma); + // Loop downwards heapification + i = ma; + } + } ``` === "Dart" ```dart title="my_heap.dart" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + int pop() { + // Handle empty case + if (isEmpty()) throw Exception('Heap is empty'); + // Delete node + _swap(0, size() - 1); + // Remove node + int val = _maxHeap.removeLast(); + // Return top element + siftDown(0); + // Return heap top element + return val; + } - [class]{MaxHeap}-[func]{siftDown} + /* Starting from node i, heapify from top to bottom */ + void siftDown(int i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma == i) break; + // Swap two nodes + _swap(i, ma); + // Loop downwards heapification + i = ma; + } + } ``` === "Rust" ```rust title="my_heap.rs" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + fn pop(&mut self) -> i32 { + // Handle empty case + if self.is_empty() { + panic!("index out of bounds"); + } + // Delete node + self.swap(0, self.size() - 1); + // Remove node + let val = self.max_heap.pop().unwrap(); + // Return top element + self.sift_down(0); + // Return heap top element + val + } - [class]{MaxHeap}-[func]{sift_down} + /* Starting from node i, heapify from top to bottom */ + fn sift_down(&mut self, mut i: usize) { + loop { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + } + // Swap two nodes + if ma == i { + break; + } + // Swap two nodes + self.swap(i, ma); + // Loop downwards heapification + i = ma; + } + } ``` === "C" ```c title="my_heap.c" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + int pop(MaxHeap *maxHeap) { + // Handle empty case + if (isEmpty(maxHeap)) { + printf("heap is empty!"); + return INT_MAX; + } + // Delete node + swap(maxHeap, 0, size(maxHeap) - 1); + // Remove node + int val = maxHeap->data[maxHeap->size - 1]; + maxHeap->size--; + // Return top element + siftDown(maxHeap, 0); - [class]{MaxHeap}-[func]{siftDown} + // Return heap top element + return val; + } + + /* Starting from node i, heapify from top to bottom */ + void siftDown(MaxHeap *maxHeap, int i) { + while (true) { + // Find node with maximum value among nodes i, l, r, denoted as max + int l = left(maxHeap, i); + int r = right(maxHeap, i); + int max = i; + if (l < size(maxHeap) && maxHeap->data[l] > maxHeap->data[max]) { + max = l; + } + if (r < size(maxHeap) && maxHeap->data[r] > maxHeap->data[max]) { + max = r; + } + // Swap two nodes + if (max == i) { + break; + } + // Swap two nodes + swap(maxHeap, i, max); + // Loop downwards heapification + i = max; + } + } ``` === "Kotlin" ```kotlin title="my_heap.kt" - [class]{MaxHeap}-[func]{pop} + /* Element exits heap */ + fun pop(): Int { + // Handle empty case + if (isEmpty()) throw IndexOutOfBoundsException() + // Delete node + swap(0, size() - 1) + // Remove node + val _val = maxHeap.removeAt(size() - 1) + // Return top element + siftDown(0) + // Return heap top element + return _val + } - [class]{MaxHeap}-[func]{siftDown} + /* Starting from node i, heapify from top to bottom */ + fun siftDown(it: Int) { + // Kotlin function parameters are immutable, so create temporary variable + var i = it + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + val l = left(i) + val r = right(i) + var ma = i + if (l < size() && maxHeap[l] > maxHeap[ma]) ma = l + if (r < size() && maxHeap[r] > maxHeap[ma]) ma = r + // Swap two nodes + if (ma == i) break + // Swap two nodes + swap(i, ma) + // Loop downwards heapification + i = ma + } + } ``` === "Ruby" ```ruby title="my_heap.rb" - [class]{MaxHeap}-[func]{pop} + ### Pop element from heap ### + def pop + # Handle empty case + raise IndexError, "Heap is empty" if is_empty? + # Delete node + swap(0, size - 1) + # Remove node + val = @max_heap.pop + # Return top element + sift_down(0) + # Return heap top element + val + end - [class]{MaxHeap}-[func]{sift_down} + ### Heapify from node i, top to bottom ### + def sift_down(i) + loop do + # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + l, r, ma = left(i), right(i), i + ma = l if l < size && @max_heap[l] > @max_heap[ma] + ma = r if r < size && @max_heap[r] > @max_heap[ma] + + # Swap two nodes + break if ma == i + + # Swap two nodes + swap(i, ma) + # Loop downwards heapification + i = ma + end + end ``` -=== "Zig" +## 8.1.3   Common Applications of Heaps - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{pop} - - [class]{MaxHeap}-[func]{siftDown} - ``` - -## 8.1.3   Common applications of heaps - -- **Priority Queue**: Heaps are often the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of $O(\log n)$, and building a queue having a time complexity of $O(n)$, all of which are very efficient. -- **Heap Sort**: Given a set of data, we can create a heap from them and then continually perform element removal operations to obtain ordered data. However, there is a more elegant way to implement heap sort, as explained in the "Heap Sort" chapter. -- **Finding the Largest $k$ Elements**: This is a classic algorithm problem and also a common use case, such as selecting the top 10 hot news for Weibo hot search, picking the top 10 selling products, etc. +- **Priority queue**: Heaps are typically the preferred data structure for implementing priority queues, with both enqueue and dequeue operations having a time complexity of $O(\log n)$, and the heap construction operation having $O(n)$, all of which are highly efficient. +- **Heap sort**: Given a set of data, we can build a heap with them and then continuously perform element removal operations to obtain sorted data. However, we usually use a more elegant approach to implement heap sort, as detailed in the "Heap Sort" chapter. +- **Getting the largest $k$ elements**: This is a classic algorithm problem and also a typical application, such as selecting the top 10 trending news for Weibo hot search, selecting the top 10 best-selling products, etc. diff --git a/en/docs/chapter_heap/index.md b/en/docs/chapter_heap/index.md index dd3da074c..91242dc74 100644 --- a/en/docs/chapter_heap/index.md +++ b/en/docs/chapter_heap/index.md @@ -9,13 +9,13 @@ icon: material/family-tree !!! abstract - Heaps resemble mountains and their jagged peaks, layered and undulating, each with its unique form. + Heaps are like mountain peaks, layered and undulating, each with its unique form. - Each mountain peak rises and falls in scattered heights, yet the tallest always captures attention first. + The peaks rise and fall at varying heights, yet the tallest peak always catches the eye first. ## Chapter contents - [8.1   Heap](heap.md) -- [8.2   Building a heap](build_heap.md) -- [8.3   Top-k problem](top_k.md) +- [8.2   Building a Heap](build_heap.md) +- [8.3   Top-K Problem](top_k.md) - [8.4   Summary](summary.md) diff --git a/en/docs/chapter_heap/summary.md b/en/docs/chapter_heap/summary.md index 5a055a149..34ce94457 100644 --- a/en/docs/chapter_heap/summary.md +++ b/en/docs/chapter_heap/summary.md @@ -4,18 +4,18 @@ comments: true # 8.4   Summary -### 1.   Key review +### 1.   Key Review -- A heap is a complete binary tree that can be categorized as either a max heap or a min heap based on its building property, where the top element of a max heap is the largest and the top element of a min heap is the smallest. -- A priority queue is defined as a queue with dequeue priority, usually implemented using a heap. -- Common operations of a heap and their corresponding time complexities include: element insertion into the heap $O(\log n)$, removing the top element from the heap $O(\log n)$, and accessing the top element of the heap $O(1)$. -- A complete binary tree is well-suited to be represented by an array, thus heaps are commonly stored using arrays. -- Heapify operations are used to maintain the properties of the heap and are used in both heap insertion and removal operations. -- The time complexity of building a heap given an input of $n$ elements can be optimized to $O(n)$, which is highly efficient. +- A heap is a complete binary tree that can be categorized as a max heap or min heap based on its property. The heap top element of a max heap (min heap) is the largest (smallest). +- A priority queue is defined as a queue with priority sorting, typically implemented using heaps. +- Common heap operations and their corresponding time complexities include: element insertion $O(\log n)$, heap top element removal $O(\log n)$, and accessing the heap top element $O(1)$. +- Complete binary trees are well-suited for array representation, so we typically use arrays to store heaps. +- Heapify operations are used to maintain the heap property and are employed in both element insertion and removal operations. +- The time complexity of building a heap with $n$ input elements can be optimized to $O(n)$, which is highly efficient. - Top-k is a classic algorithm problem that can be efficiently solved using the heap data structure, with a time complexity of $O(n \log k)$. ### 2.   Q & A -**Q**: Is the "heap" in data structures the same concept as the "heap" in memory management? +**Q**: Are the "heap" in data structures and the "heap" in memory management the same concept? -The two are not the same concept, even though they are both referred to as "heap". The heap in computer system memory is part of dynamic memory allocation, where the program can use it to store data during execution. The program can request a certain amount of heap memory to store complex structures like objects and arrays. When the allocated data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, the management and usage of heap memory demands more caution, as improper use may lead to memory leaks and dangling pointers. +The two are not the same concept; they just happen to share the name "heap." The heap in computer system memory is part of dynamic memory allocation, where programs can use it to store data during runtime. Programs can request a certain amount of heap memory to store complex structures such as objects and arrays. When this data is no longer needed, the program needs to release this memory to prevent memory leaks. Compared to stack memory, heap memory management and usage require more caution, as improper use can lead to issues such as memory leaks and dangling pointers. diff --git a/en/docs/chapter_heap/top_k.md b/en/docs/chapter_heap/top_k.md index 7e635f2a7..48eb5ca3e 100644 --- a/en/docs/chapter_heap/top_k.md +++ b/en/docs/chapter_heap/top_k.md @@ -2,33 +2,33 @@ comments: true --- -# 8.3   Top-k problem +# 8.3   Top-K Problem !!! question Given an unordered array `nums` of length $n$, return the largest $k$ elements in the array. -For this problem, we will first introduce two straightforward solutions, then explain a more efficient heap-based method. +For this problem, we'll first introduce two solutions with relatively straightforward approaches, then introduce a more efficient heap-based solution. -## 8.3.1   Method 1: Iterative selection +## 8.3.1   Method 1: Iterative Selection -We can perform $k$ rounds of iterations as shown in Figure 8-6, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$. +We can perform $k$ rounds of traversal as shown in Figure 8-6, extracting the $1^{st}$, $2^{nd}$, $\dots$, $k^{th}$ largest elements in each round, with a time complexity of $O(nk)$. -This method is only suitable when $k \ll n$, as the time complexity approaches $O(n^2)$ when $k$ is close to $n$, which is very time-consuming. +This method is only suitable when $k \ll n$, because when $k$ is close to $n$, the time complexity approaches $O(n^2)$, which is very time-consuming. -![Iteratively finding the largest k elements](top_k.assets/top_k_traversal.png){ class="animation-figure" } +![Traversing to find the largest k elements](top_k.assets/top_k_traversal.png){ class="animation-figure" } -

Figure 8-6   Iteratively finding the largest k elements

+

Figure 8-6   Traversing to find the largest k elements

!!! tip - When $k = n$, we can obtain a complete ordered sequence, which is equivalent to the "selection sort" algorithm. + When $k = n$, we can obtain a complete sorted sequence, which is equivalent to the "selection sort" algorithm. ## 8.3.2   Method 2: Sorting -As shown in Figure 8-7, we can first sort the array `nums` and then return the last $k$ elements, with a time complexity of $O(n \log n)$. +As shown in Figure 8-7, we can first sort the array `nums`, then return the rightmost $k$ elements, with a time complexity of $O(n \log n)$. -Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without the need to sort the other elements. +Clearly, this method "overachieves" the task, as we only need to find the largest $k$ elements, without needing to sort the other elements. ![Sorting to find the largest k elements](top_k.assets/top_k_sorting.png){ class="animation-figure" } @@ -36,15 +36,15 @@ Clearly, this method "overachieves" the task, as we only need to find the larges ## 8.3.3   Method 3: Heap -We can solve the Top-k problem more efficiently based on heaps, as shown in the following process. +We can solve the Top-k problem more efficiently using heaps, with the process shown in Figure 8-8. -1. Initialize a min heap, where the top element is the smallest. -2. First, insert the first $k$ elements of the array into the heap. -3. Starting from the $k + 1^{th}$ element, if the current element is greater than the top element of the heap, remove the top element of the heap and insert the current element into the heap. -4. After completing the traversal, the heap contains the largest $k$ elements. +1. Initialize a min heap, where the heap top element is the smallest. +2. First, insert the first $k$ elements of the array into the heap in sequence. +3. Starting from the $(k + 1)^{th}$ element, if the current element is greater than the heap top element, remove the heap top element and insert the current element into the heap. +4. After traversal is complete, the heap contains the largest $k$ elements. === "<1>" - ![Find the largest k elements based on heap](top_k.assets/top_k_heap_step1.png){ class="animation-figure" } + ![Finding the largest k elements using a heap](top_k.assets/top_k_heap_step1.png){ class="animation-figure" } === "<2>" ![top_k_heap_step2](top_k.assets/top_k_heap_step2.png){ class="animation-figure" } @@ -70,7 +70,7 @@ We can solve the Top-k problem more efficiently based on heaps, as shown in the === "<9>" ![top_k_heap_step9](top_k.assets/top_k_heap_step9.png){ class="animation-figure" } -

Figure 8-8   Find the largest k elements based on heap

+

Figure 8-8   Finding the largest k elements using a heap

Example code is as follows: @@ -78,15 +78,15 @@ Example code is as follows: ```python title="top_k.py" def top_k_heap(nums: list[int], k: int) -> list[int]: - """Using heap to find the largest k elements in an array""" - # Initialize min-heap + """Find the largest k elements in array based on heap""" + # Initialize min heap heap = [] - # Enter the first k elements of the array into the heap + # Enter the first k elements of array into heap for i in range(k): heapq.heappush(heap, nums[i]) - # From the k+1th element, keep the heap length as k + # Starting from the (k+1)th element, maintain heap length as k for i in range(k, len(nums)): - # If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap + # If current element is greater than top element, top element exits heap, current element enters heap if nums[i] > heap[0]: heapq.heappop(heap) heapq.heappush(heap, nums[i]) @@ -96,17 +96,17 @@ Example code is as follows: === "C++" ```cpp title="top_k.cpp" - /* Using heap to find the largest k elements in an array */ + /* Find the largest k elements in array based on heap */ priority_queue, greater> topKHeap(vector &nums, int k) { - // Initialize min-heap + // Python's heapq module implements min heap by default priority_queue, greater> heap; - // Enter the first k elements of the array into the heap + // Enter the first k elements of array into heap for (int i = 0; i < k; i++) { heap.push(nums[i]); } - // From the k+1th element, keep the heap length as k + // Starting from the (k+1)th element, maintain heap length as k for (int i = k; i < nums.size(); i++) { - // If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap + // If current element is greater than top element, top element exits heap, current element enters heap if (nums[i] > heap.top()) { heap.pop(); heap.push(nums[i]); @@ -119,17 +119,17 @@ Example code is as follows: === "Java" ```java title="top_k.java" - /* Using heap to find the largest k elements in an array */ + /* Find the largest k elements in array based on heap */ Queue topKHeap(int[] nums, int k) { - // Initialize min-heap + // Python's heapq module implements min heap by default Queue heap = new PriorityQueue(); - // Enter the first k elements of the array into the heap + // Enter the first k elements of array into heap for (int i = 0; i < k; i++) { heap.offer(nums[i]); } - // From the k+1th element, keep the heap length as k + // Starting from the (k+1)th element, maintain heap length as k for (int i = k; i < nums.length; i++) { - // If the current element is larger than the heap top element, remove the heap top element and enter the current element into the heap + // If current element is greater than top element, top element exits heap, current element enters heap if (nums[i] > heap.peek()) { heap.poll(); heap.offer(nums[i]); @@ -142,93 +142,325 @@ Example code is as follows: === "C#" ```csharp title="top_k.cs" - [class]{top_k}-[func]{TopKHeap} + /* Find the largest k elements in array based on heap */ + PriorityQueue TopKHeap(int[] nums, int k) { + // Python's heapq module implements min heap by default + PriorityQueue heap = new(); + // Enter the first k elements of array into heap + for (int i = 0; i < k; i++) { + heap.Enqueue(nums[i], nums[i]); + } + // Starting from the (k+1)th element, maintain heap length as k + for (int i = k; i < nums.Length; i++) { + // If current element is greater than top element, top element exits heap, current element enters heap + if (nums[i] > heap.Peek()) { + heap.Dequeue(); + heap.Enqueue(nums[i], nums[i]); + } + } + return heap; + } ``` === "Go" ```go title="top_k.go" - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + func topKHeap(nums []int, k int) *minHeap { + // Python's heapq module implements min heap by default + h := &minHeap{} + heap.Init(h) + // Enter the first k elements of array into heap + for i := 0; i < k; i++ { + heap.Push(h, nums[i]) + } + // Starting from the (k+1)th element, maintain heap length as k + for i := k; i < len(nums); i++ { + // If current element is greater than top element, top element exits heap, current element enters heap + if nums[i] > h.Top().(int) { + heap.Pop(h) + heap.Push(h, nums[i]) + } + } + return h + } ``` === "Swift" ```swift title="top_k.swift" - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + func topKHeap(nums: [Int], k: Int) -> [Int] { + // Initialize min heap and build heap with first k elements + var heap = Heap(nums.prefix(k)) + // Starting from the (k+1)th element, maintain heap length as k + for i in nums.indices.dropFirst(k) { + // If current element is greater than top element, top element exits heap, current element enters heap + if nums[i] > heap.min()! { + _ = heap.removeMin() + heap.insert(nums[i]) + } + } + return heap.unordered + } ``` === "JS" ```javascript title="top_k.js" - [class]{}-[func]{pushMinHeap} + /* Element enters heap */ + function pushMinHeap(maxHeap, val) { + // Negate element + maxHeap.push(-val); + } - [class]{}-[func]{popMinHeap} + /* Element exits heap */ + function popMinHeap(maxHeap) { + // Negate element + return -maxHeap.pop(); + } - [class]{}-[func]{peekMinHeap} + /* Access top element */ + function peekMinHeap(maxHeap) { + // Negate element + return -maxHeap.peek(); + } - [class]{}-[func]{getMinHeap} + /* Extract elements from heap */ + function getMinHeap(maxHeap) { + // Negate element + return maxHeap.getMaxHeap().map((num) => -num); + } - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + function topKHeap(nums, k) { + // Python's heapq module implements min heap by default + // Note: We negate all heap elements to simulate min heap using max heap + const maxHeap = new MaxHeap([]); + // Enter the first k elements of array into heap + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // Starting from the (k+1)th element, maintain heap length as k + for (let i = k; i < nums.length; i++) { + // If current element is greater than top element, top element exits heap, current element enters heap + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // Return elements in heap + return getMinHeap(maxHeap); + } ``` === "TS" ```typescript title="top_k.ts" - [class]{}-[func]{pushMinHeap} + /* Element enters heap */ + function pushMinHeap(maxHeap: MaxHeap, val: number): void { + // Negate element + maxHeap.push(-val); + } - [class]{}-[func]{popMinHeap} + /* Element exits heap */ + function popMinHeap(maxHeap: MaxHeap): number { + // Negate element + return -maxHeap.pop(); + } - [class]{}-[func]{peekMinHeap} + /* Access top element */ + function peekMinHeap(maxHeap: MaxHeap): number { + // Negate element + return -maxHeap.peek(); + } - [class]{}-[func]{getMinHeap} + /* Extract elements from heap */ + function getMinHeap(maxHeap: MaxHeap): number[] { + // Negate element + return maxHeap.getMaxHeap().map((num: number) => -num); + } - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + function topKHeap(nums: number[], k: number): number[] { + // Python's heapq module implements min heap by default + // Note: We negate all heap elements to simulate min heap using max heap + const maxHeap = new MaxHeap([]); + // Enter the first k elements of array into heap + for (let i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // Starting from the (k+1)th element, maintain heap length as k + for (let i = k; i < nums.length; i++) { + // If current element is greater than top element, top element exits heap, current element enters heap + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + // Return elements in heap + return getMinHeap(maxHeap); + } ``` === "Dart" ```dart title="top_k.dart" - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + MinHeap topKHeap(List nums, int k) { + // Initialize min heap, push first k elements of array to heap + MinHeap heap = MinHeap(nums.sublist(0, k)); + // Starting from the (k+1)th element, maintain heap length as k + for (int i = k; i < nums.length; i++) { + // If current element is greater than top element, top element exits heap, current element enters heap + if (nums[i] > heap.peek()) { + heap.pop(); + heap.push(nums[i]); + } + } + return heap; + } ``` === "Rust" ```rust title="top_k.rs" - [class]{}-[func]{top_k_heap} + /* Find the largest k elements in array based on heap */ + fn top_k_heap(nums: Vec, k: usize) -> BinaryHeap> { + // BinaryHeap is a max heap, use Reverse to negate elements to implement min heap + let mut heap = BinaryHeap::>::new(); + // Enter the first k elements of array into heap + for &num in nums.iter().take(k) { + heap.push(Reverse(num)); + } + // Starting from the (k+1)th element, maintain heap length as k + for &num in nums.iter().skip(k) { + // If current element is greater than top element, top element exits heap, current element enters heap + if num > heap.peek().unwrap().0 { + heap.pop(); + heap.push(Reverse(num)); + } + } + heap + } ``` === "C" ```c title="top_k.c" - [class]{}-[func]{pushMinHeap} + /* Element enters heap */ + void pushMinHeap(MaxHeap *maxHeap, int val) { + // Negate element + push(maxHeap, -val); + } - [class]{}-[func]{popMinHeap} + /* Element exits heap */ + int popMinHeap(MaxHeap *maxHeap) { + // Negate element + return -pop(maxHeap); + } - [class]{}-[func]{peekMinHeap} + /* Access top element */ + int peekMinHeap(MaxHeap *maxHeap) { + // Negate element + return -peek(maxHeap); + } - [class]{}-[func]{getMinHeap} + /* Extract elements from heap */ + int *getMinHeap(MaxHeap *maxHeap) { + // Negate all heap elements and store in res array + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } - [class]{}-[func]{topKHeap} + /* Extract elements from heap */ + int *getMinHeap(MaxHeap *maxHeap) { + // Negate all heap elements and store in res array + int *res = (int *)malloc(maxHeap->size * sizeof(int)); + for (int i = 0; i < maxHeap->size; i++) { + res[i] = -maxHeap->data[i]; + } + return res; + } + + // Function to find k largest elements in array using heap + int *topKHeap(int *nums, int sizeNums, int k) { + // Python's heapq module implements min heap by default + // Note: We negate all heap elements to simulate min heap using max heap + int *empty = (int *)malloc(0); + MaxHeap *maxHeap = newMaxHeap(empty, 0); + // Enter the first k elements of array into heap + for (int i = 0; i < k; i++) { + pushMinHeap(maxHeap, nums[i]); + } + // Starting from the (k+1)th element, maintain heap length as k + for (int i = k; i < sizeNums; i++) { + // If current element is greater than top element, top element exits heap, current element enters heap + if (nums[i] > peekMinHeap(maxHeap)) { + popMinHeap(maxHeap); + pushMinHeap(maxHeap, nums[i]); + } + } + int *res = getMinHeap(maxHeap); + // Free memory + delMaxHeap(maxHeap); + return res; + } ``` === "Kotlin" ```kotlin title="top_k.kt" - [class]{}-[func]{topKHeap} + /* Find the largest k elements in array based on heap */ + fun topKHeap(nums: IntArray, k: Int): Queue { + // Python's heapq module implements min heap by default + val heap = PriorityQueue() + // Enter the first k elements of array into heap + for (i in 0.. heap.peek()) { + heap.poll() + heap.offer(nums[i]) + } + } + return heap + } ``` === "Ruby" ```ruby title="top_k.rb" - [class]{}-[func]{top_k_heap} + ### Find largest k elements in array using heap ### + def top_k_heap(nums, k) + # Python's heapq module implements min heap by default + # Note: We negate all heap elements to simulate min heap using max heap + max_heap = MaxHeap.new([]) + + # Enter the first k elements of array into heap + for i in 0...k + push_min_heap(max_heap, nums[i]) + end + + # Starting from the (k+1)th element, maintain heap length as k + for i in k...nums.length + # If current element is greater than top element, top element exits heap, current element enters heap + if nums[i] > peek_min_heap(max_heap) + pop_min_heap(max_heap) + push_min_heap(max_heap, nums[i]) + end + end + + get_min_heap(max_heap) + end ``` -=== "Zig" +A total of $n$ rounds of heap insertions and removals are performed, with the heap's maximum length being $k$, so the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity approaches $O(n)$; when $k$ is large, the time complexity does not exceed $O(n \log n)$. - ```zig title="top_k.zig" - [class]{}-[func]{topKHeap} - ``` - -A total of $n$ rounds of heap insertions and deletions are performed, with the maximum heap size being $k$, hence the time complexity is $O(n \log k)$. This method is very efficient; when $k$ is small, the time complexity tends towards $O(n)$; when $k$ is large, the time complexity will not exceed $O(n \log n)$. - -Additionally, this method is suitable for scenarios with dynamic data streams. By continuously adding data, we can maintain the elements within the heap, thereby achieving dynamic updates of the largest $k$ elements. +Additionally, this method is suitable for dynamic data stream scenarios. By continuously adding data, we can maintain the elements in the heap, thus achieving dynamic updates of the largest $k$ elements. diff --git a/en/docs/chapter_hello_algo/index.md b/en/docs/chapter_hello_algo/index.md index 6953f39a5..7d398ed55 100644 --- a/en/docs/chapter_hello_algo/index.md +++ b/en/docs/chapter_hello_algo/index.md @@ -3,28 +3,28 @@ comments: true icon: material/rocket-launch-outline --- -# Before starting +# Before Starting -A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most common question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question. +A few years ago, I shared the "Sword for Offer" problem solutions on LeetCode, receiving encouragement and support from many readers. During interactions with readers, the most frequently asked question I encountered was "how to get started with algorithms." Gradually, I developed a keen interest in this question. -Directly solving problems seems to be the most popular method — it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-study skills can navigate the pitfalls one by one, while those lacking a solid foundation may find themselves repeatedly stumbling and retreating in frustration. Reading through textbooks is also a common practice, but for job seekers, writing graduation thesis, submitting resumes, preparing for written tests and interviews have already consumed most of their energy, and reading thick books often becomes a daunting challenge. +Diving straight into problem-solving seems to be the most popular approach—it's simple, direct, and effective. However, problem-solving is like playing Minesweeper: those with strong self-learning abilities can successfully defuse the mines one by one, while those with insufficient foundations may end up bruised and battered, retreating step by step in frustration. Reading through textbooks is also a common practice, but for job seekers, graduation theses, resume submissions, and preparations for written tests and interviews have already consumed most of their energy, making working through thick books an arduous challenge. -If you're facing similar troubles, then this book is lucky to have found you. This book is my answer to the question. While it may not be the best solution, it is at least a positive attempt. Although this book is not enough to get you an offer directly, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and locations of different "mines", and enable you to master various "mine clearance methods". With these skills, I believe you can solve problems and read literature more comfortably, gradually building a knowledge system. +If you're facing similar struggles, then it's fortunate that this book has "found" you. This book is my answer to this question—even if it may not be the optimal solution, it is at least a positive attempt. While this book alone won't directly land you a job offer, it will guide you to explore the "knowledge map" of data structures and algorithms, help you understand the shapes, sizes, and distributions of different "mines," and enable you to master various "mine-clearing methods." With these skills, I believe you can tackle problems and read technical literature more confidently, gradually building a complete knowledge system. -I deeply agree with Professor Feynman's statement: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to your precious "attention" for this book, I will do my best and devote my greatest "attention" to write this book. +I deeply agree with Professor Feynman's words: "Knowledge isn't free. You have to pay attention." In this sense, this book is not entirely "free." In order to live up to the precious "attention" you invest in this book, I will do my utmost and devote my greatest "attention" to completing this work. -Aware of my limitations, I recognize that despite the content of this book being refined over time, errors surely remain. I sincerely welcome critiques and corrections from both teachers and students. +I'm acutely aware of my limited knowledge and shallow expertise. Although the content of this book has been refined over a period of time, there are certainly still many errors, and I sincerely welcome critiques and corrections from teachers and fellow students. -![Hello Algo](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" } +![Hello Algorithms](../assets/covers/chapter_hello_algo.jpg){ class="cover-image" }
-

Hello, Algo!

+

Hello, Algorithms!

-The advent of computers has brought significant changes to the world. With their high-speed computing power and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics of video games, the intelligent decisions in autonomous driving, the brilliant Go games of AlphaGo, or the natural interactions of ChatGPT, these applications are all exquisite demonstrations of algorithms at work on computers. +The advent of computers has brought tremendous changes to the world. With their high-speed computing capabilities and excellent programmability, they have become the ideal medium for executing algorithms and processing data. Whether it's the realistic graphics in video games, the intelligent decision-making in autonomous driving, AlphaGo's brilliant Go matches, or ChatGPT's natural interactions, these applications are all exquisite interpretations of algorithms on computers. -In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms became more refined and complex. From the exquisite craftsmanship of artisans, to industrial products that liberate productive forces, to the scientific laws governing the universe, almost every ordinary or astonishing thing has behind it the ingenious thought of algorithms. +In fact, before the advent of computers, algorithms and data structures already existed in every corner of the world. Early algorithms were relatively simple, such as ancient counting methods and tool-making procedures. As civilization progressed, algorithms gradually became more refined and complex. From the ingenious craftsmanship of master artisans, to industrial products that liberate productive forces, to the scientific laws governing the operation of the universe, behind almost every ordinary or astonishing thing lies ingenious algorithmic thinking. -Similarly, data structures are everywhere: from social networks to subway lines, many systems can be modeled as "graphs"; from a country to a family, the main forms of social organization exhibit characteristics of "trees"; winter clothes are like a "stack", where the first item worn is the last to be taken off; a badminton shuttle tube resembles a "queue", with one end for insertion and the other for retrieval; a dictionary is like a "hash table", enabling quick search for target entries. +Similarly, data structures are everywhere: from large-scale social networks to small subway systems, many systems can be modeled as "graphs"; from a nation to a family, the primary organizational forms of society exhibit characteristics of "trees"; winter clothing is like a "stack," where the first item put on is the last to be taken off; a badminton tube is like a "queue," with items inserted at one end and retrieved from the other; a dictionary is like a "hash table," enabling quick lookup of target entries. -This book aims to help readers understand the core concepts of algorithms and data structures through clear, easy-to-understand animated illustrations and runnable code examples, and to be able to implement them through programming. On this basis, this book strives to reveal the vivid manifestations of algorithms in the complex world, showcasing the beauty of algorithms. I hope this book can help you! +This book aims to help readers understand the core concepts of algorithms and data structures through clear and accessible animated illustrations and runnable code examples, and to implement them through programming. Building on this foundation, the book endeavors to reveal the vivid manifestations of algorithms in the complex world and showcase the beauty of algorithms. I hope this book can be of help to you! diff --git a/en/docs/chapter_introduction/algorithms_are_everywhere.md b/en/docs/chapter_introduction/algorithms_are_everywhere.md index 8db90c332..84fbb4c13 100644 --- a/en/docs/chapter_introduction/algorithms_are_everywhere.md +++ b/en/docs/chapter_introduction/algorithms_are_everywhere.md @@ -2,7 +2,7 @@ comments: true --- -# 1.1   Algorithms are everywhere +# 1.1   Algorithms Are Everywhere When we hear the term "algorithm," we naturally think of mathematics. However, many algorithms do not involve complex mathematics but rely more on basic logic, which can be seen everywhere in our daily lives. diff --git a/en/docs/chapter_introduction/index.md b/en/docs/chapter_introduction/index.md index 3adc9a25e..e4b68c8bd 100644 --- a/en/docs/chapter_introduction/index.md +++ b/en/docs/chapter_introduction/index.md @@ -3,18 +3,18 @@ comments: true icon: material/calculator-variant-outline --- -# Chapter 1.   Encounter with algorithms +# Chapter 1.   Encounter with Algorithms -![Encounter with algorithms](../assets/covers/chapter_introduction.jpg){ class="cover-image" } +![Encounter with Algorithms](../assets/covers/chapter_introduction.jpg){ class="cover-image" } !!! abstract - A graceful maiden dances, intertwined with the data, her skirt swaying to the melody of algorithms. - - She invites you to a dance, follow her steps, and enter the world of algorithms full of logic and beauty. + A young girl dances gracefully, intertwined with data, her skirt flowing with the melody of algorithms. + + She invites you to dance with her. Follow her steps closely and enter the world of algorithms, full of logic and beauty. ## Chapter contents -- [1.1   Algorithms are everywhere](algorithms_are_everywhere.md) -- [1.2   What is an algorithm](what_is_dsa.md) +- [1.1   Algorithms Are Everywhere](algorithms_are_everywhere.md) +- [1.2   What Is an Algorithm](what_is_dsa.md) - [1.3   Summary](summary.md) diff --git a/en/docs/chapter_introduction/summary.md b/en/docs/chapter_introduction/summary.md index fb9fd7fc2..ea82bae5b 100644 --- a/en/docs/chapter_introduction/summary.md +++ b/en/docs/chapter_introduction/summary.md @@ -4,23 +4,25 @@ comments: true # 1.3   Summary -- Algorithms are ubiquitous in daily life and are not as inaccessible and complex as they might seem. In fact, we have already unconsciously learned many algorithms to solve various problems in life. -- The principle of looking up a word in a dictionary is consistent with the binary search algorithm. The binary search algorithm embodies the important algorithmic concept of divide and conquer. -- The process of organizing playing cards is very similar to the insertion sort algorithm. The insertion sort algorithm is suitable for sorting small datasets. -- The steps of making change in currency essentially follow the greedy algorithm, where each step involves making the best possible choice at the moment. -- An algorithm is a set of step-by-step instructions for solving a specific problem within a finite time, while a data structure defines how data is organized and stored in a computer. -- Data structures and algorithms are closely linked. Data structures are the foundation of algorithms, and algorithms are the stage to utilize the functions of data structures. -- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent data structures, and the steps of assembling the blocks correspond to algorithms. +### 1.   Key Review -### 1.   Q & A +- Algorithms are ubiquitous in daily life and are not distant, esoteric knowledge. In fact, we have already learned many algorithms unconsciously and use them to solve problems big and small in life. +- The principle of looking up a dictionary is consistent with the binary search algorithm. Binary search embodies the important algorithmic idea of divide and conquer. +- The process of organizing playing cards is very similar to the insertion sort algorithm. Insertion sort is suitable for sorting small datasets. +- The steps of making change are essentially a greedy algorithm, where the best choice is made at each step based on the current situation. +- An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time, while a data structure is the way computers organize and store data. +- Data structures and algorithms are closely connected. Data structures are the foundation of algorithms, and algorithms breathe life into data structures. +- We can compare data structures and algorithms to assembling building blocks. The blocks represent data, the shape and connection method of the blocks represent the data structure, and the steps to assemble the blocks correspond to the algorithm. -**Q**:As a programmer, I’ve rarely needed to implement algorithms manually in my daily work. Most commonly used algorithms are already built into programming languages and libraries, ready to use. Does this suggest that the problems we encounter in our work haven’t yet reached the level of complexity that demands custom algorithm design? +### 2.   Q & A -If specific work skills are like the "moves" in martial arts, then fundamental subjects are more like "internal strength". +**Q**: As a programmer, I have never used algorithms to solve problems in my daily work. Common algorithms are already encapsulated by programming languages and can be used directly. Does this mean that the problems in our work have not yet reached the level where algorithms are needed? -I believe the significance of learning algorithms (and other fundamental subjects) isn’t necessarily to implement them from scratch at work, but to enable more professional decision-making and problem-solving based on a solid understanding of the concepts. This, in turn, raises the overall quality of our work. For example, every programming language provides a built-in sorting function: +If we compare specific work skills to "techniques" in martial arts, then fundamental subjects should be more like "internal skills". -- If we have not learned data structures and algorithms, then given any data, we might just give it to this sorting function. It runs smoothly, has good performance, and seems to have no problems. -- However, if we’ve studied algorithms, we understand that the time complexity of a built-in sorting function is typically $O(n \log n)$. Moreover, if the data consists of integers with a fixed number of digits (such as student IDs), we can apply a more efficient approach like radix sort, reducing the time complexity to O(nk) , where k is the number of digits. When handling large volumes of data, the time saved can turn into significant value — lowering costs, improving user experience, and enhancing system performance. +I believe the significance of learning algorithms (and other fundamental subjects) is not to implement them from scratch at work, but rather to be able to make professional reactions and judgments when solving problems based on the knowledge learned, thereby improving the overall quality of work. Here is a simple example. Every programming language has a built-in sorting function: -In engineering, many problems are difficult to solve optimally; most are addressed with ‘near-optimal’ solutions. The difficulty of a problem depends not only on its inherent complexity but also on the knowledge and experience of the person tackling it. The deeper one’s expertise and experience, the more thorough the analysis, and the more elegantly the problem can be solved. +- If we have not studied data structures and algorithms, we might simply feed any given data to this sorting function. It runs smoothly with good performance, and there doesn't seem to be any problem. +- But if we have studied algorithms, we would know that the time complexity of the built-in sorting function is $O(n \log n)$. However, if the given data consists of integers with a fixed number of digits (such as student IDs), we can use the more efficient "radix sort", reducing the time complexity to $O(nk)$, where $k$ is the number of digits. When the data volume is very large, the saved running time can create significant value (reduced costs, improved experience, etc.). + +In the field of engineering, a large number of problems are difficult to reach optimal solutions, and many problems are only solved "approximately". The difficulty of a problem depends on one hand on the nature of the problem itself, and on the other hand on the knowledge reserve of the person observing the problem. The more complete a person's knowledge and the more experience they have, the deeper their analysis of the problem will be, and the more elegantly the problem can be solved. diff --git a/en/docs/chapter_introduction/what_is_dsa.md b/en/docs/chapter_introduction/what_is_dsa.md index 02e9def94..5d7439946 100644 --- a/en/docs/chapter_introduction/what_is_dsa.md +++ b/en/docs/chapter_introduction/what_is_dsa.md @@ -2,42 +2,42 @@ comments: true --- -# 1.2   What is an algorithm +# 1.2   What Is an Algorithm -## 1.2.1   Definition of an algorithm +## 1.2.1   Algorithm Definition -An algorithm is a set of instructions or steps to solve a specific problem within a finite amount of time. It has the following characteristics: +An algorithm is a set of instructions or operational steps that solves a specific problem within a finite amount of time. It has the following characteristics. -- The problem is clearly defined, including unambiguous definitions of input and output. -- The algorithm is feasible, meaning it can be completed within a finite number of steps, time, and memory space. -- Each step has a definitive meaning. The output is consistently the same under the same inputs and conditions. +- The problem is well-defined, with clear input and output definitions. +- It is feasible and can be completed within a finite number of steps, time, and memory space. +- Each step has a definite meaning, and under the same input and operating conditions, the output is always the same. -## 1.2.2   Definition of a data structure +## 1.2.2   Data Structure Definition -A data structure is a way of organizing and storing data in a computer, with the following design goals: +A data structure is a way of organizing and storing data, covering the data content, relationships between data, and methods for data operations. It has the following design objectives. -- Minimize space occupancy to save computer memory. -- Make data operations as fast as possible, covering data access, addition, deletion, updating, etc. -- Provide concise data representation and logical information to enable efficient algorithm execution. +- Occupy as little space as possible to save computer memory. +- Data operations should be as fast as possible, covering data access, addition, deletion, update, etc. +- Provide a concise data representation and logical information so that algorithms can run efficiently. -**Designing data structures is a balancing act, often requiring trade-offs**. If you want to improve in one aspect, you often need to compromise in another. Here are two examples: +**Data structure design is a process full of trade-offs**. If we want to achieve improvements in one aspect, we often need to make compromises in another aspect. Here are two examples. -- Compared to arrays, linked lists offer more convenience in data addition and deletion but sacrifice data access speed. -- Compared with linked lists, graphs provide richer logical information but require more memory space. +- Compared to arrays, linked lists are more convenient for data addition and deletion operations but sacrifice data access speed. +- Compared to linked lists, graphs provide richer logical information but require larger memory space. -## 1.2.3   Relationship between data structures and algorithms +## 1.2.3   The Relationship Between Data Structures and Algorithms -As shown in Figure 1-4, data structures and algorithms are highly related and closely integrated, specifically in the following three aspects: +As shown in Figure 1-4, data structures and algorithms are highly related and tightly coupled, specifically manifested in the following three aspects. -- Data structures are the foundation of algorithms. They provide structured data storage and methods for manipulating data for algorithms. -- Algorithms inject vitality into data structures. The data structure alone only stores data information; it is through the application of algorithms that specific problems can be solved. -- Algorithms can often be implemented based on different data structures, but their execution efficiency can vary greatly. Choosing the right data structure is key. +- Data structures are the foundation of algorithms. Data structures provide algorithms with structured storage of data and methods for operating on data. +- Algorithms breathe life into data structures. Data structures themselves only store data information; combined with algorithms, they can solve specific problems. +- Algorithms can usually be implemented based on different data structures, but execution efficiency may vary greatly. Choosing the appropriate data structure is key. -![Relationship between data structures and algorithms](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" } +![The relationship between data structures and algorithms](what_is_dsa.assets/relationship_between_data_structure_and_algorithm.png){ class="animation-figure" } -

Figure 1-4   Relationship between data structures and algorithms

+

Figure 1-4   The relationship between data structures and algorithms

-Data structures and algorithms can be likened to a set of building blocks, as illustrated in Figure 1-5. A building block set includes numerous pieces, accompanied by detailed assembly instructions. Following these instructions step by step allows us to construct an intricate block model. +Data structures and algorithms are like assembling building blocks as shown in Figure 1-5. A set of building blocks, in addition to containing many parts, also comes with detailed assembly instructions. By following the instructions step by step, we can assemble an exquisite building block model. ![Assembling blocks](what_is_dsa.assets/assembling_blocks.png){ class="animation-figure" } @@ -45,21 +45,21 @@ Data structures and algorithms can be likened to a set of building blocks, as il The detailed correspondence between the two is shown in Table 1-1. -

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

+

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

-| Data Structures and Algorithms | Building Blocks | -| ------------------------------ | --------------------------------------------------------------- | -| Input data | Unassembled blocks | -| Data structure | Organization of blocks, including shape, size, connections, etc | -| Algorithm | A series of steps to assemble the blocks into the desired shape | -| Output data | Completed Block model | +| Data structures and algorithms | Assembling building blocks | +| ------------------------------ | ------------------------------------------------------------------ | +| Input data | Unassembled building blocks | +| Data structure | Organization form of building blocks, including shape, size, connection method, etc. | +| Algorithm | A series of operational steps to assemble the blocks into the target form | +| Output data | Building block model |
-It's worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations in multiple programming languages. +It is worth noting that data structures and algorithms are independent of programming languages. For this reason, this book is able to provide implementations based on multiple programming languages. -!!! tip "Conventional Abbreviation" +!!! tip "Conventional abbreviation" - In real-life discussions, we often refer to "Data Structures and Algorithms" simply as "Algorithms". For example, the well-known LeetCode algorithm questions actually test knowledge of both data structures and algorithms. + In actual discussions, we usually abbreviate "data structures and algorithms" as "algorithms". For example, the well-known LeetCode algorithm problems actually examine knowledge of both data structures and algorithms. diff --git a/en/docs/chapter_preface/about_the_book.md b/en/docs/chapter_preface/about_the_book.md index 1c624ecad..9d69c812c 100644 --- a/en/docs/chapter_preface/about_the_book.md +++ b/en/docs/chapter_preface/about_the_book.md @@ -2,57 +2,57 @@ comments: true --- -# 0.1   About this book +# 0.1   About This Book -This open-source project aims to create a free, and beginner-friendly crash course on data structures and algorithms. +This project aims to create an open-source, free, beginner-friendly introductory tutorial on data structures and algorithms. -- Animated illustrations, easy-to-understand content, and a smooth learning curve help beginners explore the "knowledge map" of data structures and algorithms. -- Run code with just one click, helping readers improve their programming skills and understand the working principle of algorithms and the underlying implementation of data structures. -- Promoting learning by teaching, feel free to ask questions and share insights. Let's grow together through discussion. +- The entire book uses animated illustrations, with clear and easy-to-understand content and a smooth learning curve, guiding beginners to explore the knowledge map of data structures and algorithms. +- The source code can be run with one click, helping readers improve their programming skills through practice and understand how algorithms work and the underlying implementation of data structures. +- We encourage readers to learn from each other, and everyone is welcome to ask questions and share insights in the comments section, making progress together through discussion and exchange. -## 0.1.1   Target audience +## 0.1.1   Target Audience -If you are new to algorithms with limited exposure, or you have accumulated some experience in algorithms, but you only have a vague understanding of data structures and algorithms, and you are constantly jumping between "yep" and "hmm", then this book is for you! +If you are an algorithm beginner who has never been exposed to algorithms, or if you already have some problem-solving experience and have a vague understanding of data structures and algorithms, oscillating between knowing and not knowing, then this book is tailor-made for you! -If you have already accumulated a certain amount of problem-solving experience, and are familiar with most types of problems, then this book can help you review and organize your algorithm knowledge system. The repository's source code can be used as a "problem-solving toolkit" or an "algorithm cheat sheet". +If you have already accumulated a certain amount of problem-solving experience and are familiar with most question types, this book can help you review and organize your algorithm knowledge system, and the repository's source code can be used as a "problem-solving toolkit" or "algorithm dictionary." -If you are an algorithm expert, we look forward to receiving your valuable suggestions, or [join us and collaborate](https://www.hello-algo.com/chapter_appendix/contribution/). +If you are an algorithm "expert," we look forward to receiving your valuable suggestions, or [participating in creation together](https://www.hello-algo.com/chapter_appendix/contribution/). !!! success "Prerequisites" - You should know how to write and read simple code in at least one programming language. + You need to have at least a programming foundation in any language, and be able to read and write simple code. -## 0.1.2   Content structure +## 0.1.2   Content Structure -The main content of the book is shown in Figure 0-1. +The main content of this book is shown in Figure 0-1. -- **Complexity analysis**: explores aspects and methods for evaluating data structures and algorithms. Covers methods of deriving time complexity and space complexity, along with common types and examples. -- **Data structures**: focuses on fundamental data types, classification methods, definitions, pros and cons, common operations, types, applications, and implementation methods of data structures such as array, linked list, stack, queue, hash table, tree, heap, graph, etc. -- **Algorithms**: defines algorithms, discusses their pros and cons, efficiency, application scenarios, problem-solving steps, and includes sample questions for various algorithms such as search, sorting, divide and conquer, backtracking, dynamic programming, greedy algorithms, and more. +- **Complexity analysis**: Evaluation dimensions and methods for data structures and algorithms. Methods for calculating time complexity and space complexity, common types, examples, etc. +- **Data structures**: Classification methods for basic data types and data structures. The definition, advantages and disadvantages, common operations, common types, typical applications, implementation methods, etc. of data structures such as arrays, linked lists, stacks, queues, hash tables, trees, heaps, and graphs. +- **Algorithms**: The definition, advantages and disadvantages, efficiency, application scenarios, problem-solving steps, and example problems of algorithms such as searching, sorting, divide and conquer, backtracking, dynamic programming, and greedy algorithms. -![Main content of the book](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } +![Main content of this book](about_the_book.assets/hello_algo_mindmap.png){ class="animation-figure" } -

Figure 0-1   Main content of the book

+

Figure 0-1   Main content of this book

## 0.1.3   Acknowledgements -This book is continuously improved with the joint efforts of many contributors from the open-source community. Thanks to each writer who invested their time and energy, listed in the order generated by GitHub: krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, msk397, gvenusleo, khoaxuantu, RiverTwilight, rongyi, gyt95, zhuoqinyue, K3v123, Zuoxun, mingXta, hello-ikun, FangYuan33, GN-Yu, yuelinxin, longsizhuo, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, QiLOL, pengchzn, Guanngxu, L-Super, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, longranger2, cy-by-side, xiongsp, JeffersonHuang, Transmigration-zhou, magentaqin, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, Shyam-Chen, nanlei, hongyun-robot, Phoenix0415, MolDuM, Nigh, he-weilai, junminhong, mgisr, iron-irax, yd-j, XiaChuerwu, XC-Zero, seven1240, SamJin98, wodray, reeswell, NI-SW, Horbin-Magician, Enlightenus, xjr7670, YangXuanyi, DullSword, boloboloda, iStig, qq909244296, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, liuxjerry, lucaswangdev, lyl625760, hts0000, gledfish, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, luluxia, xb534, bitsmi, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, steventimes, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, ZhongGuanbin, shanghai-Jerry, JackYang-hellobobo, Javesun99, lipusheng, BlindTerran, ShiMaRing, FreddieLi, FloranceYeh, iFleey, fanchenggang, gltianwen, goerll, Dr-XYZ, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, fanenr, eagleanurag, LifeGoesOnionOnionOnion, 52coder, foursevenlove, KorsChen, hezhizhen, linzeyan, ZJKung, GaochaoZhu, hopkings2008, yang-le, Evilrabbit520, Turing-1024-Lee, thomasq0, Suremotoo, Allen-Scai, Risuntsy, Richard-Zhang1019, qingpeng9802, primexiao, nidhoggfgg, 1ch0, MwumLi, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai and 0130w. +This book has been continuously improved through the joint efforts of many contributors in the open-source community. Thanks to every writer who invested time and effort, they are (in the order automatically generated by GitHub): krahets, coderonion, Gonglja, nuomi1, Reanon, justin-tse, hpstory, danielsss, curtishd, night-cruise, S-N-O-R-L-A-X, msk397, gvenusleo, khoaxuantu, RiverTwilight, rongyi, gyt95, zhuoqinyue, K3v123, Zuoxun, mingXta, hello-ikun, FangYuan33, GN-Yu, yuelinxin, longsizhuo, Cathay-Chen, guowei-gong, xBLACKICEx, IsChristina, JoseHung, qualifier1024, QiLOL, pengchzn, Guanngxu, L-Super, WSL0809, Slone123c, lhxsm, yuan0221, what-is-me, theNefelibatas, longranger2, cy-by-side, xiongsp, JeffersonHuang, Transmigration-zhou, magentaqin, Wonderdch, malone6, xiaomiusa87, gaofer, bluebean-cloud, a16su, Shyam-Chen, nanlei, hongyun-robot, Phoenix0415, MolDuM, Nigh, he-weilai, junminhong, mgisr, iron-irax, yd-j, XiaChuerwu, XC-Zero, seven1240, SamJin98, wodray, reeswell, NI-SW, Horbin-Magician, Enlightenus, xjr7670, YangXuanyi, DullSword, boloboloda, iStig, qq909244296, jiaxianhua, wenjianmin, keshida, kilikilikid, lclc6, lwbaptx, liuxjerry, lucaswangdev, lyl625760, hts0000, gledfish, fbigm, echo1937, szu17dmy, dshlstarr, Yucao-cy, coderlef, czruby, bongbongbakudan, beintentional, ZongYangL, ZhongYuuu, luluxia, xb534, bitsmi, ElaBosak233, baagod, zhouLion, yishangzhang, yi427, yabo083, weibk, wangwang105, th1nk3r-ing, tao363, 4yDX3906, syd168, steventimes, sslmj2020, smilelsb, siqyka, selear, sdshaoda, Xi-Row, popozhu, nuquist19, noobcodemaker, XiaoK29, chadyi, ZhongGuanbin, shanghai-Jerry, JackYang-hellobobo, Javesun99, lipusheng, BlindTerran, ShiMaRing, FreddieLi, FloranceYeh, iFleey, fanchenggang, gltianwen, goerll, Dr-XYZ, nedchu, curly210102, CuB3y0nd, KraHsu, CarrotDLaw, youshaoXG, bubble9um, fanenr, eagleanurag, LifeGoesOnionOnionOnion, 52coder, foursevenlove, KorsChen, hezhizhen, linzeyan, ZJKung, GaochaoZhu, hopkings2008, yang-le, Evilrabbit520, Turing-1024-Lee, thomasq0, Suremotoo, Allen-Scai, Risuntsy, Richard-Zhang1019, qingpeng9802, primexiao, nidhoggfgg, 1ch0, MwumLi, martinx, ZnYang2018, hugtyftg, logan-qiu, psychelzh, Keynman, KeiichiKasai and 0130w. -The code review work for this book was completed by coderonion, Gonglja, gvenusleo, hpstory, justin‐tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (listed in alphabetical order). Thanks to them for their time and effort, ensuring the standardization and uniformity of the code in various languages. +The code review work for this book was completed by coderonion, curtishd, Gonglja, gvenusleo, hpstory, justin-tse, khoaxuantu, krahets, night-cruise, nuomi1, Reanon and rongyi (in alphabetical order). Thanks to them for the time and effort they put in, it is they who ensure the standardization and unity of code in various languages. -The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, while the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0, and magentaqin. It is thanks to their continuous contributions that this book can reach and serve a broader audience. +The Traditional Chinese version of this book was reviewed by Shyam-Chen and Dr-XYZ, and the English version was reviewed by yuelinxin, K3v123, QiLOL, Phoenix0415, SamJin98, yanedie, RafaelCaso, pengchzn, thomasq0 and magentaqin. It is because of their continuous contributions that this book can serve a wider readership, and we thank them. -Throughout the creation of this book, numerous individuals provided invaluable assistance, including but not limited to: +During the creation of this book, I received help from many people. -- Thanks to my mentor at the company, Dr. Xi Li, who encouraged me in a conversation to "get moving fast," which solidified my determination to write this book; -- Thanks to my girlfriend Bubble, as the first reader of this book, for offering many valuable suggestions from the perspective of a beginner in algorithms, making this book more suitable for newbies; +- Thanks to my mentor at the company, Dr. Li Xi, who encouraged me to "take action quickly" during a conversation, strengthening my determination to write this book; +- Thanks to my girlfriend Bubble as the first reader of this book, who provided many valuable suggestions from the perspective of an algorithm beginner, making this book more suitable for novices to read; - Thanks to Tengbao, Qibao, and Feibao for coming up with a creative name for this book, evoking everyone's fond memories of writing their first line of code "Hello World!"; -- Thanks to Xiaoquan for providing professional help in intellectual property, which has played a significant role in the development of this open-source book; -- Thanks to Sutong for designing a beautiful cover and logo for this book, and for patiently making multiple revisions under my insistence; -- Thanks to @squidfunk for providing writing and typesetting suggestions, as well as his developed open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master). +- Thanks to Xiaoquan for providing professional help in intellectual property rights, which played an important role in the improvement of this open-source book; +- Thanks to Sutong for designing the beautiful cover and logo for this book, and for patiently making revisions multiple times driven by my obsessive-compulsive disorder; +- Thanks to @squidfunk for the typesetting suggestions, as well as for developing the open-source documentation theme [Material-for-MkDocs](https://github.com/squidfunk/mkdocs-material/tree/master). -Throughout the writing journey, I delved into numerous textbooks and articles on data structures and algorithms. These works served as exemplary models, ensuring the accuracy and quality of this book's content. I extend my gratitude to all who preceded me for their invaluable contributions! +During the writing process, I read many textbooks and articles on data structures and algorithms. These works provided excellent examples for this book and ensured the accuracy and quality of the book's content. I would like to thank all the teachers and predecessors for their outstanding contributions! -This book advocates a combination of hands-on and minds-on learning, inspired in this regard by ["Dive into Deep Learning"](https://github.com/d2l-ai/d2l-en). I highly recommend this excellent book to all readers. +This book advocates a learning method that combines hands and brain, and in this regard I was deeply inspired by [Dive into Deep Learning](https://github.com/d2l-ai/d2l-zh). I highly recommend this excellent work to all readers. -**Heartfelt thanks to my parents, whose ongoing support and encouragement have allowed me to do this interesting work**. +**Heartfelt thanks to my parents, it is your support and encouragement that has given me the opportunity to do this interesting thing**. diff --git a/en/docs/chapter_preface/index.md b/en/docs/chapter_preface/index.md index 62bf95bb7..6d837a3d5 100644 --- a/en/docs/chapter_preface/index.md +++ b/en/docs/chapter_preface/index.md @@ -9,12 +9,12 @@ icon: material/book-open-outline !!! abstract - Algorithms are like a beautiful symphony, with each line of code flowing like a rhythm. - - May this book ring softly in your mind, leaving a unique and profound melody. + Algorithms are like a beautiful symphony, each line of code flows like a melody. + + May this book gently resonate in your mind, leaving a unique and profound melody. ## Chapter contents -- [0.1   About this book](about_the_book.md) -- [0.2   How to read](suggestions.md) +- [0.1   About This Book](about_the_book.md) +- [0.2   How to Use This Book](suggestions.md) - [0.3   Summary](summary.md) diff --git a/en/docs/chapter_preface/suggestions.md b/en/docs/chapter_preface/suggestions.md index 9718d44ce..0e8062163 100644 --- a/en/docs/chapter_preface/suggestions.md +++ b/en/docs/chapter_preface/suggestions.md @@ -2,254 +2,258 @@ comments: true --- -# 0.2   How to read +# 0.2   How to Use This Book !!! tip For the best reading experience, it is recommended that you read through this section. -## 0.2.1   Writing conventions +## 0.2.1   Writing Style Conventions -- Chapters marked with '*' after the title are optional and contain relatively challenging content. If you are short on time, it is advisable to skip them. -- Technical terms will be in boldface (in the print and PDF versions) or underlined (in the web version), for instance, array. It's advisable to familiarize yourself with these for better comprehension of technical texts. -- **Bolded text** indicates key content or summary statements, which deserve special attention. -- Words and phrases with specific meanings are indicated with “quotation marks” to avoid ambiguity. -- When it comes to terms that are inconsistent between programming languages, this book follows Python, for example using `None` to mean `null`. -- This book partially ignores the comment conventions for programming languages in exchange for a more compact layout of the content. The comments primarily consist of three types: title comments, content comments, and multi-line comments. +- Titles marked with `*` are optional sections with relatively difficult content. If you have limited time, you can skip them first. +- Technical terms will be in bold (in paper and PDF versions) or underlined (in web versions), such as array. It is recommended to memorize them for reading literature. +- Key content and summary statements will be **bolded**, and such text deserves special attention. +- Words and phrases with specific meanings will be marked with "quotation marks" to avoid ambiguity. +- When it comes to nouns that are inconsistent between programming languages, this book uses Python as the standard, for example, using `None` to represent "null". +- This book partially abandons the comment conventions of programming languages in favor of more compact content layout. Comments are mainly divided into three types: title comments, content comments, and multi-line comments. === "Python" ```python title="" - """Header comments for labeling functions, classes, test samples, etc""" - - # Comments for explaining details - + """Title comment, used to label functions, classes, test cases, etc.""" + + # Content comment, used to explain code in detail + """ - Multiline - comments + Multi-line + comment """ ``` === "C++" ```cpp title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Java" ```java title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "C#" ```csharp title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Go" ```go title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Swift" ```swift title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "JS" ```javascript title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "TS" ```typescript title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Dart" ```dart title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Rust" ```rust title="" - /* Header comments for labeling functions, classes, test samples, etc */ + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail - // Comments for explaining details. - /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "C" ```c title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` === "Kotlin" ```kotlin title="" - /* Header comments for labeling functions, classes, test samples, etc */ - - // Comments for explaining details. - + /* Title comment, used to label functions, classes, test cases, etc. */ + + // Content comment, used to explain code in detail + /** - * Multiline - * comments + * Multi-line + * comment */ ``` -=== "Zig" +=== "Ruby" - ```zig title="" - // Header comments for labeling functions, classes, test samples, etc - - // Comments for explaining details. - - // Multiline - // comments + ```ruby title="" + ### Title comment, used to label functions, classes, test cases, etc. ### + + # Content comment, used to explain code in detail + + # Multi-line + # comment ``` -## 0.2.2   Efficient learning via animated illustrations +## 0.2.2   Learning Efficiently with Animated Illustrations -Compared with text, videos and pictures have a higher density of information and are more structured, making them easier to understand. In this book, **key and difficult concepts are mainly presented through animations and illustrations**, with text serving as explanations and supplements. +Compared to text, videos and images have higher information density and structural organization, making them easier to understand. In this book, **key and difficult knowledge will mainly be presented in the form of animated illustrations**, with text serving as explanation and supplement. -When encountering content with animations or illustrations as shown in Figure 0-2, **prioritize understanding the figure, with text as supplementary**, integrating both for a comprehensive understanding. +If you find that a section of content provides animated illustrations as shown in Figure 0-2 while reading this book, **please focus on the illustrations first, with text as a supplement**, and combine the two to understand the content. -![Animated illustration example](../index.assets/animation.gif){ class="animation-figure" } +![Example of animated illustrations](../index.assets/animation.gif){ class="animation-figure" } -

Figure 0-2   Animated illustration example

+

Figure 0-2   Example of animated illustrations

-## 0.2.3   Deepen understanding through coding practice +## 0.2.3   Deepening Understanding Through Code Practice -The source code of this book is hosted on the [GitHub Repository](https://github.com/krahets/hello-algo). As shown in Figure 0-3, **the source code comes with test examples and can be executed with just a single click**. +The accompanying code for this book is hosted in the [GitHub repository](https://github.com/krahets/hello-algo). As shown in Figure 0-3, **the source code comes with test cases and can be run with one click**. -If time permits, **it's recommended to type out the code yourself**. If pressed for time, at least read and run all the codes. +If time permits, **it is recommended that you type out the code yourself**. If you have limited study time, please at least read through and run all the code. -Compared to just reading code, writing code often yields more learning. **Learning by doing is the real way to learn.** +Compared to reading code, the process of writing code often brings more rewards. **Learning by doing is the real learning**. -![Running code example](../index.assets/running_code.gif){ class="animation-figure" } +![Example of running code](../index.assets/running_code.gif){ class="animation-figure" } -

Figure 0-3   Running code example

+

Figure 0-3   Example of running code

-Setting up to run the code involves three main steps. +The preliminary work for running code is mainly divided into three steps. -**Step 1: Install a local programming environment**. Follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) in the appendix for installation, or skip this step if already installed. +**Step 1: Install the local programming environment**. Please follow the [tutorial](https://www.hello-algo.com/chapter_appendix/installation/) shown in the appendix for installation. If already installed, you can skip this step. -**Step 2: Clone or download the code repository**. Visit the [GitHub Repository](https://github.com/krahets/hello-algo). - -If [Git](https://git-scm.com/downloads) is installed, use the following command to clone the repository: +**Step 2: Clone or download the code repository**. Visit the [GitHub repository](https://github.com/krahets/hello-algo). If you have already installed [Git](https://git-scm.com/downloads), you can clone this repository with the following command: ```shell git clone https://github.com/krahets/hello-algo.git ``` -Alternatively, you can also click the "Download ZIP" button at the location shown in Figure 0-4 to directly download the code as a compressed ZIP file. Then, you can simply extract it locally. +Of course, you can also click the "Download ZIP" button at the location shown in Figure 0-4 to directly download the code compressed package, and then extract it locally. -![Cloning repository and downloading code](suggestions.assets/download_code.png){ class="animation-figure" } +![Clone repository and download code](suggestions.assets/download_code.png){ class="animation-figure" } -

Figure 0-4   Cloning repository and downloading code

+

Figure 0-4   Clone repository and download code

-**Step 3: Run the source code**. As shown in Figure 0-5, for the code block labeled with the file name at the top, we can find the corresponding source code file in the `codes` folder of the repository. These files can be executed with a single click, which will help you save unnecessary debugging time and allow you to focus on learning. +**Step 3: Run the source code**. As shown in Figure 0-5, for code blocks with file names at the top, we can find the corresponding source code files in the `codes` folder of the repository. The source code files can be run with one click, which will help you save unnecessary debugging time and allow you to focus on learning content. -![Code block and corresponding source code file](suggestions.assets/code_md_to_repo.png){ class="animation-figure" } +![Code blocks and corresponding source code files](suggestions.assets/code_md_to_repo.png){ class="animation-figure" } -

Figure 0-5   Code block and corresponding source code file

+

Figure 0-5   Code blocks and corresponding source code files

-## 0.2.4   Learning together in discussion +In addition to running code locally, **the web version also supports visual running of Python code** (implemented based on [pythontutor](https://pythontutor.com/)). As shown in Figure 0-6, you can click "Visual Run" below the code block to expand the view and observe the execution process of the algorithm code; you can also click "Full Screen View" for a better viewing experience. -While reading this book, please don't skip over the points that you didn't learn. **Feel free to post your questions in the comment section**. We will be happy to answer them and can usually respond within two days. +![Visual running of Python code](suggestions.assets/pythontutor_example.png){ class="animation-figure" } -As illustrated in Figure 0-6, each chapter features a comment section at the bottom. I encourage you to pay attention to these comments. They not only expose you to others' encountered problems, aiding in identifying knowledge gaps and sparking deeper contemplation, but also invite you to generously contribute by answering fellow readers' inquiries, sharing insights, and fostering mutual improvement. +

Figure 0-6   Visual running of Python code

-![Comment section example](../index.assets/comment.gif){ class="animation-figure" } +## 0.2.4   Growing Together Through Questions and Discussions -

Figure 0-6   Comment section example

+When reading this book, please do not easily skip knowledge points that you have not learned well. **Feel free to ask your questions in the comments section**, and my friends and I will do our best to answer you, and generally reply within two days. -## 0.2.5   Algorithm learning path +As shown in Figure 0-7, the web version has a comments section at the bottom of each chapter. I hope you will pay more attention to the content of the comments section. On the one hand, you can learn about the problems that everyone encounters, thus checking for omissions and stimulating deeper thinking. On the other hand, I hope you can generously answer other friends' questions, share your insights, and help others progress. -Overall, the journey of mastering data structures and algorithms can be divided into three stages: +![Example of comments section](../index.assets/comment.gif){ class="animation-figure" } -1. **Stage 1: Introduction to algorithms**. We need to familiarize ourselves with the characteristics and usage of various data structures and learn about the principles, processes, uses, and efficiency of different algorithms. -2. **Stage 2: Practicing algorithm problems**. It is recommended to start from popular problems, such as [Sword for Offer](https://leetcode.cn/studyplan/coding-interviews/) and [LeetCode Hot 100](https://leetcode.cn/studyplan/top-100- liked/), and accumulate at least 100 questions to familiarize yourself with mainstream algorithmic problems. Forgetfulness can be a challenge when you start practicing, but rest assured that this is normal. We can follow the "Ebbinghaus Forgetting Curve" to review the questions, and usually after 3~5 rounds of repetitions, we will be able to memorize them. -3. **Stage 3: Building the knowledge system**. In terms of learning, we can read algorithm column articles, solution frameworks, and algorithm textbooks to continuously enrich the knowledge system. In terms of practicing, we can try advanced strategies, such as categorizing by topic, multiple solutions for a single problem, and one solution for multiple problems, etc. Insights on these strategies can be found in various communities. +

Figure 0-7   Example of comments section

-As shown in Figure 0-7, this book mainly covers “Stage 1,” aiming to help you more efficiently embark on Stages 2 and 3. +## 0.2.5   Algorithm Learning Roadmap -![Algorithm learning path](suggestions.assets/learning_route.png){ class="animation-figure" } +From an overall perspective, we can divide the process of learning data structures and algorithms into three stages. -

Figure 0-7   Algorithm learning path

+1. **Stage 1: Algorithm introduction**. We need to familiarize ourselves with the characteristics and usage of various data structures, and learn the principles, processes, uses, and efficiency of different algorithms. +2. **Stage 2: Practice algorithm problems**. It is recommended to start with popular problems, and accumulate at least 100 problems first, to familiarize yourself with mainstream algorithm problems. When first practicing problems, "knowledge forgetting" may be a challenge, but rest assured, this is very normal. We can review problems according to the "Ebbinghaus forgetting curve", and usually after 3-5 rounds of repetition, we can firmly remember them. For recommended problem lists and practice plans, please see this [GitHub repository](https://github.com/krahets/LeetCode-Book). +3. **Stage 3: Building a knowledge system**. In terms of learning, we can read algorithm column articles, problem-solving frameworks, and algorithm textbooks to continuously enrich our knowledge system. In terms of practicing problems, we can try advanced problem-solving strategies, such as categorization by topic, one problem multiple solutions, one solution multiple problems, etc. Related problem-solving insights can be found in various communities. + +As shown in Figure 0-8, the content of this book mainly covers "Stage 1", aiming to help you more efficiently carry out Stage 2 and Stage 3 learning. + +![Algorithm learning roadmap](suggestions.assets/learning_route.png){ class="animation-figure" } + +

Figure 0-8   Algorithm learning roadmap

diff --git a/en/docs/chapter_preface/summary.md b/en/docs/chapter_preface/summary.md index dbde4fbc4..47dd82f78 100644 --- a/en/docs/chapter_preface/summary.md +++ b/en/docs/chapter_preface/summary.md @@ -4,9 +4,11 @@ comments: true # 0.3   Summary -- The main audience of this book is beginners in algorithm. If you already have some basic knowledge, this book can help you systematically review your algorithm knowledge, and the source code in this book can also be used as a "Coding Toolkit". -- The book consists of three main sections, Complexity Analysis, Data Structures, and Algorithms, covering most of the topics in the field. -- For newcomers to algorithms, it is crucial to read an introductory book in the beginning stages to avoid many detours or common pitfalls. -- Animations and figures within the book are usually used to introduce key points and difficult knowledge. These should be given more attention when reading the book. -- Practice is the best way to learn programming. It is highly recommended that you run the source code and type in the code yourself. -- Each chapter in the web version of this book features a discussion section, and you are welcome to share your questions and insights at any time. +### 1.   Key Review + +- The main audience of this book is algorithm beginners. If you already have a certain foundation, this book can help you systematically review algorithm knowledge, and the source code in the book can also be used as a "problem-solving toolkit." +- The content of the book mainly includes three parts: complexity analysis, data structures, and algorithms, covering most topics in this field. +- For algorithm novices, reading an introductory book during the initial learning stage is crucial, as it can help you avoid many detours. +- The animated illustrations in the book are usually used to introduce key and difficult knowledge. When reading this book, you should pay more attention to these contents. +- Practice is the best way to learn programming. It is strongly recommended to run the source code and type the code yourself. +- The web version of this book has a comments section for each chapter, where you are welcome to share your questions and insights at any time. diff --git a/en/docs/chapter_reference/index.md b/en/docs/chapter_reference/index.md index 39cf6a52c..2c015705f 100644 --- a/en/docs/chapter_reference/index.md +++ b/en/docs/chapter_reference/index.md @@ -16,7 +16,7 @@ icon: material/bookshelf [6] Mark Allen Weiss, translated by Chen Yue. Data Structures and Algorithm Analysis in Java (Third Edition). -[7] Cheng Jie. Speaking of Data Structures. +[7] Cheng Jie. Conversational Data Structures. [8] Wang Zheng. The Beauty of Data Structures and Algorithms. diff --git a/en/docs/chapter_searching/binary_search.md b/en/docs/chapter_searching/binary_search.md index d9e815ac1..9990d1067 100644 --- a/en/docs/chapter_searching/binary_search.md +++ b/en/docs/chapter_searching/binary_search.md @@ -2,29 +2,29 @@ comments: true --- -# 10.1   Binary search +# 10.1   Binary Search -Binary search is an efficient search algorithm that uses a divide-and-conquer strategy. It takes advantage of the sorted order of elements in an array by reducing the search interval by half in each iteration, continuing until either the target element is found or the search interval becomes empty. +Binary search is an efficient searching algorithm based on the divide-and-conquer strategy. It leverages the orderliness of data to reduce the search range by half in each round until the target element is found or the search interval becomes empty. !!! question - Given an array `nums` of length $n$, where elements are arranged in ascending order without duplicates. Please find and return the index of element `target` in this array. If the array does not contain the element, return $-1$. An example is shown in Figure 10-1. + Given an array `nums` of length $n$ with elements arranged in ascending order and no duplicates, search for and return the index of element `target` in the array. If the array does not contain the element, return $-1$. An example is shown in Figure 10-1. ![Binary search example data](binary_search.assets/binary_search_example.png){ class="animation-figure" }

Figure 10-1   Binary search example data

-As shown in Figure 10-2, we firstly initialize pointers with $i = 0$ and $j = n - 1$, pointing to the first and last element of the array respectively. They also represent the whole search interval $[0, n - 1]$. Please note that square brackets indicate a closed interval, which includes the boundary values themselves. +As shown in Figure 10-2, we first initialize pointers $i = 0$ and $j = n - 1$, pointing to the first and last elements of the array respectively, representing the search interval $[0, n - 1]$. Note that square brackets denote a closed interval, which includes the boundary values themselves. -And then the following two steps may be performed in a loop. +Next, perform the following two steps in a loop: 1. Calculate the midpoint index $m = \lfloor {(i + j) / 2} \rfloor$, where $\lfloor \: \rfloor$ denotes the floor operation. -2. Based on the comparison between the value of `nums[m]` and `target`, one of the following three cases will be chosen to execute. - 1. If `nums[m] < target`, it indicates that `target` is in the interval $[m + 1, j]$, thus set $i = m + 1$. - 2. If `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, thus set $j = m - 1$. - 3. If `nums[m] = target`, it indicates that `target` is found, thus return index $m$. +2. Compare `nums[m]` and `target`, which results in three cases: + 1. When `nums[m] < target`, it indicates that `target` is in the interval $[m + 1, j]$, so execute $i = m + 1$. + 2. When `nums[m] > target`, it indicates that `target` is in the interval $[i, m - 1]$, so execute $j = m - 1$. + 3. When `nums[m] = target`, it indicates that `target` has been found, so return index $m$. -If the array does not contain the target element, the search interval will eventually reduce to empty, ending up returning $-1$. +If the array does not contain the target element, the search interval will eventually shrink to empty. In this case, return $-1$. === "<1>" ![Binary search process](binary_search.assets/binary_search_step1.png){ class="animation-figure" } @@ -49,48 +49,48 @@ If the array does not contain the target element, the search interval will event

Figure 10-2   Binary search process

-It's worth noting that as $i$ and $j$ are both of type `int`, **$i + j$ might exceed the range of `int` type**. To avoid large number overflow, we usually use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. +It's worth noting that since both $i$ and $j$ are of `int` type, **$i + j$ may exceed the range of the `int` type**. To avoid large number overflow, we typically use the formula $m = \lfloor {i + (j - i) / 2} \rfloor$ to calculate the midpoint. -The code is as follows: +The code is shown below: === "Python" ```python title="binary_search.py" def binary_search(nums: list[int], target: int) -> int: - """Binary search (double closed interval)""" - # Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively + """Binary search (closed interval)""" + # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array i, j = 0, len(nums) - 1 - # Loop until the search interval is empty (when i > j, it is empty) + # Loop, exit when the search interval is empty (empty when i > j) while i <= j: - # Theoretically, Python's numbers can be infinitely large (depending on memory size), so there is no need to consider large number overflow - m = i + (j - i) // 2 # Calculate midpoint index m + # In theory, Python numbers can be infinitely large (depending on memory size), no need to consider large number overflow + m = (i + j) // 2 # Calculate midpoint index m if nums[m] < target: - i = m + 1 # This situation indicates that target is in the interval [m+1, j] + i = m + 1 # This means target is in the interval [m+1, j] elif nums[m] > target: - j = m - 1 # This situation indicates that target is in the interval [i, m-1] + j = m - 1 # This means target is in the interval [i, m-1] else: - return m # Found the target element, thus return its index - return -1 # Did not find the target element, thus return -1 + return m # Found the target element, return its index + return -1 # Target element not found, return -1 ``` === "C++" ```cpp title="binary_search.cpp" - /* Binary search (double closed interval) */ + /* Binary search (closed interval on both sides) */ int binarySearch(vector &nums, int target) { - // Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array int i = 0, j = nums.size() - 1; - // Loop until the search interval is empty (when i > j, it is empty) + // Loop, exit when the search interval is empty (empty when i > j) while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m - if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j] + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j] i = m + 1; - else if (nums[m] > target) // This situation indicates that target is in the interval [i, m-1] + else if (nums[m] > target) // This means target is in the interval [i, m-1] j = m - 1; - else // Found the target element, thus return its index + else // Found the target element, return its index return m; } - // Did not find the target element, thus return -1 + // Target element not found, return -1 return -1; } ``` @@ -98,21 +98,21 @@ The code is as follows: === "Java" ```java title="binary_search.java" - /* Binary search (double closed interval) */ + /* Binary search (closed interval on both sides) */ int binarySearch(int[] nums, int target) { - // Initialize double closed interval [0, n-1], i.e., i, j point to the first element and last element of the array respectively + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array int i = 0, j = nums.length - 1; - // Loop until the search interval is empty (when i > j, it is empty) + // Loop, exit when the search interval is empty (empty when i > j) while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m - if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j] + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j] i = m + 1; - else if (nums[m] > target) // This situation indicates that target is in the interval [i, m-1] + else if (nums[m] > target) // This means target is in the interval [i, m-1] j = m - 1; - else // Found the target element, thus return its index + else // Found the target element, return its index return m; } - // Did not find the target element, thus return -1 + // Target element not found, return -1 return -1; } ``` @@ -120,76 +120,255 @@ The code is as follows: === "C#" ```csharp title="binary_search.cs" - [class]{binary_search}-[func]{BinarySearch} + /* Binary search (closed interval on both sides) */ + int BinarySearch(int[] nums, int target) { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + int i = 0, j = nums.Length - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j] + i = m + 1; + else if (nums[m] > target) // This means target is in the interval [i, m-1] + j = m - 1; + else // Found the target element, return its index + return m; + } + // Target element not found, return -1 + return -1; + } ``` === "Go" ```go title="binary_search.go" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + func binarySearch(nums []int, target int) int { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + i, j := 0, len(nums)-1 + // Loop, exit when the search interval is empty (empty when i > j) + for i <= j { + m := i + (j-i)/2 // Calculate the midpoint index m + if nums[m] < target { // This means target is in the interval [m+1, j] + i = m + 1 + } else if nums[m] > target { // This means target is in the interval [i, m-1] + j = m - 1 + } else { // Found the target element, return its index + return m + } + } + // Target element not found, return -1 + return -1 + } ``` === "Swift" ```swift title="binary_search.swift" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + func binarySearch(nums: [Int], target: Int) -> Int { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + var i = nums.startIndex + var j = nums.endIndex - 1 + // Loop, exit when the search interval is empty (empty when i > j) + while i <= j { + let m = i + (j - i) / 2 // Calculate the midpoint index m + if nums[m] < target { // This means target is in the interval [m+1, j] + i = m + 1 + } else if nums[m] > target { // This means target is in the interval [i, m-1] + j = m - 1 + } else { // Found the target element, return its index + return m + } + } + // Target element not found, return -1 + return -1 + } ``` === "JS" ```javascript title="binary_search.js" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + function binarySearch(nums, target) { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + let i = 0, + j = nums.length - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + // Calculate midpoint index m, use parseInt() to round down + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // This means target is in the interval [m+1, j] + i = m + 1; + else if (nums[m] > target) + // This means target is in the interval [i, m-1] + j = m - 1; + else return m; // Found the target element, return its index + } + // Target element not found, return -1 + return -1; + } ``` === "TS" ```typescript title="binary_search.ts" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + function binarySearch(nums: number[], target: number): number { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + let i = 0, + j = nums.length - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + // Calculate the midpoint index m + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // This means target is in the interval [m+1, j] + i = m + 1; + } else if (nums[m] > target) { + // This means target is in the interval [i, m-1] + j = m - 1; + } else { + // Found the target element, return its index + return m; + } + } + return -1; // Target element not found, return -1 + } ``` === "Dart" ```dart title="binary_search.dart" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + int binarySearch(List nums, int target) { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + int i = 0, j = nums.length - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + int m = i + (j - i) ~/ 2; // Calculate the midpoint index m + if (nums[m] < target) { + // This means target is in the interval [m+1, j] + i = m + 1; + } else if (nums[m] > target) { + // This means target is in the interval [i, m-1] + j = m - 1; + } else { + // Found the target element, return its index + return m; + } + } + // Target element not found, return -1 + return -1; + } ``` === "Rust" ```rust title="binary_search.rs" - [class]{}-[func]{binary_search} + /* Binary search (closed interval on both sides) */ + fn binary_search(nums: &[i32], target: i32) -> i32 { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + let mut i = 0; + let mut j = nums.len() as i32 - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while i <= j { + let m = i + (j - i) / 2; // Calculate the midpoint index m + if nums[m as usize] < target { + // This means target is in the interval [m+1, j] + i = m + 1; + } else if nums[m as usize] > target { + // This means target is in the interval [i, m-1] + j = m - 1; + } else { + // Found the target element, return its index + return m; + } + } + // Target element not found, return -1 + return -1; + } ``` === "C" ```c title="binary_search.c" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + int binarySearch(int *nums, int len, int target) { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + int i = 0, j = len - 1; + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j] + i = m + 1; + else if (nums[m] > target) // This means target is in the interval [i, m-1] + j = m - 1; + else // Found the target element, return its index + return m; + } + // Target element not found, return -1 + return -1; + } ``` === "Kotlin" ```kotlin title="binary_search.kt" - [class]{}-[func]{binarySearch} + /* Binary search (closed interval on both sides) */ + fun binarySearch(nums: IntArray, target: Int): Int { + // Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + var i = 0 + var j = nums.size - 1 + // Loop, exit when the search interval is empty (empty when i > j) + while (i <= j) { + val m = i + (j - i) / 2 // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j] + i = m + 1 + else if (nums[m] > target) // This means target is in the interval [i, m-1] + j = m - 1 + else // Found the target element, return its index + return m + } + // Target element not found, return -1 + return -1 + } ``` === "Ruby" ```ruby title="binary_search.rb" - [class]{}-[func]{binary_search} + ### Binary search (closed interval) ### + def binary_search(nums, target) + # Initialize closed interval [0, n-1], i.e., i, j point to the first and last elements of the array + i, j = 0, nums.length - 1 + + # Loop, exit when the search interval is empty (empty when i > j) + while i <= j + # In theory, Ruby numbers can be infinitely large (limited by memory), no need to consider overflow + m = (i + j) / 2 # Calculate the midpoint index m + + if nums[m] < target + i = m + 1 # This means target is in the interval [m+1, j] + elsif nums[m] > target + j = m - 1 # This means target is in the interval [i, m-1] + else + return m # Found the target element, return its index + end + end + + -1 # Target element not found, return -1 + end ``` -=== "Zig" +**Time complexity is $O(\log n)$**: In the binary loop, the interval is reduced by half each round, so the number of loops is $\log_2 n$. - ```zig title="binary_search.zig" - [class]{}-[func]{binarySearch} - ``` +**Space complexity is $O(1)$**: Pointers $i$ and $j$ use constant-size space. -**Time complexity is $O(\log n)$** : In the binary loop, the interval decreases by half each round, hence the number of iterations is $\log_2 n$. +## 10.1.1   Interval Representation Methods -**Space complexity is $O(1)$** : Pointers $i$ and $j$ occupies constant size of space. - -## 10.1.1   Interval representation methods - -Besides the above closed interval, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, where the left boundary includes itself, and the right boundary does not. In this representation, the interval $[i, j)$ is empty when $i = j$. +In addition to the closed interval mentioned above, another common interval representation is the "left-closed right-open" interval, defined as $[0, n)$, meaning the left boundary includes itself while the right boundary does not. Under this representation, the interval $[i, j)$ is empty when $i = j$. We can implement a binary search algorithm with the same functionality based on this representation: @@ -197,39 +376,39 @@ We can implement a binary search algorithm with the same functionality based on ```python title="binary_search.py" def binary_search_lcro(nums: list[int], target: int) -> int: - """Binary search (left closed right open interval)""" - # Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively + """Binary search (left-closed right-open interval)""" + # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 i, j = 0, len(nums) - # Loop until the search interval is empty (when i = j, it is empty) + # Loop, exit when the search interval is empty (empty when i = j) while i < j: - m = i + (j - i) // 2 # Calculate midpoint index m + m = (i + j) // 2 # Calculate midpoint index m if nums[m] < target: - i = m + 1 # This situation indicates that target is in the interval [m+1, j) + i = m + 1 # This means target is in the interval [m+1, j) elif nums[m] > target: - j = m # This situation indicates that target is in the interval [i, m) + j = m # This means target is in the interval [i, m) else: - return m # Found the target element, thus return its index - return -1 # Did not find the target element, thus return -1 + return m # Found the target element, return its index + return -1 # Target element not found, return -1 ``` === "C++" ```cpp title="binary_search.cpp" - /* Binary search (left closed right open interval) */ + /* Binary search (left-closed right-open interval) */ int binarySearchLCRO(vector &nums, int target) { - // Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 int i = 0, j = nums.size(); - // Loop until the search interval is empty (when i = j, it is empty) + // Loop, exit when the search interval is empty (empty when i = j) while (i < j) { - int m = i + (j - i) / 2; // Calculate midpoint index m - if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j) + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j) i = m + 1; - else if (nums[m] > target) // This situation indicates that target is in the interval [i, m) + else if (nums[m] > target) // This means target is in the interval [i, m) j = m; - else // Found the target element, thus return its index + else // Found the target element, return its index return m; } - // Did not find the target element, thus return -1 + // Target element not found, return -1 return -1; } ``` @@ -237,21 +416,21 @@ We can implement a binary search algorithm with the same functionality based on === "Java" ```java title="binary_search.java" - /* Binary search (left closed right open interval) */ + /* Binary search (left-closed right-open interval) */ int binarySearchLCRO(int[] nums, int target) { - // Initialize left closed right open interval [0, n), i.e., i, j point to the first element and the last element +1 of the array respectively + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 int i = 0, j = nums.length; - // Loop until the search interval is empty (when i = j, it is empty) + // Loop, exit when the search interval is empty (empty when i = j) while (i < j) { - int m = i + (j - i) / 2; // Calculate midpoint index m - if (nums[m] < target) // This situation indicates that target is in the interval [m+1, j) + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j) i = m + 1; - else if (nums[m] > target) // This situation indicates that target is in the interval [i, m) + else if (nums[m] > target) // This means target is in the interval [i, m) j = m; - else // Found the target element, thus return its index + else // Found the target element, return its index return m; } - // Did not find the target element, thus return -1 + // Target element not found, return -1 return -1; } ``` @@ -259,86 +438,266 @@ We can implement a binary search algorithm with the same functionality based on === "C#" ```csharp title="binary_search.cs" - [class]{binary_search}-[func]{BinarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + int BinarySearchLCRO(int[] nums, int target) { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + int i = 0, j = nums.Length; + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j) + i = m + 1; + else if (nums[m] > target) // This means target is in the interval [i, m) + j = m; + else // Found the target element, return its index + return m; + } + // Target element not found, return -1 + return -1; + } ``` === "Go" ```go title="binary_search.go" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + func binarySearchLCRO(nums []int, target int) int { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + i, j := 0, len(nums) + // Loop, exit when the search interval is empty (empty when i = j) + for i < j { + m := i + (j-i)/2 // Calculate the midpoint index m + if nums[m] < target { // This means target is in the interval [m+1, j) + i = m + 1 + } else if nums[m] > target { // This means target is in the interval [i, m) + j = m + } else { // Found the target element, return its index + return m + } + } + // Target element not found, return -1 + return -1 + } ``` === "Swift" ```swift title="binary_search.swift" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + func binarySearchLCRO(nums: [Int], target: Int) -> Int { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + var i = nums.startIndex + var j = nums.endIndex + // Loop, exit when the search interval is empty (empty when i = j) + while i < j { + let m = i + (j - i) / 2 // Calculate the midpoint index m + if nums[m] < target { // This means target is in the interval [m+1, j) + i = m + 1 + } else if nums[m] > target { // This means target is in the interval [i, m) + j = m + } else { // Found the target element, return its index + return m + } + } + // Target element not found, return -1 + return -1 + } ``` === "JS" ```javascript title="binary_search.js" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + function binarySearchLCRO(nums, target) { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + let i = 0, + j = nums.length; + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + // Calculate midpoint index m, use parseInt() to round down + const m = parseInt(i + (j - i) / 2); + if (nums[m] < target) + // This means target is in the interval [m+1, j) + i = m + 1; + else if (nums[m] > target) + // This means target is in the interval [i, m) + j = m; + // Found the target element, return its index + else return m; + } + // Target element not found, return -1 + return -1; + } ``` === "TS" ```typescript title="binary_search.ts" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + function binarySearchLCRO(nums: number[], target: number): number { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + let i = 0, + j = nums.length; + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + // Calculate the midpoint index m + const m = Math.floor(i + (j - i) / 2); + if (nums[m] < target) { + // This means target is in the interval [m+1, j) + i = m + 1; + } else if (nums[m] > target) { + // This means target is in the interval [i, m) + j = m; + } else { + // Found the target element, return its index + return m; + } + } + return -1; // Target element not found, return -1 + } ``` === "Dart" ```dart title="binary_search.dart" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + int binarySearchLCRO(List nums, int target) { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + int i = 0, j = nums.length; + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + int m = i + (j - i) ~/ 2; // Calculate the midpoint index m + if (nums[m] < target) { + // This means target is in the interval [m+1, j) + i = m + 1; + } else if (nums[m] > target) { + // This means target is in the interval [i, m) + j = m; + } else { + // Found the target element, return its index + return m; + } + } + // Target element not found, return -1 + return -1; + } ``` === "Rust" ```rust title="binary_search.rs" - [class]{}-[func]{binary_search_lcro} + /* Binary search (left-closed right-open interval) */ + fn binary_search_lcro(nums: &[i32], target: i32) -> i32 { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + let mut i = 0; + let mut j = nums.len() as i32; + // Loop, exit when the search interval is empty (empty when i = j) + while i < j { + let m = i + (j - i) / 2; // Calculate the midpoint index m + if nums[m as usize] < target { + // This means target is in the interval [m+1, j) + i = m + 1; + } else if nums[m as usize] > target { + // This means target is in the interval [i, m) + j = m; + } else { + // Found the target element, return its index + return m; + } + } + // Target element not found, return -1 + return -1; + } ``` === "C" ```c title="binary_search.c" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + int binarySearchLCRO(int *nums, int len, int target) { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + int i = 0, j = len; + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j) + i = m + 1; + else if (nums[m] > target) // This means target is in the interval [i, m) + j = m; + else // Found the target element, return its index + return m; + } + // Target element not found, return -1 + return -1; + } ``` === "Kotlin" ```kotlin title="binary_search.kt" - [class]{}-[func]{binarySearchLCRO} + /* Binary search (left-closed right-open interval) */ + fun binarySearchLCRO(nums: IntArray, target: Int): Int { + // Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + var i = 0 + var j = nums.size + // Loop, exit when the search interval is empty (empty when i = j) + while (i < j) { + val m = i + (j - i) / 2 // Calculate the midpoint index m + if (nums[m] < target) // This means target is in the interval [m+1, j) + i = m + 1 + else if (nums[m] > target) // This means target is in the interval [i, m) + j = m + else // Found the target element, return its index + return m + } + // Target element not found, return -1 + return -1 + } ``` === "Ruby" ```ruby title="binary_search.rb" - [class]{}-[func]{binary_search_lcro} + ### Binary search (left-closed right-open interval) ### + def binary_search_lcro(nums, target) + # Initialize left-closed right-open interval [0, n), i.e., i, j point to the first element and last element+1 + i, j = 0, nums.length + + # Loop, exit when the search interval is empty (empty when i = j) + while i < j + # Calculate the midpoint index m + m = (i + j) / 2 + + if nums[m] < target + i = m + 1 # This means target is in the interval [m+1, j) + elsif nums[m] > target + j = m - 1 # This means target is in the interval [i, m) + else + return m # Found the target element, return its index + end + end + + -1 # Target element not found, return -1 + end ``` -=== "Zig" +As shown in Figure 10-3, under the two interval representations, the initialization, loop condition, and interval narrowing operations of the binary search algorithm are all different. - ```zig title="binary_search.zig" - [class]{}-[func]{binarySearchLCRO} - ``` +Since both the left and right boundaries in the "closed interval" representation are defined as closed, the operations to narrow the interval through pointers $i$ and $j$ are also symmetric. This makes it less error-prone, **so the "closed interval" approach is generally recommended**. -As shown in Figure 10-3, under the two types of interval representations, the initialization, loop condition, and narrowing interval operation of the binary search algorithm differ. +![Two interval definitions](binary_search.assets/binary_search_ranges.png){ class="animation-figure" } -Since both boundaries in the "closed interval" representation are inclusive, the operations to narrow the interval through pointers $i$ and $j$ are also symmetrical. This makes it less prone to errors, **therefore, it is generally recommended to use the "closed interval" approach**. +

Figure 10-3   Two interval definitions

-![Two types of interval definitions](binary_search.assets/binary_search_ranges.png){ class="animation-figure" } - -

Figure 10-3   Two types of interval definitions

- -## 10.1.2   Advantages and limitations +## 10.1.2   Advantages and Limitations Binary search performs well in both time and space aspects. -- Binary search is time-efficient. With large dataset, the logarithmic time complexity offers a major advantage. For instance, given a dataset with size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ iterations, while binary search only demands $\log_2 2^{20} = 20$ loops. -- Binary search does not need extra space. Compared to search algorithms that rely on additional space (like hash search), binary search is more space-efficient. +- Binary search has high time efficiency. With large data volumes, the logarithmic time complexity has significant advantages. For example, when the data size $n = 2^{20}$, linear search requires $2^{20} = 1048576$ loop rounds, while binary search only needs $\log_2 2^{20} = 20$ rounds. +- Binary search requires no extra space. Compared to searching algorithms that require additional space (such as hash-based search), binary search is more space-efficient. -However, binary search may not be suitable for all scenarios due to the following concerns. +However, binary search is not suitable for all situations, mainly for the following reasons: -- Binary search can only be applied to sorted data. Unsorted data must be sorted before applying binary search, which may not be worthwhile as sorting algorithm typically has a time complexity of $O(n \log n)$. Such cost is even higher than linear search, not to mention binary search itself. For scenarios with frequent insertion, the cost of remaining the array in order is pretty high as the time complexity of inserting new elements into specific positions is $O(n)$. -- Binary search may use array only. Binary search requires non-continuous (jumping) element access, which is inefficient in linked list. As a result, linked list or data structures based on linked list may not be suitable for this algorithm. -- Linear search performs better on small dataset. In linear search, only 1 decision operation is required for each iteration; whereas in binary search, it involves 1 addition, 1 division, 1 to 3 decision operations, 1 addition (subtraction), totaling 4 to 6 operations. Therefore, if data size $n$ is small, linear search is faster than binary search. +- Binary search is only applicable to sorted data. If the input data is unsorted, sorting specifically to use binary search would be counterproductive, as sorting algorithms typically have a time complexity of $O(n \log n)$, which is higher than both linear search and binary search. For scenarios with frequent element insertions, maintaining array orderliness requires inserting elements at specific positions with a time complexity of $O(n)$, which is also very expensive. +- Binary search is only applicable to arrays. Binary search requires jump-style (non-contiguous) element access, and jump-style access has low efficiency in linked lists, making it unsuitable for linked lists or data structures based on linked list implementations. +- For small data volumes, linear search performs better. In linear search, each round requires only 1 comparison operation; while in binary search, it requires 1 addition, 1 division, 1-3 comparison operations, and 1 addition (subtraction), totaling 4-6 unit operations. Therefore, when the data volume $n$ is small, linear search is actually faster than binary search. diff --git a/en/docs/chapter_searching/binary_search_edge.md b/en/docs/chapter_searching/binary_search_edge.md index c445afb7a..669ebc9c3 100644 --- a/en/docs/chapter_searching/binary_search_edge.md +++ b/en/docs/chapter_searching/binary_search_edge.md @@ -2,22 +2,22 @@ comments: true --- -# 10.3   Binary search boundaries +# 10.3   Binary Search Edge Cases -## 10.3.1   Find the left boundary +## 10.3.1   Finding the Left Boundary !!! question - Given a sorted array `nums` of length $n$, which may contain duplicate elements, return the index of the leftmost element `target`. If the element is not present in the array, return $-1$. + Given a sorted array `nums` of length $n$ that may contain duplicate elements, return the index of the leftmost element `target` in the array. If the array does not contain the element, return $-1$. -Recalling the method of binary search for an insertion point, after the search is completed, the index $i$ will point to the leftmost occurrence of `target`. Therefore, **searching for the insertion point is essentially the same as finding the index of the leftmost `target`**. +Recall the method for finding the insertion point with binary search. After the search completes, $i$ points to the leftmost `target`, **so finding the insertion point is essentially finding the index of the leftmost `target`**. -We can use the function for finding an insertion point to find the left boundary of `target`. Note that the array might not contain `target`, which could lead to the following two results: +Consider implementing the left boundary search using the insertion point finding function. Note that the array may not contain `target`, which could result in the following two cases: -- The index $i$ of the insertion point is out of bounds. +- The insertion point index $i$ is out of bounds. - The element `nums[i]` is not equal to `target`. -In these cases, simply return $-1$. The code is as follows: +When either of these situations occurs, simply return $-1$. The code is shown below: === "Python" @@ -26,7 +26,7 @@ In these cases, simply return $-1$. The code is as follows: """Binary search for the leftmost target""" # Equivalent to finding the insertion point of target i = binary_search_insertion(nums, target) - # Did not find target, thus return -1 + # Target not found, return -1 if i == len(nums) or nums[i] != target: return -1 # Found target, return index i @@ -40,7 +40,7 @@ In these cases, simply return $-1$. The code is as follows: int binarySearchLeftEdge(vector &nums, int target) { // Equivalent to finding the insertion point of target int i = binarySearchInsertion(nums, target); - // Did not find target, thus return -1 + // Target not found, return -1 if (i == nums.size() || nums[i] != target) { return -1; } @@ -56,7 +56,7 @@ In these cases, simply return $-1$. The code is as follows: int binarySearchLeftEdge(int[] nums, int target) { // Equivalent to finding the insertion point of target int i = binary_search_insertion.binarySearchInsertion(nums, target); - // Did not find target, thus return -1 + // Target not found, return -1 if (i == nums.length || nums[i] != target) { return -1; } @@ -68,86 +68,179 @@ In these cases, simply return $-1$. The code is as follows: === "C#" ```csharp title="binary_search_edge.cs" - [class]{binary_search_edge}-[func]{BinarySearchLeftEdge} + /* Binary search for the leftmost target */ + int BinarySearchLeftEdge(int[] nums, int target) { + // Equivalent to finding the insertion point of target + int i = binary_search_insertion.BinarySearchInsertion(nums, target); + // Target not found, return -1 + if (i == nums.Length || nums[i] != target) { + return -1; + } + // Found target, return index i + return i; + } ``` === "Go" ```go title="binary_search_edge.go" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + func binarySearchLeftEdge(nums []int, target int) int { + // Equivalent to finding the insertion point of target + i := binarySearchInsertion(nums, target) + // Target not found, return -1 + if i == len(nums) || nums[i] != target { + return -1 + } + // Found target, return index i + return i + } ``` === "Swift" ```swift title="binary_search_edge.swift" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + func binarySearchLeftEdge(nums: [Int], target: Int) -> Int { + // Equivalent to finding the insertion point of target + let i = binarySearchInsertion(nums: nums, target: target) + // Target not found, return -1 + if i == nums.endIndex || nums[i] != target { + return -1 + } + // Found target, return index i + return i + } ``` === "JS" ```javascript title="binary_search_edge.js" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + function binarySearchLeftEdge(nums, target) { + // Equivalent to finding the insertion point of target + const i = binarySearchInsertion(nums, target); + // Target not found, return -1 + if (i === nums.length || nums[i] !== target) { + return -1; + } + // Found target, return index i + return i; + } ``` === "TS" ```typescript title="binary_search_edge.ts" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + function binarySearchLeftEdge(nums: Array, target: number): number { + // Equivalent to finding the insertion point of target + const i = binarySearchInsertion(nums, target); + // Target not found, return -1 + if (i === nums.length || nums[i] !== target) { + return -1; + } + // Found target, return index i + return i; + } ``` === "Dart" ```dart title="binary_search_edge.dart" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + int binarySearchLeftEdge(List nums, int target) { + // Equivalent to finding the insertion point of target + int i = binarySearchInsertion(nums, target); + // Target not found, return -1 + if (i == nums.length || nums[i] != target) { + return -1; + } + // Found target, return index i + return i; + } ``` === "Rust" ```rust title="binary_search_edge.rs" - [class]{}-[func]{binary_search_left_edge} + /* Binary search for the leftmost target */ + fn binary_search_left_edge(nums: &[i32], target: i32) -> i32 { + // Equivalent to finding the insertion point of target + let i = binary_search_insertion(nums, target); + // Target not found, return -1 + if i == nums.len() as i32 || nums[i as usize] != target { + return -1; + } + // Found target, return index i + i + } ``` === "C" ```c title="binary_search_edge.c" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + int binarySearchLeftEdge(int *nums, int numSize, int target) { + // Equivalent to finding the insertion point of target + int i = binarySearchInsertion(nums, numSize, target); + // Target not found, return -1 + if (i == numSize || nums[i] != target) { + return -1; + } + // Found target, return index i + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_edge.kt" - [class]{}-[func]{binarySearchLeftEdge} + /* Binary search for the leftmost target */ + fun binarySearchLeftEdge(nums: IntArray, target: Int): Int { + // Equivalent to finding the insertion point of target + val i = binarySearchInsertion(nums, target) + // Target not found, return -1 + if (i == nums.size || nums[i] != target) { + return -1 + } + // Found target, return index i + return i + } ``` === "Ruby" ```ruby title="binary_search_edge.rb" - [class]{}-[func]{binary_search_left_edge} + ### Binary search leftmost target ### + def binary_search_left_edge(nums, target) + # Equivalent to finding the insertion point of target + i = binary_search_insertion(nums, target) + + # Target not found, return -1 + return -1 if i == nums.length || nums[i] != target + + i # Found target, return index i + end ``` -=== "Zig" +## 10.3.2   Finding the Right Boundary - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchLeftEdge} - ``` +So how do we find the rightmost `target`? The most direct approach is to modify the code and replace the pointer shrinking operation in the `nums[m] == target` case. The code is omitted here; interested readers can implement it themselves. -## 10.3.2   Find the right boundary +Below we introduce two more clever methods. -How do we find the rightmost occurrence of `target`? The most straightforward way is to modify the traditional binary search logic by changing how we adjust the search boundaries in the case of `nums[m] == target`. The code is omitted here. If you are interested, try to implement the code on your own. +### 1.   Reusing Left Boundary Search -Below we are going to introduce two more ingenious methods. +In fact, we can use the function for finding the leftmost element to find the rightmost element. The specific method is: **Convert finding the rightmost `target` into finding the leftmost `target + 1`**. -### 1.   Reuse the left boundary search +As shown in Figure 10-7, after the search completes, pointer $i$ points to the leftmost `target + 1` (if it exists), while $j$ points to the rightmost `target`, **so we can simply return $j$**. -To find the rightmost occurrence of `target`, we can reuse the function used for locating the leftmost `target`. Specifically, we transform the search for the rightmost target into a search for the leftmost target + 1. +![Converting right boundary search to left boundary search](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } -As shown in Figure 10-7, after the search is complete, pointer $i$ will point to the leftmost `target + 1` (if exists), while pointer $j$ will point to the rightmost occurrence of `target`. Therefore, returning $j$ will give us the right boundary. +

Figure 10-7   Converting right boundary search to left boundary search

-![Transforming the search for the right boundary into the search for the left boundary](binary_search_edge.assets/binary_search_right_edge_by_left_edge.png){ class="animation-figure" } - -

Figure 10-7   Transforming the search for the right boundary into the search for the left boundary

- -Note that the insertion point returned is $i$, therefore, it should be subtracted by $1$ to obtain $j$: +Note that the returned insertion point is $i$, so we need to subtract $1$ from it to obtain $j$: === "Python" @@ -158,7 +251,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte i = binary_search_insertion(nums, target + 1) # j points to the rightmost target, i points to the first element greater than target j = i - 1 - # Did not find target, thus return -1 + # Target not found, return -1 if j == -1 or nums[j] != target: return -1 # Found target, return index j @@ -174,7 +267,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte int i = binarySearchInsertion(nums, target + 1); // j points to the rightmost target, i points to the first element greater than target int j = i - 1; - // Did not find target, thus return -1 + // Target not found, return -1 if (j == -1 || nums[j] != target) { return -1; } @@ -192,7 +285,7 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte int i = binary_search_insertion.binarySearchInsertion(nums, target + 1); // j points to the rightmost target, i points to the first element greater than target int j = i - 1; - // Did not find target, thus return -1 + // Target not found, return -1 if (j == -1 || nums[j] != target) { return -1; } @@ -204,83 +297,197 @@ Note that the insertion point returned is $i$, therefore, it should be subtracte === "C#" ```csharp title="binary_search_edge.cs" - [class]{binary_search_edge}-[func]{BinarySearchRightEdge} + /* Binary search for the rightmost target */ + int BinarySearchRightEdge(int[] nums, int target) { + // Convert to finding the leftmost target + 1 + int i = binary_search_insertion.BinarySearchInsertion(nums, target + 1); + // j points to the rightmost target, i points to the first element greater than target + int j = i - 1; + // Target not found, return -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // Found target, return index j + return j; + } ``` === "Go" ```go title="binary_search_edge.go" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + func binarySearchRightEdge(nums []int, target int) int { + // Convert to finding the leftmost target + 1 + i := binarySearchInsertion(nums, target+1) + // j points to the rightmost target, i points to the first element greater than target + j := i - 1 + // Target not found, return -1 + if j == -1 || nums[j] != target { + return -1 + } + // Found target, return index j + return j + } ``` === "Swift" ```swift title="binary_search_edge.swift" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + func binarySearchRightEdge(nums: [Int], target: Int) -> Int { + // Convert to finding the leftmost target + 1 + let i = binarySearchInsertion(nums: nums, target: target + 1) + // j points to the rightmost target, i points to the first element greater than target + let j = i - 1 + // Target not found, return -1 + if j == -1 || nums[j] != target { + return -1 + } + // Found target, return index j + return j + } ``` === "JS" ```javascript title="binary_search_edge.js" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + function binarySearchRightEdge(nums, target) { + // Convert to finding the leftmost target + 1 + const i = binarySearchInsertion(nums, target + 1); + // j points to the rightmost target, i points to the first element greater than target + const j = i - 1; + // Target not found, return -1 + if (j === -1 || nums[j] !== target) { + return -1; + } + // Found target, return index j + return j; + } ``` === "TS" ```typescript title="binary_search_edge.ts" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + function binarySearchRightEdge(nums: Array, target: number): number { + // Convert to finding the leftmost target + 1 + const i = binarySearchInsertion(nums, target + 1); + // j points to the rightmost target, i points to the first element greater than target + const j = i - 1; + // Target not found, return -1 + if (j === -1 || nums[j] !== target) { + return -1; + } + // Found target, return index j + return j; + } ``` === "Dart" ```dart title="binary_search_edge.dart" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + int binarySearchRightEdge(List nums, int target) { + // Convert to finding the leftmost target + 1 + int i = binarySearchInsertion(nums, target + 1); + // j points to the rightmost target, i points to the first element greater than target + int j = i - 1; + // Target not found, return -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // Found target, return index j + return j; + } ``` === "Rust" ```rust title="binary_search_edge.rs" - [class]{}-[func]{binary_search_right_edge} + /* Binary search for the rightmost target */ + fn binary_search_right_edge(nums: &[i32], target: i32) -> i32 { + // Convert to finding the leftmost target + 1 + let i = binary_search_insertion(nums, target + 1); + // j points to the rightmost target, i points to the first element greater than target + let j = i - 1; + // Target not found, return -1 + if j == -1 || nums[j as usize] != target { + return -1; + } + // Found target, return index j + j + } ``` === "C" ```c title="binary_search_edge.c" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + int binarySearchRightEdge(int *nums, int numSize, int target) { + // Convert to finding the leftmost target + 1 + int i = binarySearchInsertion(nums, numSize, target + 1); + // j points to the rightmost target, i points to the first element greater than target + int j = i - 1; + // Target not found, return -1 + if (j == -1 || nums[j] != target) { + return -1; + } + // Found target, return index j + return j; + } ``` === "Kotlin" ```kotlin title="binary_search_edge.kt" - [class]{}-[func]{binarySearchRightEdge} + /* Binary search for the rightmost target */ + fun binarySearchRightEdge(nums: IntArray, target: Int): Int { + // Convert to finding the leftmost target + 1 + val i = binarySearchInsertion(nums, target + 1) + // j points to the rightmost target, i points to the first element greater than target + val j = i - 1 + // Target not found, return -1 + if (j == -1 || nums[j] != target) { + return -1 + } + // Found target, return index j + return j + } ``` === "Ruby" ```ruby title="binary_search_edge.rb" - [class]{}-[func]{binary_search_right_edge} + ### Binary search rightmost target ### + def binary_search_right_edge(nums, target) + # Convert to finding the leftmost target + 1 + i = binary_search_insertion(nums, target + 1) + + # j points to the rightmost target, i points to the first element greater than target + j = i - 1 + + # Target not found, return -1 + return -1 if j == -1 || nums[j] != target + + j # Found target, return index j + end ``` -=== "Zig" +### 2.   Converting to Element Search - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchRightEdge} - ``` +We know that when the array does not contain `target`, $i$ and $j$ will eventually point to the first elements greater than and less than `target`, respectively. -### 2.   Transform into an element search +Therefore, as shown in Figure 10-8, we can construct an element that does not exist in the array to find the left and right boundaries. -When the array does not contain `target`, $i$ and $j$ will eventually point to the first element greater and smaller than `target` respectively. +- Finding the leftmost `target`: Can be converted to finding `target - 0.5` and returning pointer $i$. +- Finding the rightmost `target`: Can be converted to finding `target + 0.5` and returning pointer $j$. -Thus, as shown in Figure 10-8, we can construct an element that does not exist in the array, to search for the left and right boundaries. +![Converting boundary search to element search](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } -- To find the leftmost `target`: it can be transformed into searching for `target - 0.5`, and return the pointer $i$. -- To find the rightmost `target`: it can be transformed into searching for `target + 0.5`, and return the pointer $j$. +

Figure 10-8   Converting boundary search to element search

-![Transforming the search for boundaries into the search for an element](binary_search_edge.assets/binary_search_edge_by_element.png){ class="animation-figure" } +The code is omitted here, but the following two points are worth noting: -

Figure 10-8   Transforming the search for boundaries into the search for an element

- -The code is omitted here, but here are two important points to note about this approach. - -- The given array `nums` does not contain decimal, so handling equal cases is not a concern. -- However, introducing decimals in this approach requires modifying the `target` variable to a floating-point type (no change needed in Python). +- Since the given array does not contain decimals, we don't need to worry about how to handle equal cases. +- Because this method introduces decimals, the variable `target` in the function needs to be changed to a floating-point type (Python does not require this change). diff --git a/en/docs/chapter_searching/binary_search_insertion.md b/en/docs/chapter_searching/binary_search_insertion.md index 41f4655d8..040cfccf3 100644 --- a/en/docs/chapter_searching/binary_search_insertion.md +++ b/en/docs/chapter_searching/binary_search_insertion.md @@ -2,47 +2,47 @@ comments: true --- -# 10.2   Binary search insertion +# 10.2   Binary Search Insertion Point -Binary search is not only used to search for target elements but also to solve many variant problems, such as searching for the insertion position of target elements. +Binary search can not only be used to search for target elements but also to solve many variant problems, such as searching for the insertion position of a target element. -## 10.2.1   Case with no duplicate elements +## 10.2.1   Case Without Duplicate Elements !!! question - Given a sorted array `nums` of length $n$ with unique elements and an element `target`, insert `target` into `nums` while maintaining its sorted order. If `target` already exists in the array, insert it to the left of the existing element. Return the index of `target` in the array after insertion. See the example shown in Figure 10-4. + Given a sorted array `nums` of length $n$ and an element `target`, where the array contains no duplicate elements. Insert `target` into the array `nums` while maintaining its sorted order. If the array already contains the element `target`, insert it to its left. Return the index of `target` in the array after insertion. An example is shown in Figure 10-4. -![Example data for binary search insertion point](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } +![Binary search insertion point example data](binary_search_insertion.assets/binary_search_insertion_example.png){ class="animation-figure" } -

Figure 10-4   Example data for binary search insertion point

+

Figure 10-4   Binary search insertion point example data

-If you want to reuse the binary search code from the previous section, you need to answer the following two questions. +If we want to reuse the binary search code from the previous section, we need to answer the following two questions. -**Question one**: If the array already contains `target`, would the insertion point be the index of existing element? +**Question 1**: When the array contains `target`, is the insertion point index the same as that element's index? -The requirement to insert `target` to the left of equal elements means that the newly inserted `target` will replace the original `target` position. In other words, **when the array contains `target`, the insertion point is indeed the index of that `target`**. +The problem requires inserting `target` to the left of equal elements, which means the newly inserted `target` replaces the position of the original `target`. In other words, **when the array contains `target`, the insertion point index is the index of that `target`**. -**Question two**: When the array does not contain `target`, at which index would it be inserted? +**Question 2**: When the array does not contain `target`, what is the insertion point index? -Let's further consider the binary search process: when `nums[m] < target`, pointer $i$ moves, meaning that pointer $i$ is approaching an element greater than or equal to `target`. Similarly, pointer $j$ is always approaching an element less than or equal to `target`. +Further consider the binary search process: When `nums[m] < target`, $i$ moves, which means pointer $i$ is approaching elements greater than or equal to `target`. Similarly, pointer $j$ is always approaching elements less than or equal to `target`. -Therefore, at the end of the binary, it is certain that: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It is easy to see that when the array does not contain `target`, the insertion point is $i$**. The code is as follows: +Therefore, when the binary search ends, we must have: $i$ points to the first element greater than `target`, and $j$ points to the first element less than `target`. **It's easy to see that when the array does not contain `target`, the insertion index is $i$**. The code is shown below: === "Python" ```python title="binary_search_insertion.py" def binary_search_insertion_simple(nums: list[int], target: int) -> int: """Binary search for insertion point (no duplicate elements)""" - i, j = 0, len(nums) - 1 # Initialize double closed interval [0, n-1] + i, j = 0, len(nums) - 1 # Initialize closed interval [0, n-1] while i <= j: - m = i + (j - i) // 2 # Calculate midpoint index m + m = (i + j) // 2 # Calculate midpoint index m if nums[m] < target: - i = m + 1 # Target is in interval [m+1, j] + i = m + 1 # target is in the interval [m+1, j] elif nums[m] > target: - j = m - 1 # Target is in interval [i, m-1] + j = m - 1 # target is in the interval [i, m-1] else: return m # Found target, return insertion point m - # Did not find target, return insertion point i + # Target not found, return insertion point i return i ``` @@ -51,18 +51,18 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first ```cpp title="binary_search_insertion.cpp" /* Binary search for insertion point (no duplicate elements) */ int binarySearchInsertionSimple(vector &nums, int target) { - int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1] + int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1] while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m + int m = i + (j - i) / 2; // Calculate the midpoint index m if (nums[m] < target) { - i = m + 1; // Target is in interval [m+1, j] + i = m + 1; // target is in the interval [m+1, j] } else if (nums[m] > target) { - j = m - 1; // Target is in interval [i, m-1] + j = m - 1; // target is in the interval [i, m-1] } else { return m; // Found target, return insertion point m } } - // Did not find target, return insertion point i + // Target not found, return insertion point i return i; } ``` @@ -72,18 +72,18 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first ```java title="binary_search_insertion.java" /* Binary search for insertion point (no duplicate elements) */ int binarySearchInsertionSimple(int[] nums, int target) { - int i = 0, j = nums.length - 1; // Initialize double closed interval [0, n-1] + int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1] while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m + int m = i + (j - i) / 2; // Calculate the midpoint index m if (nums[m] < target) { - i = m + 1; // Target is in interval [m+1, j] + i = m + 1; // target is in the interval [m+1, j] } else if (nums[m] > target) { - j = m - 1; // Target is in interval [i, m-1] + j = m - 1; // target is in the interval [i, m-1] } else { return m; // Found target, return insertion point m } } - // Did not find target, return insertion point i + // Target not found, return insertion point i return i; } ``` @@ -91,94 +91,255 @@ Therefore, at the end of the binary, it is certain that: $i$ points to the first === "C#" ```csharp title="binary_search_insertion.cs" - [class]{binary_search_insertion}-[func]{BinarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + int BinarySearchInsertionSimple(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i; + } ``` === "Go" ```go title="binary_search_insertion.go" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + func binarySearchInsertionSimple(nums []int, target int) int { + // Initialize closed interval [0, n-1] + i, j := 0, len(nums)-1 + for i <= j { + // Calculate the midpoint index m + m := i + (j-i)/2 + if nums[m] < target { + // target is in the interval [m+1, j] + i = m + 1 + } else if nums[m] > target { + // target is in the interval [i, m-1] + j = m - 1 + } else { + // Found target, return insertion point m + return m + } + } + // Target not found, return insertion point i + return i + } ``` === "Swift" ```swift title="binary_search_insertion.swift" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + func binarySearchInsertionSimple(nums: [Int], target: Int) -> Int { + // Initialize closed interval [0, n-1] + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // Calculate the midpoint index m + if nums[m] < target { + i = m + 1 // target is in the interval [m+1, j] + } else if nums[m] > target { + j = m - 1 // target is in the interval [i, m-1] + } else { + return m // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i + } ``` === "JS" ```javascript title="binary_search_insertion.js" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + function binarySearchInsertionSimple(nums, target) { + let i = 0, + j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i; + } ``` === "TS" ```typescript title="binary_search_insertion.ts" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + function binarySearchInsertionSimple( + nums: Array, + target: number + ): number { + let i = 0, + j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i; + } ``` === "Dart" ```dart title="binary_search_insertion.dart" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + int binarySearchInsertionSimple(List nums, int target) { + int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) ~/ 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i; + } ``` === "Rust" ```rust title="binary_search_insertion.rs" - [class]{}-[func]{binary_search_insertion_simple} + /* Binary search for insertion point (no duplicate elements) */ + fn binary_search_insertion_simple(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // Calculate the midpoint index m + if nums[m as usize] < target { + i = m + 1; // target is in the interval [m+1, j] + } else if nums[m as usize] > target { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; + } + } + // Target not found, return insertion point i + i + } ``` === "C" ```c title="binary_search_insertion.c" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + int binarySearchInsertionSimple(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + return m; // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_insertion.kt" - [class]{}-[func]{binarySearchInsertionSimple} + /* Binary search for insertion point (no duplicate elements) */ + fun binarySearchInsertionSimple(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // Initialize closed interval [0, n-1] + while (i <= j) { + val m = i + (j - i) / 2 // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1 // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1 // target is in the interval [i, m-1] + } else { + return m // Found target, return insertion point m + } + } + // Target not found, return insertion point i + return i + } ``` === "Ruby" ```ruby title="binary_search_insertion.rb" - [class]{}-[func]{binary_search_insertion_simple} + ### Binary search insertion point (no duplicates) ### + def binary_search_insertion_simple(nums, target) + # Initialize closed interval [0, n-1] + i, j = 0, nums.length - 1 + + while i <= j + # Calculate the midpoint index m + m = (i + j) / 2 + + if nums[m] < target + i = m + 1 # target is in the interval [m+1, j] + elsif nums[m] > target + j = m - 1 # target is in the interval [i, m-1] + else + return m # Found target, return insertion point m + end + end + + i # Target not found, return insertion point i + end ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertionSimple} - ``` - -## 10.2.2   Case with duplicate elements +## 10.2.2   Case with Duplicate Elements !!! question - Based on the previous question, assume the array may contain duplicate elements, all else remains the same. + Based on the previous problem, assume the array may contain duplicate elements, with everything else remaining the same. -When there are multiple occurrences of `target` in the array, a regular binary search can only return the index of one occurrence of `target`, **and it cannot determine how many occurrences of `target` are to the left and right of that position**. +Suppose there are multiple `target` elements in the array. Ordinary binary search can only return the index of one `target`, **and cannot determine how many `target` elements are to the left and right of that element**. -The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. Initially consider implementing this through the steps shown in Figure 10-5. +The problem requires inserting the target element at the leftmost position, **so we need to find the index of the leftmost `target` in the array**. Initially, consider implementing this through the steps shown in Figure 10-5: -1. Perform a binary search to find any index of `target`, say $k$. -2. Starting from index $k$, conduct a linear search to the left until the leftmost occurrence of `target` is found, then return this index. +1. Perform binary search to obtain the index of any `target`, denoted as $k$. +2. Starting from index $k$, perform linear traversal to the left, and return when the leftmost `target` is found. -![Linear search for the insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" } +![Linear search for insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_naive.png){ class="animation-figure" } -

Figure 10-5   Linear search for the insertion point of duplicate elements

+

Figure 10-5   Linear search for insertion point of duplicate elements

-Although this method is feasible, it includes linear search, so its time complexity is $O(n)$. This method is inefficient when the array contains many duplicate `target`s. +Although this method works, it includes linear search, resulting in a time complexity of $O(n)$. When the array contains many duplicate `target` elements, this method is very inefficient. -Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains the same. In each round, we first calculate the middle index $m$, then compare the value of `target` with `nums[m]`, leading to the following cases. +Now consider extending the binary search code. As shown in Figure 10-6, the overall process remains unchanged: calculate the midpoint index $m$ in each round, then compare `target` with `nums[m]`, divided into the following cases: -- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, thus use the normal binary search to narrow the search range, **bringing pointers $i$ and $j$ closer to `target`**. -- When `nums[m] == target`, it indicates that the elements less than `target` are in the range $[i, m - 1]$, therefore use $j = m - 1$ to narrow the range, **thus bringing pointer $j$ closer to the elements less than `target`**. +- When `nums[m] < target` or `nums[m] > target`, it means `target` has not been found yet, so use the ordinary binary search interval narrowing operation to **make pointers $i$ and $j$ approach `target`**. +- When `nums[m] == target`, it means elements less than `target` are in the interval $[i, m - 1]$, so use $j = m - 1$ to narrow the interval, thereby **making pointer $j$ approach elements less than `target`**. -After the loop, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **therefore index $i$ is the insertion point**. +After the loop completes, $i$ points to the leftmost `target`, and $j$ points to the first element less than `target`, **so index $i$ is the insertion point**. === "<1>" ![Steps for binary search insertion point of duplicate elements](binary_search_insertion.assets/binary_search_insertion_step1.png){ class="animation-figure" } @@ -206,24 +367,24 @@ After the loop, $i$ points to the leftmost `target`, and $j$ points to the first

Figure 10-6   Steps for binary search insertion point of duplicate elements

-Observe the following code. The operations in the branches `nums[m] > target` and `nums[m] == target` are the same, so these two branches can be merged. +Observe the following code: the operations for branches `nums[m] > target` and `nums[m] == target` are the same, so the two can be merged. -Even so, we can still keep the conditions expanded, as it makes the logic clearer and improves readability. +Even so, we can still keep the conditional branches expanded, as the logic is clearer and more readable. === "Python" ```python title="binary_search_insertion.py" def binary_search_insertion(nums: list[int], target: int) -> int: """Binary search for insertion point (with duplicate elements)""" - i, j = 0, len(nums) - 1 # Initialize double closed interval [0, n-1] + i, j = 0, len(nums) - 1 # Initialize closed interval [0, n-1] while i <= j: - m = i + (j - i) // 2 # Calculate midpoint index m + m = (i + j) // 2 # Calculate midpoint index m if nums[m] < target: - i = m + 1 # Target is in interval [m+1, j] + i = m + 1 # target is in the interval [m+1, j] elif nums[m] > target: - j = m - 1 # Target is in interval [i, m-1] + j = m - 1 # target is in the interval [i, m-1] else: - j = m - 1 # First element less than target is in interval [i, m-1] + j = m - 1 # The first element less than target is in the interval [i, m-1] # Return insertion point i return i ``` @@ -233,15 +394,15 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare ```cpp title="binary_search_insertion.cpp" /* Binary search for insertion point (with duplicate elements) */ int binarySearchInsertion(vector &nums, int target) { - int i = 0, j = nums.size() - 1; // Initialize double closed interval [0, n-1] + int i = 0, j = nums.size() - 1; // Initialize closed interval [0, n-1] while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m + int m = i + (j - i) / 2; // Calculate the midpoint index m if (nums[m] < target) { - i = m + 1; // Target is in interval [m+1, j] + i = m + 1; // target is in the interval [m+1, j] } else if (nums[m] > target) { - j = m - 1; // Target is in interval [i, m-1] + j = m - 1; // target is in the interval [i, m-1] } else { - j = m - 1; // First element less than target is in interval [i, m-1] + j = m - 1; // The first element less than target is in the interval [i, m-1] } } // Return insertion point i @@ -254,15 +415,15 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare ```java title="binary_search_insertion.java" /* Binary search for insertion point (with duplicate elements) */ int binarySearchInsertion(int[] nums, int target) { - int i = 0, j = nums.length - 1; // Initialize double closed interval [0, n-1] + int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1] while (i <= j) { - int m = i + (j - i) / 2; // Calculate midpoint index m + int m = i + (j - i) / 2; // Calculate the midpoint index m if (nums[m] < target) { - i = m + 1; // Target is in interval [m+1, j] + i = m + 1; // target is in the interval [m+1, j] } else if (nums[m] > target) { - j = m - 1; // Target is in interval [i, m-1] + j = m - 1; // target is in the interval [i, m-1] } else { - j = m - 1; // First element less than target is in interval [i, m-1] + j = m - 1; // The first element less than target is in the interval [i, m-1] } } // Return insertion point i @@ -273,73 +434,231 @@ Even so, we can still keep the conditions expanded, as it makes the logic cleare === "C#" ```csharp title="binary_search_insertion.cs" - [class]{binary_search_insertion}-[func]{BinarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + int BinarySearchInsertion(int[] nums, int target) { + int i = 0, j = nums.Length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i; + } ``` === "Go" ```go title="binary_search_insertion.go" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + func binarySearchInsertion(nums []int, target int) int { + // Initialize closed interval [0, n-1] + i, j := 0, len(nums)-1 + for i <= j { + // Calculate the midpoint index m + m := i + (j-i)/2 + if nums[m] < target { + // target is in the interval [m+1, j] + i = m + 1 + } else if nums[m] > target { + // target is in the interval [i, m-1] + j = m - 1 + } else { + // The first element less than target is in the interval [i, m-1] + j = m - 1 + } + } + // Return insertion point i + return i + } ``` === "Swift" ```swift title="binary_search_insertion.swift" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + func binarySearchInsertion(nums: [Int], target: Int) -> Int { + // Initialize closed interval [0, n-1] + var i = nums.startIndex + var j = nums.endIndex - 1 + while i <= j { + let m = i + (j - i) / 2 // Calculate the midpoint index m + if nums[m] < target { + i = m + 1 // target is in the interval [m+1, j] + } else if nums[m] > target { + j = m - 1 // target is in the interval [i, m-1] + } else { + j = m - 1 // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i + } ``` === "JS" ```javascript title="binary_search_insertion.js" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + function binarySearchInsertion(nums, target) { + let i = 0, + j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i; + } ``` === "TS" ```typescript title="binary_search_insertion.ts" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + function binarySearchInsertion(nums: Array, target: number): number { + let i = 0, + j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + const m = Math.floor(i + (j - i) / 2); // Calculate midpoint index m, use Math.floor() to round down + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i; + } ``` === "Dart" ```dart title="binary_search_insertion.dart" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + int binarySearchInsertion(List nums, int target) { + int i = 0, j = nums.length - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) ~/ 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i; + } ``` === "Rust" ```rust title="binary_search_insertion.rs" - [class]{}-[func]{binary_search_insertion} + /* Binary search for insertion point (with duplicate elements) */ + pub fn binary_search_insertion(nums: &[i32], target: i32) -> i32 { + let (mut i, mut j) = (0, nums.len() as i32 - 1); // Initialize closed interval [0, n-1] + while i <= j { + let m = i + (j - i) / 2; // Calculate the midpoint index m + if nums[m as usize] < target { + i = m + 1; // target is in the interval [m+1, j] + } else if nums[m as usize] > target { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + i + } ``` === "C" ```c title="binary_search_insertion.c" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + int binarySearchInsertion(int *nums, int numSize, int target) { + int i = 0, j = numSize - 1; // Initialize closed interval [0, n-1] + while (i <= j) { + int m = i + (j - i) / 2; // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1; // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1; // target is in the interval [i, m-1] + } else { + j = m - 1; // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i; + } ``` === "Kotlin" ```kotlin title="binary_search_insertion.kt" - [class]{}-[func]{binarySearchInsertion} + /* Binary search for insertion point (with duplicate elements) */ + fun binarySearchInsertion(nums: IntArray, target: Int): Int { + var i = 0 + var j = nums.size - 1 // Initialize closed interval [0, n-1] + while (i <= j) { + val m = i + (j - i) / 2 // Calculate the midpoint index m + if (nums[m] < target) { + i = m + 1 // target is in the interval [m+1, j] + } else if (nums[m] > target) { + j = m - 1 // target is in the interval [i, m-1] + } else { + j = m - 1 // The first element less than target is in the interval [i, m-1] + } + } + // Return insertion point i + return i + } ``` === "Ruby" ```ruby title="binary_search_insertion.rb" - [class]{}-[func]{binary_search_insertion} - ``` + ### Binary search insertion point (with duplicates) ### + def binary_search_insertion(nums, target) + # Initialize closed interval [0, n-1] + i, j = 0, nums.length - 1 -=== "Zig" + while i <= j + # Calculate the midpoint index m + m = (i + j) / 2 - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertion} + if nums[m] < target + i = m + 1 # target is in the interval [m+1, j] + elsif nums[m] > target + j = m - 1 # target is in the interval [i, m-1] + else + j = m - 1 # The first element less than target is in the interval [i, m-1] + end + end + + i # Return insertion point i + end ``` !!! tip - The code in this section uses "closed interval". If you are interested in "left-closed, right-open", try to implement the code on your own. + The code in this section all uses the "closed interval" approach. Interested readers can implement the "left-closed right-open" approach themselves. -In summary, binary search essentially involves setting search targets for pointers $i$ and $j$. These targets could be a specific element (like `target`) or a range of elements (such as those smaller than `target`). +Overall, binary search is simply about setting search targets for pointers $i$ and $j$ separately. The target could be a specific element (such as `target`) or a range of elements (such as elements less than `target`). -In the continuous loop of binary search, pointers $i$ and $j$ gradually approach the predefined target. Ultimately, they either find the answer or stop after crossing the boundary. +Through continuous binary iterations, both pointers $i$ and $j$ gradually approach their preset targets. Ultimately, they either successfully find the answer or stop after crossing the boundaries. diff --git a/en/docs/chapter_searching/index.md b/en/docs/chapter_searching/index.md index e519df592..7dae9ebd4 100644 --- a/en/docs/chapter_searching/index.md +++ b/en/docs/chapter_searching/index.md @@ -9,15 +9,15 @@ icon: material/text-search !!! abstract - Searching is an adventure into the unknown; where we may need to traverse every corner of a mysterious space, or perhaps we’ll quickly locate our target. - - On this journey of discovery, each exploration may end up with an unexpected answer. + Searching is an adventure into the unknown, where we may need to traverse every corner of the mysterious space, or we may be able to quickly lock onto the target. + + In this journey of discovery, each exploration may yield an unexpected answer. ## Chapter contents -- [10.1   Binary search](binary_search.md) -- [10.2   Binary search insertion](binary_search_insertion.md) -- [10.3   Binary search boundaries](binary_search_edge.md) -- [10.4   Hashing optimization strategies](replace_linear_by_hashing.md) -- [10.5   Search algorithms revisited](searching_algorithm_revisited.md) +- [10.1   Binary Search](binary_search.md) +- [10.2   Binary Search Insertion](binary_search_insertion.md) +- [10.3   Binary Search Edge Cases](binary_search_edge.md) +- [10.4   Hash Optimization Strategy](replace_linear_by_hashing.md) +- [10.5   Search Algorithms Revisited](searching_algorithm_revisited.md) - [10.6   Summary](summary.md) diff --git a/en/docs/chapter_searching/replace_linear_by_hashing.md b/en/docs/chapter_searching/replace_linear_by_hashing.md index a8bd7fe2e..fd76520ec 100644 --- a/en/docs/chapter_searching/replace_linear_by_hashing.md +++ b/en/docs/chapter_searching/replace_linear_by_hashing.md @@ -2,21 +2,21 @@ comments: true --- -# 10.4   Hash optimization strategies +# 10.4   Hash Optimization Strategy -In algorithm problems, **we often reduce the time complexity of an algorithm by replacing a linear search with a hash-based search**. Let's use an algorithm problem to deepen the understanding. +In algorithm problems, **we often reduce the time complexity of algorithms by replacing linear search with hash-based search**. Let's use an algorithm problem to deepen our understanding. !!! question - Given an integer array `nums` and a target element `target`, please search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution is acceptable. + Given an integer array `nums` and a target element `target`, search for two elements in the array whose "sum" equals `target`, and return their array indices. Any solution will do. -## 10.4.1   Linear search: trading time for space +## 10.4.1   Linear Search: Trading Time for Space -Consider traversing through all possible combinations directly. As shown in Figure 10-9, we initiate a nested loop, and in each iteration, we determine whether the sum of the two integers equals `target`. If so, we return their indices. +Consider directly traversing all possible combinations. As shown in Figure 10-9, we open a two-layer loop and judge in each round whether the sum of two integers equals `target`. If so, return their indices. -![Linear search solution for two-sum problem](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } +![Linear search solution for two sum](replace_linear_by_hashing.assets/two_sum_brute_force.png){ class="animation-figure" } -

Figure 10-9   Linear search solution for two-sum problem

+

Figure 10-9   Linear search solution for two sum

The code is shown below: @@ -24,8 +24,8 @@ The code is shown below: ```python title="two_sum.py" def two_sum_brute_force(nums: list[int], target: int) -> list[int]: - """Method one: Brute force enumeration""" - # Two-layer loop, time complexity is O(n^2) + """Method 1: Brute force enumeration""" + # Two nested loops, time complexity is O(n^2) for i in range(len(nums) - 1): for j in range(i + 1, len(nums)): if nums[i] + nums[j] == target: @@ -36,10 +36,10 @@ The code is shown below: === "C++" ```cpp title="two_sum.cpp" - /* Method one: Brute force enumeration */ + /* Method 1: Brute force enumeration */ vector twoSumBruteForce(vector &nums, int target) { int size = nums.size(); - // Two-layer loop, time complexity is O(n^2) + // Two nested loops, time complexity is 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) @@ -53,10 +53,10 @@ The code is shown below: === "Java" ```java title="two_sum.java" - /* Method one: Brute force enumeration */ + /* Method 1: Brute force enumeration */ int[] twoSumBruteForce(int[] nums, int target) { int size = nums.length; - // Two-layer loop, time complexity is O(n^2) + // Two nested loops, time complexity is 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) @@ -70,80 +70,188 @@ The code is shown below: === "C#" ```csharp title="two_sum.cs" - [class]{two_sum}-[func]{TwoSumBruteForce} + /* Method 1: Brute force enumeration */ + int[] TwoSumBruteForce(int[] nums, int target) { + int size = nums.Length; + // Two nested loops, time complexity is O(n^2) + for (int i = 0; i < size - 1; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) + return [i, j]; + } + } + return []; + } ``` === "Go" ```go title="two_sum.go" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + func twoSumBruteForce(nums []int, target int) []int { + size := len(nums) + // Two nested loops, time complexity is O(n^2) + for i := 0; i < size-1; i++ { + for j := i + 1; j < size; j++ { + if nums[i]+nums[j] == target { + return []int{i, j} + } + } + } + return nil + } ``` === "Swift" ```swift title="two_sum.swift" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + func twoSumBruteForce(nums: [Int], target: Int) -> [Int] { + // Two nested loops, time complexity is 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" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + function twoSumBruteForce(nums, target) { + const n = nums.length; + // Two nested loops, time complexity is 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" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + function twoSumBruteForce(nums: number[], target: number): number[] { + const n = nums.length; + // Two nested loops, time complexity is 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" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + List twoSumBruteForce(List nums, int target) { + int size = nums.length; + // Two nested loops, time complexity is 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" - [class]{}-[func]{two_sum_brute_force} + /* Method 1: Brute force enumeration */ + pub fn two_sum_brute_force(nums: &Vec, target: i32) -> Option> { + let size = nums.len(); + // Two nested loops, time complexity is 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" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + int *twoSumBruteForce(int *nums, int numsSize, int target, int *returnSize) { + for (int i = 0; i < numsSize; ++i) { + for (int j = i + 1; j < numsSize; ++j) { + if (nums[i] + nums[j] == target) { + int *res = malloc(sizeof(int) * 2); + res[0] = i, res[1] = j; + *returnSize = 2; + return res; + } + } + } + *returnSize = 0; + return NULL; + } ``` === "Kotlin" ```kotlin title="two_sum.kt" - [class]{}-[func]{twoSumBruteForce} + /* Method 1: Brute force enumeration */ + fun twoSumBruteForce(nums: IntArray, target: Int): IntArray { + val size = nums.size + // Two nested loops, time complexity is O(n^2) + for (i in 0.." - ![Help hash table solve two-sum](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" } + ![Hash table solution for two sum](replace_linear_by_hashing.assets/two_sum_hashtable_step1.png){ class="animation-figure" } === "<2>" ![two_sum_hashtable_step2](replace_linear_by_hashing.assets/two_sum_hashtable_step2.png){ class="animation-figure" } @@ -151,7 +259,7 @@ Consider using a hash table, where the key-value pairs are the array elements an === "<3>" ![two_sum_hashtable_step3](replace_linear_by_hashing.assets/two_sum_hashtable_step3.png){ class="animation-figure" } -

Figure 10-10   Help hash table solve two-sum

+

Figure 10-10   Hash table solution for two sum

The implementation code is shown below, requiring only a single loop: @@ -159,10 +267,10 @@ The implementation code is shown below, requiring only a single loop: ```python title="two_sum.py" def two_sum_hash_table(nums: list[int], target: int) -> list[int]: - """Method two: Auxiliary hash table""" + """Method 2: Auxiliary hash table""" # Auxiliary hash table, space complexity is O(n) dic = {} - # Single-layer loop, time complexity is O(n) + # Single loop, time complexity is O(n) for i in range(len(nums)): if target - nums[i] in dic: return [dic[target - nums[i]], i] @@ -173,12 +281,12 @@ The implementation code is shown below, requiring only a single loop: === "C++" ```cpp title="two_sum.cpp" - /* Method two: Auxiliary hash table */ + /* Method 2: Auxiliary hash table */ vector twoSumHashTable(vector &nums, int target) { int size = nums.size(); // Auxiliary hash table, space complexity is O(n) unordered_map dic; - // Single-layer loop, time complexity is O(n) + // Single loop, time complexity is O(n) for (int i = 0; i < size; i++) { if (dic.find(target - nums[i]) != dic.end()) { return {dic[target - nums[i]], i}; @@ -192,12 +300,12 @@ The implementation code is shown below, requiring only a single loop: === "Java" ```java title="two_sum.java" - /* Method two: Auxiliary hash table */ + /* Method 2: Auxiliary hash table */ int[] twoSumHashTable(int[] nums, int target) { int size = nums.length; // Auxiliary hash table, space complexity is O(n) Map dic = new HashMap<>(); - // Single-layer loop, time complexity is O(n) + // Single loop, time complexity is O(n) for (int i = 0; i < size; i++) { if (dic.containsKey(target - nums[i])) { return new int[] { dic.get(target - nums[i]), i }; @@ -211,71 +319,218 @@ The implementation code is shown below, requiring only a single loop: === "C#" ```csharp title="two_sum.cs" - [class]{two_sum}-[func]{TwoSumHashTable} + /* Method 2: Auxiliary hash table */ + int[] TwoSumHashTable(int[] nums, int target) { + int size = nums.Length; + // Auxiliary hash table, space complexity is O(n) + Dictionary dic = []; + // Single loop, time complexity is O(n) + for (int i = 0; i < size; i++) { + if (dic.ContainsKey(target - nums[i])) { + return [dic[target - nums[i]], i]; + } + dic.Add(nums[i], i); + } + return []; + } ``` === "Go" ```go title="two_sum.go" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + func twoSumHashTable(nums []int, target int) []int { + // Auxiliary hash table, space complexity is O(n) + hashTable := map[int]int{} + // Single loop, time complexity is 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" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + func twoSumHashTable(nums: [Int], target: Int) -> [Int] { + // Auxiliary hash table, space complexity is O(n) + var dic: [Int: Int] = [:] + // Single loop, time complexity is 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" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + function twoSumHashTable(nums, target) { + // Auxiliary hash table, space complexity is O(n) + let m = {}; + // Single loop, time complexity is 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" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + function twoSumHashTable(nums: number[], target: number): number[] { + // Auxiliary hash table, space complexity is O(n) + let m: Map = new Map(); + // Single loop, time complexity is 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" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + List twoSumHashTable(List nums, int target) { + int size = nums.length; + // Auxiliary hash table, space complexity is O(n) + Map dic = HashMap(); + // Single loop, time complexity is 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" - [class]{}-[func]{two_sum_hash_table} + /* Method 2: Auxiliary hash table */ + pub fn two_sum_hash_table(nums: &Vec, target: i32) -> Option> { + // Auxiliary hash table, space complexity is O(n) + let mut dic = HashMap::new(); + // Single loop, time complexity is 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" - [class]{HashTable}-[func]{} + /* Hash table */ + typedef struct { + int key; + int val; + UT_hash_handle hh; // Implemented using uthash.h + } HashTable; - [class]{}-[func]{twoSumHashTable} + /* Hash table lookup */ + HashTable *find(HashTable *h, int key) { + HashTable *tmp; + HASH_FIND_INT(h, &key, tmp); + return tmp; + } + + /* Hash table element insertion */ + 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; + } + } + + /* Method 2: Auxiliary hash table */ + int *twoSumHashTable(int *nums, int numsSize, int target, int *returnSize) { + HashTable *hashtable = NULL; + for (int i = 0; i < numsSize; i++) { + HashTable *t = find(hashtable, target - nums[i]); + if (t != NULL) { + int *res = malloc(sizeof(int) * 2); + res[0] = t->val, res[1] = i; + *returnSize = 2; + return res; + } + insert(&hashtable, nums[i], i); + } + *returnSize = 0; + return NULL; + } ``` === "Kotlin" ```kotlin title="two_sum.kt" - [class]{}-[func]{twoSumHashTable} + /* Method 2: Auxiliary hash table */ + fun twoSumHashTable(nums: IntArray, target: Int): IntArray { + val size = nums.size + // Auxiliary hash table, space complexity is O(n) + val dic = HashMap() + // Single loop, time complexity is O(n) + for (i in 0..Searching algorithms (search algorithms)
are used to retrieve one or more elements that meet specific criteria within data structures such as arrays, linked lists, trees, or graphs. +Searching algorithms are used to search for one or a group of elements that meet specific conditions in data structures (such as arrays, linked lists, trees, or graphs). -Searching algorithms can be divided into the following two categories based on their approach. +Searching algorithms can be divided into the following two categories based on their implementation approach: -- **Locating the target element by traversing the data structure**, such as traversals of arrays, linked lists, trees, and graphs, etc. -- **Using the organizational structure of the data or existing data to achieve efficient element searches**, such as binary search, hash search, binary search tree search, etc. +- **Locating target elements by traversing the data structure**, such as traversing arrays, linked lists, trees, and graphs. +- **Achieving efficient element search by utilizing data organization structure or prior information contained in the data**, such as binary search, hash-based search, and binary search tree search. -These topics were introduced in previous chapters, so they are not unfamiliar to us. In this section, we will revisit searching algorithms from a more systematic perspective. +It's not hard to see that these topics have all been covered in previous chapters, so searching algorithms are not unfamiliar to us. In this section, we will approach from a more systematic perspective and re-examine searching algorithms. -## 10.5.1   Brute-force search +## 10.5.1   Brute-Force Search -A Brute-force search locates the target element by traversing every element of the data structure. +Brute-force search locates target elements by traversing each element of the data structure. -- "Linear search" is suitable for linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses each element one by one until the target element is found or the other end is reached without finding the target element. -- "Breadth-first search" and "Depth-first search" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer (left to right), accessing nodes from near to far. Depth-first search starts from the initial node, follows a path until the end (top to bottom), then backtracks and tries other paths until the entire data structure is traversed. +- "Linear search" is applicable to linear data structures such as arrays and linked lists. It starts from one end of the data structure and accesses elements one by one until the target element is found or the other end is reached without finding the target element. +- "Breadth-first search" and "depth-first search" are two traversal strategies for graphs and trees. Breadth-first search starts from the initial node and searches layer by layer, visiting nodes from near to far. Depth-first search starts from the initial node, follows a path to the end, then backtracks and tries other paths until the entire data structure is traversed. -The advantage of brute-force search is its simplicity and versatility, **no need for data preprocessing or the help of additional data structures**. +The advantage of brute-force search is that it is simple and has good generality, **requiring no data preprocessing or additional data structures**. -However, **the time complexity of this type of algorithm is $O(n)$**, where $n$ is the number of elements, so the performance is poor with large data sets. +However, **the time complexity of such algorithms is $O(n)$**, where $n$ is the number of elements, so performance is poor when dealing with large amounts of data. -## 10.5.2   Adaptive search +## 10.5.2   Adaptive Search -An Adaptive search uses the unique properties of data (such as order) to optimize the search process, thereby locating the target element more efficiently. +Adaptive search utilizes the unique properties of data (such as orderliness) to optimize the search process, thereby locating target elements more efficiently. -- "Binary search" uses the orderliness of data to achieve efficient searching, only suitable for arrays. -- "Hash search" uses a hash table to establish a key-value mapping between search data and target data, thus implementing the query operation. -- "Tree search" in a specific tree structure (such as a binary search tree), quickly eliminates nodes based on node value comparisons, thus locating the target element. +- "Binary search" uses the orderliness of data to achieve efficient searching, applicable only to arrays. +- "Hash-based search" uses hash tables to establish key-value pair mappings between search data and target data, thereby achieving query operations. +- "Tree search" in specific tree structures (such as binary search trees), quickly eliminates nodes based on comparing node values to locate target elements. -The advantage of these algorithms is high efficiency, **with time complexities reaching $O(\log n)$ or even $O(1)$**. +The advantage of such algorithms is high efficiency, **with time complexity reaching $O(\log n)$ or even $O(1)$**. -However, **using these algorithms often requires data preprocessing**. For example, binary search requires sorting the array in advance, and hash search and tree search both require the help of additional data structures. Maintaining these structures also requires more overhead in terms of time and space. +However, **using these algorithms often requires data preprocessing**. For example, binary search requires pre-sorting the array, while hash-based search and tree search both require additional data structures, and maintaining these data structures also requires extra time and space overhead. !!! tip - Adaptive search algorithms are often referred to as search algorithms, **mainly used for quickly retrieving target elements in specific data structures**. + Adaptive search algorithms are often called lookup algorithms, **mainly used to quickly retrieve target elements in specific data structures**. -## 10.5.3   Choosing a search method +## 10.5.3   Search Method Selection -Given a set of data of size $n$, we can use a linear search, binary search, tree search, hash search, or other methods to retrieve the target element. The working principles of these methods are shown in Figure 10-11. +Given a dataset of size $n$, we can use linear search, binary search, tree search, hash-based search, and other methods to search for the target element. The working principles of each method are shown in Figure 10-11. -![Various search strategies](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" } +![Multiple search strategies](searching_algorithm_revisited.assets/searching_algorithms.png){ class="animation-figure" } -

Figure 10-11   Various search strategies

+

Figure 10-11   Multiple search strategies

-The characteristics and operational efficiency of the aforementioned methods are shown in the following table. +The operational efficiency and characteristics of the above methods are as follows:

Table 10-1   Comparison of search algorithm efficiency

-| | Linear search | Binary search | Tree search | Hash search | +| | Linear search | Binary search | Tree search | Hash-based search | | ------------------ | ------------- | --------------------- | --------------------------- | -------------------------- | | Search element | $O(n)$ | $O(\log n)$ | $O(\log n)$ | $O(1)$ | | Insert element | $O(1)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | | Delete element | $O(n)$ | $O(n)$ | $O(\log n)$ | $O(1)$ | | Extra space | $O(1)$ | $O(1)$ | $O(n)$ | $O(n)$ | -| Data preprocessing | / | Sorting $O(n \log n)$ | Building tree $O(n \log n)$ | Building hash table $O(n)$ | -| Data orderliness | Unordered | Ordered | Ordered | Unordered | +| Data preprocessing | / | Sorting $O(n \log n)$ | Tree building $O(n \log n)$ | Hash table building $O(n)$ | +| Data ordered | Unordered | Ordered | Ordered | Unordered |
-The choice of search algorithm also depends on the volume of data, search performance requirements, frequency of data queries and updates, etc. +The choice of search algorithm also depends on data volume, search performance requirements, data query and update frequency, etc. **Linear search** -- Good versatility, no need for any data preprocessing operations. If we only need to query the data once, then the time for data preprocessing in the other three methods would be longer than the time for a linear search. -- Suitable for small volumes of data, where time complexity has a smaller impact on efficiency. -- Suitable for scenarios with very frequent data updates, because this method does not require any additional maintenance of the data. +- Good generality, requiring no data preprocessing operations. If we only need to query the data once, the data preprocessing time for the other three methods would be longer than linear search. +- Suitable for small data volumes, where time complexity has less impact on efficiency. +- Suitable for scenarios with high data update frequency, as this method does not require any additional data maintenance. **Binary search** -- Suitable for larger data volumes, with stable performance and a worst-case time complexity of $O(\log n)$. -- However, the data volume cannot be too large, because storing arrays requires contiguous memory space. -- Not suitable for scenarios with frequent additions and deletions, because maintaining an ordered array incurs a lot of overhead. +- Suitable for large data volumes with stable efficiency performance, worst-case time complexity of $O(\log n)$. +- Data volume cannot be too large, as storing arrays requires contiguous memory space. +- Not suitable for scenarios with frequent data insertion and deletion, as maintaining a sorted array has high overhead. -**Hash search** +**Hash-based search** -- Suitable for scenarios where fast query performance is essential, with an average time complexity of $O(1)$. -- Not suitable for scenarios needing ordered data or range searches, because hash tables cannot maintain data orderliness. -- High dependency on hash functions and hash collision handling strategies, with significant performance degradation risks. -- Not suitable for overly large data volumes, because hash tables need extra space to minimize collisions and provide good query performance. +- Suitable for scenarios with high query performance requirements, with an average time complexity of $O(1)$. +- Not suitable for scenarios requiring ordered data or range searches, as hash tables cannot maintain data orderliness. +- High dependence on hash functions and hash collision handling strategies, with significant risk of performance degradation. +- Not suitable for excessively large data volumes, as hash tables require extra space to minimize collisions and thus provide good query performance. **Tree search** -- Suitable for massive data, because tree nodes are stored scattered in memory. -- Suitable for maintaining ordered data or range searches. -- With the continuous addition and deletion of nodes, the binary search tree may become skewed, degrading the time complexity to $O(n)$. -- If using AVL trees or red-black trees, operations can run stably at $O(\log n)$ efficiency, but the operation to maintain tree balance adds extra overhead. +- Suitable for massive data, as tree nodes are stored dispersedly in memory. +- Suitable for scenarios requiring maintained ordered data or range searches. +- During continuous node insertion and deletion, binary search trees may become skewed, degrading time complexity to $O(n)$. +- If using AVL trees or red-black trees, all operations can run stably at $O(\log n)$ efficiency, but operations to maintain tree balance add extra overhead. diff --git a/en/docs/chapter_searching/summary.md b/en/docs/chapter_searching/summary.md index a8dfd4366..5cc10250c 100644 --- a/en/docs/chapter_searching/summary.md +++ b/en/docs/chapter_searching/summary.md @@ -4,9 +4,11 @@ comments: true # 10.6   Summary -- Binary search depends on the order of data and performs the search by iteratively halving the search interval. It requires the input data to be sorted and is only applicable to arrays or array-based data structures. -- Brute force search may be required to locate an entry in an unordered dataset. Different search algorithms can be applied based on the data structure: Linear search is suitable for arrays and linked lists, while breadth-first search (BFS) and depth-first search (DFS) are suitable for graphs and trees. These algorithms are highly versatile, requiring no preprocessing of data, but they have a higher time complexity of $O(n)$. -- Hash search, tree search, and binary search are efficient search methods that can quickly locate target elements within specific data structures. These algorithms are highly efficient, with time complexities reaching $O(\log n)$ or even $O(1)$, but they usually require extra space to accommodate additional data structures. -- In practice, we need to analyze factors such as data volume, search performance requirements, data query and update frequencies, etc., to choose an appropriate search method. -- Linear search is ideal for small or frequently updated (volatile) data. Binary search works well for large and sorted data. Hash search is suitable for data that requires high query efficiency and does not need range queries. Tree search is best suited for large dynamic data that require maintaining order and need to support range queries. -- Replacing linear search with hash search is a common strategy to optimize runtime performance, reducing the time complexity from $O(n)$ to $O(1)$. +### 1.   Key Review + +- Binary search relies on data orderliness and progressively reduces the search interval by half through loops. It requires input data to be sorted and is only applicable to arrays or data structures based on array implementations. +- Brute-force search locates data by traversing the data structure. Linear search is applicable to arrays and linked lists, while breadth-first search and depth-first search are applicable to graphs and trees. Such algorithms have good generality and require no data preprocessing, but have a relatively high time complexity of $O(n)$. +- Hash-based search, tree search, and binary search are efficient search methods that can quickly locate target elements in specific data structures. Such algorithms are highly efficient with time complexity reaching $O(\log n)$ or even $O(1)$, but typically require additional data structures. +- In practice, we need to analyze factors such as data scale, search performance requirements, and data query and update frequency to choose the appropriate search method. +- Linear search is suitable for small-scale or frequently updated data; binary search is suitable for large-scale, sorted data; hash-based search is suitable for data with high query efficiency requirements and no need for range queries; tree search is suitable for large-scale dynamic data that needs to maintain order and support range queries. +- Replacing linear search with hash-based search is a commonly used strategy to optimize runtime, reducing time complexity from $O(n)$ to $O(1)$. diff --git a/en/docs/chapter_sorting/bubble_sort.md b/en/docs/chapter_sorting/bubble_sort.md index c015d1673..75f2b557e 100644 --- a/en/docs/chapter_sorting/bubble_sort.md +++ b/en/docs/chapter_sorting/bubble_sort.md @@ -2,14 +2,14 @@ comments: true --- -# 11.3   Bubble sort +# 11.3   Bubble Sort -Bubble sort works by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name "bubble sort." +Bubble sort (bubble sort) achieves sorting by continuously comparing and swapping adjacent elements. This process is like bubbles rising from the bottom to the top, hence the name bubble sort. -As shown in Figure 11-4, the bubbling process can be simulated using element swaps: start from the leftmost end of the array and move right, comparing each pair of adjacent elements. If the left element is greater than the right element, swap them. After the traversal, the largest element will have bubbled up to the rightmost end of the array. +As shown in Figure 11-4, the bubbling process can be simulated using element swap operations: starting from the leftmost end of the array and traversing to the right, compare the size of adjacent elements, and if "left element > right element", swap them. After completing the traversal, the largest element will be moved to the rightmost end of the array. === "<1>" - ![Simulating bubble process using element swap](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } + ![Simulating bubble using element swap operation](bubble_sort.assets/bubble_operation_step1.png){ class="animation-figure" } === "<2>" ![bubble_operation_step2](bubble_sort.assets/bubble_operation_step2.png){ class="animation-figure" } @@ -29,20 +29,20 @@ As shown in Figure 11-4, the bubbling process can be simulated using element swa === "<7>" ![bubble_operation_step7](bubble_sort.assets/bubble_operation_step7.png){ class="animation-figure" } -

Figure 11-4   Simulating bubble process using element swap

+

Figure 11-4   Simulating bubble using element swap operation

-## 11.3.1   Algorithm process +## 11.3.1   Algorithm Flow -Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5: +Assume the array has length $n$. The steps of bubble sort are shown in Figure 11-5. -1. First, perform one "bubble" pass on $n$ elements, **swapping the largest element to its correct position**. -2. Next, perform a "bubble" pass on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**. -3. Continue in this manner; after $n - 1$ such passes, **the largest $n - 1$ elements will have been moved to their correct positions**. -4. The only remaining element **must** be the smallest, so **no** further sorting is required. At this point, the array is sorted. +1. First, perform "bubbling" on $n$ elements, **swapping the largest element of the array to its correct position**. +2. Next, perform "bubbling" on the remaining $n - 1$ elements, **swapping the second largest element to its correct position**. +3. And so on. After $n - 1$ rounds of "bubbling", **the largest $n - 1$ elements have all been swapped to their correct positions**. +4. The only remaining element must be the smallest element, requiring no sorting, so the array sorting is complete. -![Bubble sort process](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" } +![Bubble sort flow](bubble_sort.assets/bubble_sort_overview.png){ class="animation-figure" } -

Figure 11-5   Bubble sort process

+

Figure 11-5   Bubble sort flow

Example code is as follows: @@ -52,9 +52,9 @@ Example code is as follows: def bubble_sort(nums: list[int]): """Bubble sort""" n = len(nums) - # Outer loop: unsorted range is [0, i] + # Outer loop: unsorted interval is [0, i] for i in range(n - 1, 0, -1): - # Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval for j in range(i): if nums[j] > nums[j + 1]: # Swap nums[j] and nums[j + 1] @@ -68,11 +68,11 @@ Example code is as follows: void bubbleSort(vector &nums) { // Outer loop: unsorted range is [0, i] for (int i = nums.size() - 1; i > 0; i--) { - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] - // Here, the std + // Using std::swap() function here swap(nums[j], nums[j + 1]); } } @@ -87,7 +87,7 @@ Example code is as follows: void bubbleSort(int[] nums) { // Outer loop: unsorted range is [0, i] for (int i = nums.length - 1; i > 0; i--) { - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] @@ -103,113 +103,237 @@ Example code is as follows: === "C#" ```csharp title="bubble_sort.cs" - [class]{bubble_sort}-[func]{BubbleSort} + /* Bubble sort */ + void BubbleSort(int[] nums) { + // Outer loop: unsorted range is [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + } + } + } + } ``` === "Go" ```go title="bubble_sort.go" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + func bubbleSort(nums []int) { + // Outer loop: unsorted range is [0, i] + for i := len(nums) - 1; i > 0; i-- { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // Swap nums[j] and nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + } + } + } + } ``` === "Swift" ```swift title="bubble_sort.swift" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + func bubbleSort(nums: inout [Int]) { + // Outer loop: unsorted range is [0, i] + for i in nums.indices.dropFirst().reversed() { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + nums.swapAt(j, j + 1) + } + } + } + } ``` === "JS" ```javascript title="bubble_sort.js" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + function bubbleSort(nums) { + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "TS" ```typescript title="bubble_sort.ts" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + function bubbleSort(nums: number[]): void { + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "Dart" ```dart title="bubble_sort.dart" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + void bubbleSort(List nums) { + // Outer loop: unsorted range is [0, i] + for (int i = nums.length - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + } + } + } + } ``` === "Rust" ```rust title="bubble_sort.rs" - [class]{}-[func]{bubble_sort} + /* Bubble sort */ + fn bubble_sort(nums: &mut [i32]) { + // Outer loop: unsorted range is [0, i] + for i in (1..nums.len()).rev() { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0..i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + nums.swap(j, j + 1); + } + } + } + } ``` === "C" ```c title="bubble_sort.c" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + void bubbleSort(int nums[], int size) { + // Outer loop: unsorted range is [0, i] + for (int i = size - 1; i > 0; i--) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + } + } + } + } ``` === "Kotlin" ```kotlin title="bubble_sort.kt" - [class]{}-[func]{bubbleSort} + /* Bubble sort */ + fun bubbleSort(nums: IntArray) { + // Outer loop: unsorted range is [0, i] + for (i in nums.size - 1 downTo 1) { + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (j in 0.. nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + } + } + } + } ``` === "Ruby" ```ruby title="bubble_sort.rb" - [class]{}-[func]{bubble_sort} + ### Bubble sort ### + def bubble_sort(nums) + n = nums.length + # Outer loop: unsorted range is [0, i] + for i in (n - 1).downto(1) + # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0...i + if nums[j] > nums[j + 1] + # Swap nums[j] and nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + end + end + end + end ``` -=== "Zig" +## 11.3.2   Efficiency Optimization - ```zig title="bubble_sort.zig" - [class]{}-[func]{bubbleSort} - ``` +We notice that if no swap operations are performed during a certain round of "bubbling", it means the array has already completed sorting and can directly return the result. Therefore, we can add a flag `flag` to monitor this situation and return immediately once it occurs. -## 11.3.2   Efficiency optimization - -If no swaps occur during a round of "bubbling," the array is already sorted, so we can return immediately. To detect this, we can add a `flag` variable; whenever no swaps are made in a pass, we set the flag and return early. - -Even with this optimization, the worst time complexity and average time complexity of bubble sort remains $O(n^2)$. However, if the input array is already sorted, the best-case time complexity can be as low as $O(n)$. +After optimization, the worst-case time complexity and average time complexity of bubble sort remain $O(n^2)$; but when the input array is completely ordered, the best-case time complexity can reach $O(n)$. === "Python" ```python title="bubble_sort.py" def bubble_sort_with_flag(nums: list[int]): - """Bubble sort (optimized with flag)""" + """Bubble sort (flag optimization)""" n = len(nums) - # Outer loop: unsorted range is [0, i] + # Outer loop: unsorted interval is [0, i] for i in range(n - 1, 0, -1): flag = False # Initialize flag - # Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + # Inner loop: swap the largest element in the unsorted interval [0, i] to the rightmost end of the interval for j in range(i): if nums[j] > nums[j + 1]: # Swap nums[j] and nums[j + 1] nums[j], nums[j + 1] = nums[j + 1], nums[j] - flag = True # Record swapped elements + flag = True # Record element swap if not flag: - break # If no elements were swapped in this round of "bubbling", exit + break # No elements were swapped in this round of "bubbling", exit directly ``` === "C++" ```cpp title="bubble_sort.cpp" - /* Bubble sort (optimized with flag)*/ + /* Bubble sort (flag optimization)*/ void bubbleSortWithFlag(vector &nums) { // Outer loop: unsorted range is [0, i] for (int i = nums.size() - 1; i > 0; i--) { bool flag = false; // Initialize flag - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] - // Here, the std + // Using std::swap() function here swap(nums[j], nums[j + 1]); - flag = true; // Record swapped elements + flag = true; // Record element swap } } if (!flag) - break; // If no elements were swapped in this round of "bubbling", exit + break; // No elements were swapped in this round of "bubbling", exit directly } } ``` @@ -217,23 +341,23 @@ Even with this optimization, the worst time complexity and average time complexi === "Java" ```java title="bubble_sort.java" - /* Bubble sort (optimized with flag) */ + /* Bubble sort (flag optimization) */ void bubbleSortWithFlag(int[] nums) { // Outer loop: unsorted range is [0, i] for (int i = nums.length - 1; i > 0; i--) { boolean flag = false; // Initialize flag - // Inner loop: swap the largest element in the unsorted range [0, i] to the right end of the range + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range for (int j = 0; j < i; j++) { if (nums[j] > nums[j + 1]) { // Swap nums[j] and nums[j + 1] int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; - flag = true; // Record swapped elements + flag = true; // Record element swap } } if (!flag) - break; // If no elements were swapped in this round of "bubbling", exit + break; // No elements were swapped in this round of "bubbling", exit directly } } ``` @@ -241,71 +365,233 @@ Even with this optimization, the worst time complexity and average time complexi === "C#" ```csharp title="bubble_sort.cs" - [class]{bubble_sort}-[func]{BubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + void BubbleSortWithFlag(int[] nums) { + // Outer loop: unsorted range is [0, i] + for (int i = nums.Length - 1; i > 0; i--) { + bool flag = false; // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + (nums[j + 1], nums[j]) = (nums[j], nums[j + 1]); + flag = true; // Record element swap + } + } + if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly + } + } ``` === "Go" ```go title="bubble_sort.go" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + func bubbleSortWithFlag(nums []int) { + // Outer loop: unsorted range is [0, i] + for i := len(nums) - 1; i > 0; i-- { + flag := false // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j := 0; j < i; j++ { + if nums[j] > nums[j+1] { + // Swap nums[j] and nums[j + 1] + nums[j], nums[j+1] = nums[j+1], nums[j] + flag = true // Record element swap + } + } + if flag == false { // No elements were swapped in this round of "bubbling", exit directly + break + } + } + } ``` === "Swift" ```swift title="bubble_sort.swift" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + func bubbleSortWithFlag(nums: inout [Int]) { + // Outer loop: unsorted range is [0, i] + for i in nums.indices.dropFirst().reversed() { + var flag = false // Initialize flag + for j in 0 ..< i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + nums.swapAt(j, j + 1) + flag = true // Record element swap + } + } + if !flag { // No elements were swapped in this round of "bubbling", exit directly + break + } + } + } ``` === "JS" ```javascript title="bubble_sort.js" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + function bubbleSortWithFlag(nums) { + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // Record element swap + } + } + if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly + } + } ``` === "TS" ```typescript title="bubble_sort.ts" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + function bubbleSortWithFlag(nums: number[]): void { + // Outer loop: unsorted range is [0, i] + for (let i = nums.length - 1; i > 0; i--) { + let flag = false; // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (let j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + let tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // Record element swap + } + } + if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly + } + } ``` === "Dart" ```dart title="bubble_sort.dart" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + void bubbleSortWithFlag(List nums) { + // Outer loop: unsorted range is [0, i] + for (int i = nums.length - 1; i > 0; i--) { + bool flag = false; // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + int tmp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = tmp; + flag = true; // Record element swap + } + } + if (!flag) break; // No elements were swapped in this round of "bubbling", exit directly + } + } ``` === "Rust" ```rust title="bubble_sort.rs" - [class]{}-[func]{bubble_sort_with_flag} + /* Bubble sort (flag optimization) */ + fn bubble_sort_with_flag(nums: &mut [i32]) { + // Outer loop: unsorted range is [0, i] + for i in (1..nums.len()).rev() { + let mut flag = false; // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0..i { + if nums[j] > nums[j + 1] { + // Swap nums[j] and nums[j + 1] + nums.swap(j, j + 1); + flag = true; // Record element swap + } + } + if !flag { + break; // No elements were swapped in this round of "bubbling", exit directly + }; + } + } ``` === "C" ```c title="bubble_sort.c" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + void bubbleSortWithFlag(int nums[], int size) { + // Outer loop: unsorted range is [0, i] + for (int i = size - 1; i > 0; i--) { + bool flag = false; + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (int j = 0; j < i; j++) { + if (nums[j] > nums[j + 1]) { + int temp = nums[j]; + nums[j] = nums[j + 1]; + nums[j + 1] = temp; + flag = true; + } + } + if (!flag) + break; + } + } ``` === "Kotlin" ```kotlin title="bubble_sort.kt" - [class]{}-[func]{bubbleSortWithFlag} + /* Bubble sort (flag optimization) */ + fun bubbleSortWithFlag(nums: IntArray) { + // Outer loop: unsorted range is [0, i] + for (i in nums.size - 1 downTo 1) { + var flag = false // Initialize flag + // Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for (j in 0.. nums[j + 1]) { + // Swap nums[j] and nums[j + 1] + val temp = nums[j] + nums[j] = nums[j + 1] + nums[j + 1] = temp + flag = true // Record element swap + } + } + if (!flag) break // No elements were swapped in this round of "bubbling", exit directly + } + } ``` === "Ruby" ```ruby title="bubble_sort.rb" - [class]{}-[func]{bubble_sort_with_flag} + ### Bubble sort (flag optimization) ### + def bubble_sort_with_flag(nums) + n = nums.length + # Outer loop: unsorted range is [0, i] + for i in (n - 1).downto(1) + flag = false # Initialize flag + + # Inner loop: swap the largest element in the unsorted range [0, i] to the rightmost end of that range + for j in 0...i + if nums[j] > nums[j + 1] + # Swap nums[j] and nums[j + 1] + nums[j], nums[j + 1] = nums[j + 1], nums[j] + flag = true # Record element swap + end + end + + break unless flag # No elements were swapped in this round of "bubbling", exit directly + end + end ``` -=== "Zig" +## 11.3.3   Algorithm Characteristics - ```zig title="bubble_sort.zig" - [class]{}-[func]{bubbleSortWithFlag} - ``` - -## 11.3.3   Algorithm characteristics - -- **Time complexity of $O(n^2)$, adaptive sorting.** Each round of "bubbling" traverses array segments of length $n - 1$, $n - 2$, $\dots$, $2$, $1$, which sums to $(n - 1) n / 2$. With a `flag` optimization, the best-case time complexity can reach $O(n)$ when the array is already sorted. -- **Space complexity of $O(1)$, in-place sorting.** Only a constant amount of extra space is used by pointers $i$ and $j$. -- **Stable sorting.** Because equal elements are not swapped during "bubbling," their original order is preserved, making this a stable sort. +- **Time complexity of $O(n^2)$, adaptive sorting**: The array lengths traversed in each round of "bubbling" are $n - 1$, $n - 2$, $\dots$, $2$, $1$, totaling $(n - 1) n / 2$. After introducing the `flag` optimization, the best-case time complexity can reach $O(n)$. +- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. +- **Stable sorting**: Since equal elements are not swapped during "bubbling". diff --git a/en/docs/chapter_sorting/bucket_sort.md b/en/docs/chapter_sorting/bucket_sort.md index 9d858a281..fc287bc34 100644 --- a/en/docs/chapter_sorting/bucket_sort.md +++ b/en/docs/chapter_sorting/bucket_sort.md @@ -2,25 +2,25 @@ comments: true --- -# 11.8   Bucket sort +# 11.8   Bucket Sort -The previously mentioned sorting algorithms are all "comparison-based sorting algorithms," which sort elements by comparing their values. Such sorting algorithms cannot have better time complexity of $O(n \log n)$. Next, we will discuss several "non-comparison sorting algorithms" that could achieve linear time complexity. +The several sorting algorithms mentioned earlier all belong to "comparison-based sorting algorithms", which achieve sorting by comparing the size of elements. The time complexity of such sorting algorithms cannot exceed $O(n \log n)$. Next, we will explore several "non-comparison sorting algorithms", whose time complexity can reach linear order. -Bucket sort is a typical application of the divide-and-conquer strategy. It works by setting up a series of ordered buckets, each containing a range of data, and distributing the input data evenly across these buckets. And then, the data in each bucket is sorted individually. Finally, the sorted data from all the buckets is merged in sequence to produce the final result. +Bucket sort (bucket sort) is a typical application of the divide-and-conquer strategy. It works by setting up buckets with size order, each bucket corresponding to a data range, evenly distributing data to each bucket; then, sorting within each bucket separately; finally, merging all data in the order of the buckets. -## 11.8.1   Algorithm process +## 11.8.1   Algorithm Flow -Consider an array of length $n$, with float numbers in the range $[0, 1)$. The bucket sort process is illustrated in Figure 11-13. +Consider an array of length $n$, whose elements are floating-point numbers in the range $[0, 1)$. The flow of bucket sort is shown in Figure 11-13. -1. Initialize $k$ buckets and distribute $n$ elements into these $k$ buckets. -2. Sort each bucket individually (using the built-in sorting function of the programming language). -3. Merge the results in the order from the smallest to the largest bucket. +1. Initialize $k$ buckets and distribute the $n$ elements into the $k$ buckets. +2. Sort each bucket separately (here we use the built-in sorting function of the programming language). +3. Merge the results in order from smallest to largest bucket. -![Bucket sort algorithm process](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" } +![Bucket sort algorithm flow](bucket_sort.assets/bucket_sort_overview.png){ class="animation-figure" } -

Figure 11-13   Bucket sort algorithm process

+

Figure 11-13   Bucket sort algorithm flow

-The code is shown as follows: +The code is as follows: === "Python" @@ -60,7 +60,7 @@ The code is shown as follows: for (float num : nums) { // Input data range is [0, 1), use num * k to map to index range [0, k-1] int i = num * k; - // Add number to bucket_idx + // Add num to bucket bucket_idx buckets[i].push_back(num); } // 2. Sort each bucket @@ -114,92 +114,352 @@ The code is shown as follows: === "C#" ```csharp title="bucket_sort.cs" - [class]{bucket_sort}-[func]{BucketSort} + /* Bucket sort */ + void BucketSort(float[] nums) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + int k = nums.Length / 2; + List> buckets = []; + for (int i = 0; i < k; i++) { + buckets.Add([]); + } + // 1. Distribute array elements into various buckets + foreach (float num in nums) { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + int i = (int)(num * k); + // Add num to bucket i + buckets[i].Add(num); + } + // 2. Sort each bucket + foreach (List bucket in buckets) { + // Use built-in sorting function, can also replace with other sorting algorithms + bucket.Sort(); + } + // 3. Traverse buckets to merge results + int j = 0; + foreach (List bucket in buckets) { + foreach (float num in bucket) { + nums[j++] = num; + } + } + } ``` === "Go" ```go title="bucket_sort.go" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + func bucketSort(nums []float64) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + k := len(nums) / 2 + buckets := make([][]float64, k) + for i := 0; i < k; i++ { + buckets[i] = make([]float64, 0) + } + // 1. Distribute array elements into various buckets + for _, num := range nums { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + i := int(num * float64(k)) + // Add num to bucket i + buckets[i] = append(buckets[i], num) + } + // 2. Sort each bucket + for i := 0; i < k; i++ { + // Use built-in slice sorting function, can also be replaced with other sorting algorithms + sort.Float64s(buckets[i]) + } + // 3. Traverse buckets to merge results + i := 0 + for _, bucket := range buckets { + for _, num := range bucket { + nums[i] = num + i++ + } + } + } ``` === "Swift" ```swift title="bucket_sort.swift" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + func bucketSort(nums: inout [Double]) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + let k = nums.count / 2 + var buckets = (0 ..< k).map { _ in [Double]() } + // 1. Distribute array elements into various buckets + for num in nums { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + let i = Int(num * Double(k)) + // Add num to bucket i + buckets[i].append(num) + } + // 2. Sort each bucket + for i in buckets.indices { + // Use built-in sorting function, can also replace with other sorting algorithms + buckets[i].sort() + } + // 3. Traverse buckets to merge results + var i = nums.startIndex + for bucket in buckets { + for num in bucket { + nums[i] = num + i += 1 + } + } + } ``` === "JS" ```javascript title="bucket_sort.js" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + function bucketSort(nums) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + const k = nums.length / 2; + const buckets = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. Distribute array elements into various buckets + for (const num of nums) { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + const i = Math.floor(num * k); + // Add num to bucket i + buckets[i].push(num); + } + // 2. Sort each bucket + for (const bucket of buckets) { + // Use built-in sorting function, can also replace with other sorting algorithms + bucket.sort((a, b) => a - b); + } + // 3. Traverse buckets to merge results + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "TS" ```typescript title="bucket_sort.ts" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + function bucketSort(nums: number[]): void { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + const k = nums.length / 2; + const buckets: number[][] = []; + for (let i = 0; i < k; i++) { + buckets.push([]); + } + // 1. Distribute array elements into various buckets + for (const num of nums) { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + const i = Math.floor(num * k); + // Add num to bucket i + buckets[i].push(num); + } + // 2. Sort each bucket + for (const bucket of buckets) { + // Use built-in sorting function, can also replace with other sorting algorithms + bucket.sort((a, b) => a - b); + } + // 3. Traverse buckets to merge results + let i = 0; + for (const bucket of buckets) { + for (const num of bucket) { + nums[i++] = num; + } + } + } ``` === "Dart" ```dart title="bucket_sort.dart" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + void bucketSort(List nums) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + int k = nums.length ~/ 2; + List> buckets = List.generate(k, (index) => []); + + // 1. Distribute array elements into various buckets + for (double _num in nums) { + // Input data range is [0, 1), use _num * k to map to index range [0, k-1] + int i = (_num * k).toInt(); + // Add _num to bucket bucket_idx + buckets[i].add(_num); + } + // 2. Sort each bucket + for (List bucket in buckets) { + bucket.sort(); + } + // 3. Traverse buckets to merge results + int i = 0; + for (List bucket in buckets) { + for (double _num in bucket) { + nums[i++] = _num; + } + } + } ``` === "Rust" ```rust title="bucket_sort.rs" - [class]{}-[func]{bucket_sort} + /* Bucket sort */ + fn bucket_sort(nums: &mut [f64]) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + let k = nums.len() / 2; + let mut buckets = vec![vec![]; k]; + // 1. Distribute array elements into various buckets + for &num in nums.iter() { + // Input data range is [0, 1), use num * k to map to index range [0, k-1] + let i = (num * k as f64) as usize; + // Add num to bucket i + buckets[i].push(num); + } + // 2. Sort each bucket + for bucket in &mut buckets { + // Use built-in sorting function, can also replace with other sorting algorithms + bucket.sort_by(|a, b| a.partial_cmp(b).unwrap()); + } + // 3. Traverse buckets to merge results + let mut i = 0; + for bucket in buckets.iter() { + for &num in bucket.iter() { + nums[i] = num; + i += 1; + } + } + } ``` === "C" ```c title="bucket_sort.c" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + void bucketSort(float nums[], int n) { + int k = n / 2; // Initialize k = n/2 buckets + int *sizes = malloc(k * sizeof(int)); // Record each bucket's size + float **buckets = malloc(k * sizeof(float *)); // Array of dynamic arrays (buckets) + // Pre-allocate sufficient space for each bucket + for (int i = 0; i < k; ++i) { + buckets[i] = (float *)malloc(n * sizeof(float)); + sizes[i] = 0; + } + // 1. Distribute array elements into various buckets + for (int i = 0; i < n; ++i) { + int idx = (int)(nums[i] * k); + buckets[idx][sizes[idx]++] = nums[i]; + } + // 2. Sort each bucket + for (int i = 0; i < k; ++i) { + qsort(buckets[i], sizes[i], sizeof(float), compare); + } + // 3. Merge sorted buckets + int idx = 0; + for (int i = 0; i < k; ++i) { + for (int j = 0; j < sizes[i]; ++j) { + nums[idx++] = buckets[i][j]; + } + // Free memory + free(buckets[i]); + } + } ``` === "Kotlin" ```kotlin title="bucket_sort.kt" - [class]{}-[func]{bucketSort} + /* Bucket sort */ + fun bucketSort(nums: FloatArray) { + // Initialize k = n/2 buckets, expected to allocate 2 elements per bucket + val k = nums.size / 2 + val buckets = mutableListOf>() + for (i in 0.. Figure 11-14   Recursively dividing buckets

-![Recursive division of buckets](bucket_sort.assets/scatter_in_buckets_recursively.png){ class="animation-figure" } +If we know the probability distribution of product prices in advance, **we can set the price dividing line for each bucket based on the data probability distribution**. It is worth noting that the data distribution does not necessarily need to be specifically calculated, but can also be approximated using a certain probability model based on data characteristics. -

Figure 11-14   Recursive division of buckets

- -If we know the probability distribution of product prices in advance, **we can set the price boundaries for each bucket based on the data probability distribution**. It is worth noting that it is not necessarily required to specifically calculate the data distribution; instead, it can be approximated based on data characteristics using a probability model. - -As shown in Figure 11-15, assuming that product prices follow a normal distribution, we can define reasonable price intervals to balance the distribution of items across the buckets. +As shown in Figure 11-15, we assume that product prices follow a normal distribution, which allows us to reasonably set price intervals to evenly distribute products to each bucket. ![Dividing buckets based on probability distribution](bucket_sort.assets/scatter_in_buckets_distribution.png){ class="animation-figure" } diff --git a/en/docs/chapter_sorting/counting_sort.md b/en/docs/chapter_sorting/counting_sort.md index 81b07a70d..8a7da9a8f 100644 --- a/en/docs/chapter_sorting/counting_sort.md +++ b/en/docs/chapter_sorting/counting_sort.md @@ -2,23 +2,23 @@ comments: true --- -# 11.9   Counting sort +# 11.9   Counting Sort -Counting sort achieves sorting by counting the number of elements, usually applied to integer arrays. +Counting sort (counting sort) achieves sorting by counting the number of elements, typically applied to integer arrays. -## 11.9.1   Simple implementation +## 11.9.1   Simple Implementation -Let's start with a simple example. Given an array `nums` of length $n$, where all elements are "non-negative integers", the overall process of counting sort is shown in Figure 11-16. +Let's start with a simple example. Given an array `nums` of length $n$, where the elements are all "non-negative integers", the overall flow of counting sort is shown in Figure 11-16. -1. Traverse the array to find the maximum number, denoted as $m$, then create an auxiliary array `counter` of length $m + 1$. -2. **Use `counter` to count the occurrence of each number in `nums`**, where `counter[num]` corresponds to the occurrence of the number `num`. The counting method is simple, just traverse `nums` (suppose the current number is `num`), and increase `counter[num]` by $1$ each round. -3. **Since the indices of `counter` are naturally ordered, all numbers are essentially sorted already**. Next, we traverse `counter`, and fill in `nums` in ascending order of occurrence. +1. Traverse the array to find the largest number, denoted as $m$, and then create an auxiliary array `counter` of length $m + 1$. +2. **Use `counter` to count the number of occurrences of each number in `nums`**, where `counter[num]` corresponds to the number of occurrences of the number `num`. The counting method is simple: just traverse `nums` (let the current number be `num`), and increase `counter[num]` by $1$ in each round. +3. **Since each index of `counter` is naturally ordered, this is equivalent to all numbers being sorted**. Next, we traverse `counter` and fill in `nums` in ascending order based on the number of occurrences of each number. -![Counting sort process](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } +![Counting sort flow](counting_sort.assets/counting_sort_overview.png){ class="animation-figure" } -

Figure 11-16   Counting sort process

+

Figure 11-16   Counting sort flow

-The code is shown below: +The code is as follows: === "Python" @@ -30,7 +30,7 @@ The code is shown below: m = 0 for num in nums: m = max(m, num) - # 2. Count the occurrence of each digit + # 2. Count the occurrence of each number # counter[num] represents the occurrence of num counter = [0] * (m + 1) for num in nums: @@ -54,7 +54,7 @@ The code is shown below: for (int num : nums) { m = max(m, num); } - // 2. Count the occurrence of each digit + // 2. Count the occurrence of each number // counter[num] represents the occurrence of num vector counter(m + 1, 0); for (int num : nums) { @@ -81,7 +81,7 @@ The code is shown below: for (int num : nums) { m = Math.max(m, num); } - // 2. Count the occurrence of each digit + // 2. Count the occurrence of each number // counter[num] represents the occurrence of num int[] counter = new int[m + 1]; for (int num : nums) { @@ -100,92 +100,292 @@ The code is shown below: === "C#" ```csharp title="counting_sort.cs" - [class]{counting_sort}-[func]{CountingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + void CountingSortNaive(int[] nums) { + // 1. Count the maximum element m in the array + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + func countingSortNaive(nums []int) { + // 1. Count the maximum element m in the array + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + func countingSortNaive(nums: inout [Int]) { + // 1. Count the maximum element m in the array + let m = nums.max()! + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. Traverse counter, filling each element back into the original array nums + var i = 0 + for num in 0 ..< m + 1 { + for _ in 0 ..< counter[num] { + nums[i] = num + i += 1 + } + } + } ``` === "JS" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + function countingSortNaive(nums) { + // 1. Count the maximum element m in the array + let m = Math.max(...nums); + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + function countingSortNaive(nums: number[]): void { + // 1. Count the maximum element m in the array + let m: number = Math.max(...nums); + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + void countingSortNaive(List nums) { + // 1. Count the maximum element m in the array + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. Count the occurrence of each number + // counter[_num] represents occurrence count of _num + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{counting_sort_naive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + fn counting_sort_naive(nums: &mut [i32]) { + // 1. Count the maximum element m in the array + let m = *nums.iter().max().unwrap(); + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + let mut counter = vec![0; m as usize + 1]; + for &num in nums.iter() { + counter[num as usize] += 1; + } + // 3. Traverse counter, filling each element back into the original array 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" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + void countingSortNaive(int nums[], int size) { + // 1. Count the maximum element m in the array + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + int *counter = calloc(m + 1, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. Traverse counter, filling each element back into the original array nums + int i = 0; + for (int num = 0; num < m + 1; num++) { + for (int j = 0; j < counter[num]; j++, i++) { + nums[i] = num; + } + } + // 4. Free memory + free(counter); + } ``` === "Kotlin" ```kotlin title="counting_sort.kt" - [class]{}-[func]{countingSortNaive} + /* Counting sort */ + // Simple implementation, cannot be used for sorting objects + fun countingSortNaive(nums: IntArray) { + // 1. Count the maximum element m in the array + var m = 0 + for (num in nums) { + m = max(m, num) + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. Traverse counter, filling each element back into the original array nums + var i = 0 + for (num in 0.." - ![Counting sort process](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" } + ![Counting sort steps](counting_sort.assets/counting_sort_step1.png){ class="animation-figure" } === "<2>" ![counting_sort_step2](counting_sort.assets/counting_sort_step2.png){ class="animation-figure" } @@ -208,9 +408,9 @@ After the traversal, the array `res` contains the sorted result, and finally, `r === "<8>" ![counting_sort_step8](counting_sort.assets/counting_sort_step8.png){ class="animation-figure" } -

Figure 11-17   Counting sort process

+

Figure 11-17   Counting sort steps

-The implementation code of counting sort is shown below: +The implementation code of counting sort is as follows: === "Python" @@ -220,7 +420,7 @@ The implementation code of counting sort is shown below: # Complete implementation, can sort objects and is a stable sort # 1. Count the maximum element m in the array m = max(nums) - # 2. Count the occurrence of each digit + # 2. Count the occurrence of each number # counter[num] represents the occurrence of num counter = [0] * (m + 1) for num in nums: @@ -253,7 +453,7 @@ The implementation code of counting sort is shown below: for (int num : nums) { m = max(m, num); } - // 2. Count the occurrence of each digit + // 2. Count the occurrence of each number // counter[num] represents the occurrence of num vector counter(m + 1, 0); for (int num : nums) { @@ -289,7 +489,7 @@ The implementation code of counting sort is shown below: for (int num : nums) { m = Math.max(m, num); } - // 2. Count the occurrence of each digit + // 2. Count the occurrence of each number // counter[num] represents the occurrence of num int[] counter = new int[m + 1]; for (int num : nums) { @@ -319,79 +519,371 @@ The implementation code of counting sort is shown below: === "C#" ```csharp title="counting_sort.cs" - [class]{counting_sort}-[func]{CountingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + void CountingSort(int[] nums) { + // 1. Count the maximum element m in the array + int m = 0; + foreach (int num in nums) { + m = Math.Max(m, num); + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + int[] counter = new int[m + 1]; + foreach (int num in nums) { + counter[num]++; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + 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; // Place num at the corresponding index + counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "Go" ```go title="counting_sort.go" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + func countingSort(nums []int) { + // 1. Count the maximum element m in the array + m := 0 + for _, num := range nums { + if num > m { + m = num + } + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + counter := make([]int, m+1) + for _, num := range nums { + counter[num]++ + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for i := 0; i < m; i++ { + counter[i+1] += counter[i] + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + n := len(nums) + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + num := nums[i] + // Place num at the corresponding index + res[counter[num]-1] = num + // Decrement the prefix sum by 1, getting the next index to place num + counter[num]-- + } + // Use result array res to overwrite the original array nums + copy(nums, res) + } ``` === "Swift" ```swift title="counting_sort.swift" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + func countingSort(nums: inout [Int]) { + // 1. Count the maximum element m in the array + let m = nums.max()! + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + var counter = Array(repeating: 0, count: m + 1) + for num in nums { + counter[num] += 1 + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for i in 0 ..< m { + counter[i + 1] += counter[i] + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let num = nums[i] + res[counter[num] - 1] = num // Place num at the corresponding index + counter[num] -= 1 // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + for i in nums.indices { + nums[i] = res[i] + } + } ``` === "JS" ```javascript title="counting_sort.js" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + function countingSort(nums) { + // 1. Count the maximum element m in the array + let m = Math.max(...nums); + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + const counter = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + 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; // Place num at the corresponding index + counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "TS" ```typescript title="counting_sort.ts" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + function countingSort(nums: number[]): void { + // 1. Count the maximum element m in the array + let m: number = Math.max(...nums); + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + const counter: number[] = new Array(m + 1).fill(0); + for (const num of nums) { + counter[num]++; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for (let i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + 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; // Place num at the corresponding index + counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } ``` === "Dart" ```dart title="counting_sort.dart" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + void countingSort(List nums) { + // 1. Count the maximum element m in the array + int m = 0; + for (int _num in nums) { + m = max(m, _num); + } + // 2. Count the occurrence of each number + // counter[_num] represents occurrence count of _num + List counter = List.filled(m + 1, 0); + for (int _num in nums) { + counter[_num]++; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // That is, counter[_num]-1 is the last occurrence index of _num in res + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + 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; // Place _num at corresponding index + counter[_num]--; // Decrement prefix sum by 1 to get next placement index for _num + } + // Use result array res to overwrite the original array nums + nums.setAll(0, res); + } ``` === "Rust" ```rust title="counting_sort.rs" - [class]{}-[func]{counting_sort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + fn counting_sort(nums: &mut [i32]) { + // 1. Count the maximum element m in the array + let m = *nums.iter().max().unwrap() as usize; + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + let mut counter = vec![0; m + 1]; + for &num in nums.iter() { + counter[num as usize] += 1; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for i in 0..m { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + 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; // Place num at the corresponding index + counter[num as usize] -= 1; // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + nums.copy_from_slice(&res) + } ``` === "C" ```c title="counting_sort.c" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + void countingSort(int nums[], int size) { + // 1. Count the maximum element m in the array + int m = 0; + for (int i = 0; i < size; i++) { + if (nums[i] > m) { + m = nums[i]; + } + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + int *counter = calloc(m, sizeof(int)); + for (int i = 0; i < size; i++) { + counter[nums[i]]++; + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for (int i = 0; i < m; i++) { + counter[i + 1] += counter[i]; + } + // 4. Traverse nums in reverse order, placing each element into the result array res + // Initialize the array res to record results + int *res = malloc(sizeof(int) * size); + for (int i = size - 1; i >= 0; i--) { + int num = nums[i]; + res[counter[num] - 1] = num; // Place num at the corresponding index + counter[num]--; // Decrement the prefix sum by 1, getting the next index to place num + } + // Use result array res to overwrite the original array nums + memcpy(nums, res, size * sizeof(int)); + // 5. Free memory + free(res); + free(counter); + } ``` === "Kotlin" ```kotlin title="counting_sort.kt" - [class]{}-[func]{countingSort} + /* Counting sort */ + // Complete implementation, can sort objects and is a stable sort + fun countingSort(nums: IntArray) { + // 1. Count the maximum element m in the array + var m = 0 + for (num in nums) { + m = max(m, num) + } + // 2. Count the occurrence of each number + // counter[num] represents the occurrence of num + val counter = IntArray(m + 1) + for (num in nums) { + counter[num]++ + } + // 3. Calculate the prefix sum of counter, converting "occurrence count" to "tail index" + // counter[num]-1 is the last index where num appears in res + for (i in 0..Heap sort is an efficient sorting algorithm based on the heap data structure. We can implement heap sort using the "heap creation" and "element extraction" operations we have already learned. +Heap sort (heap sort) is an efficient sorting algorithm based on the heap data structure. We can use the "build heap operation" and "element out-heap operation" that we have already learned to implement heap sort. -1. Input the array and construct a min-heap, where the smallest element is at the top of the heap. -2. Continuously perform the extraction operation, record the extracted elements sequentially to obtain a sorted list from smallest to largest. +1. Input the array and build a min-heap, at which point the smallest element is at the heap top. +2. Continuously perform the out-heap operation, record the out-heap elements in sequence, and an ascending sorted sequence can be obtained. -Although the above method is feasible, it requires an additional array to store the popped elements, which is somewhat space-consuming. In practice, we usually use a more elegant implementation. +Although the above method is feasible, it requires an additional array to save the popped elements, which is quite wasteful of space. In practice, we usually use a more elegant implementation method. -## 11.7.1   Algorithm flow +## 11.7.1   Algorithm Flow -Suppose the array length is $n$, the heap sort process is as follows. +Assume the array length is $n$. The flow of heap sort is shown in Figure 11-12. -1. Input the array and establish a max-heap. After this step, the largest element is positioned at the top of the heap. -2. Swap the top element of the heap (the first element) with the heap's bottom element (the last element). Following this swap, reduce the heap's length by $1$ and increase the sorted elements count by $1$. -3. Starting from the heap top, perform the sift-down operation from top to bottom. After the sift-down, the heap's property is restored. -4. Repeat steps `2.` and `3.` Loop for $n - 1$ rounds to complete the sorting of the array. +1. Input the array and build a max-heap. After completion, the largest element is at the heap top. +2. Swap the heap top element (first element) with the heap bottom element (last element). After the swap is complete, reduce the heap length by $1$ and increase the count of sorted elements by $1$. +3. Starting from the heap top element, perform top-to-bottom heapify operation (sift down). After heapify is complete, the heap property is restored. +4. Loop through steps `2.` and `3.` After looping $n - 1$ rounds, the array sorting can be completed. !!! tip - In fact, the element extraction operation also includes steps `2.` and `3.`, with an additional step to pop (remove) the extracted element from the heap. + In fact, the element out-heap operation also includes steps `2.` and `3.`, with just an additional step to pop the element. === "<1>" - ![Heap sort process](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } + ![Heap sort steps](heap_sort.assets/heap_sort_step1.png){ class="animation-figure" } === "<2>" ![heap_sort_step2](heap_sort.assets/heap_sort_step2.png){ class="animation-figure" } @@ -64,9 +64,9 @@ Suppose the array length is $n$, the heap sort process is as follows. === "<12>" ![heap_sort_step12](heap_sort.assets/heap_sort_step12.png){ class="animation-figure" } -

Figure 11-12   Heap sort process

+

Figure 11-12   Heap sort steps

-In the code implementation, we used the sift-down function `sift_down()` from the "Heap" chapter. It is important to note that since the heap's length decreases as the maximum element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is shown below: +In the code implementation, we use the same top-to-bottom heapify function `sift_down()` from the "Heap" chapter. It is worth noting that since the heap length will decrease as the largest element is extracted, we need to add a length parameter $n$ to the `sift_down()` function to specify the current effective length of the heap. The code is as follows: === "Python" @@ -109,7 +109,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th /* Heap length is n, start heapifying node i, from top to bottom */ void siftDown(vector &nums, int n, int i) { while (true) { - // Determine the largest node among i, l, r, noted as ma + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break int l = 2 * i + 1; int r = 2 * i + 2; int ma = i; @@ -117,7 +117,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th ma = l; if (r < n && nums[r] > nums[ma]) ma = r; - // If node i is the largest or indices l, r are out of bounds, no further heapification needed, break + // Swap two nodes if (ma == i) { break; } @@ -136,7 +136,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th } // Extract the largest element from the heap and repeat for n-1 rounds for (int i = nums.size() - 1; i > 0; --i) { - // Swap the root node with the rightmost leaf node (swap the first element with the last element) + // Delete node swap(nums[0], nums[i]); // Start heapifying the root node, from top to bottom siftDown(nums, i, 0); @@ -150,7 +150,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th /* Heap length is n, start heapifying node i, from top to bottom */ void siftDown(int[] nums, int n, int i) { while (true) { - // Determine the largest node among i, l, r, noted as ma + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break int l = 2 * i + 1; int r = 2 * i + 2; int ma = i; @@ -158,7 +158,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th ma = l; if (r < n && nums[r] > nums[ma]) ma = r; - // If node i is the largest or indices l, r are out of bounds, no further heapification needed, break + // Swap two nodes if (ma == i) break; // Swap two nodes @@ -178,7 +178,7 @@ In the code implementation, we used the sift-down function `sift_down()` from th } // Extract the largest element from the heap and repeat for n-1 rounds for (int i = nums.length - 1; i > 0; i--) { - // Swap the root node with the rightmost leaf node (swap the first element with the last element) + // Delete node int tmp = nums[0]; nums[0] = nums[i]; nums[i] = tmp; @@ -191,93 +191,429 @@ In the code implementation, we used the sift-down function `sift_down()` from th === "C#" ```csharp title="heap_sort.cs" - [class]{heap_sort}-[func]{SiftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + void SiftDown(int[] nums, int n, int i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma == i) + break; + // Swap two nodes + (nums[ma], nums[i]) = (nums[i], nums[ma]); + // Loop downwards heapification + i = ma; + } + } - [class]{heap_sort}-[func]{HeapSort} + /* Heap sort */ + void HeapSort(int[] nums) { + // Build heap operation: heapify all nodes except leaves + for (int i = nums.Length / 2 - 1; i >= 0; i--) { + SiftDown(nums, nums.Length, i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (int i = nums.Length - 1; i > 0; i--) { + // Delete node + (nums[i], nums[0]) = (nums[0], nums[i]); + // Start heapifying the root node, from top to bottom + SiftDown(nums, i, 0); + } + } ``` === "Go" ```go title="heap_sort.go" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + func siftDown(nums *[]int, n, i int) { + for true { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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 + } + // Swap two nodes + if ma == i { + break + } + // Swap two nodes + (*nums)[i], (*nums)[ma] = (*nums)[ma], (*nums)[i] + // Loop downwards heapification + i = ma + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + func heapSort(nums *[]int) { + // Build heap operation: heapify all nodes except leaves + for i := len(*nums)/2 - 1; i >= 0; i-- { + siftDown(nums, len(*nums), i) + } + // Extract the largest element from the heap and repeat for n-1 rounds + for i := len(*nums) - 1; i > 0; i-- { + // Delete node + (*nums)[0], (*nums)[i] = (*nums)[i], (*nums)[0] + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0) + } + } ``` === "Swift" ```swift title="heap_sort.swift" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + func siftDown(nums: inout [Int], n: Int, i: Int) { + var i = i + while true { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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 + } + // Swap two nodes + if ma == i { + break + } + // Swap two nodes + nums.swapAt(i, ma) + // Loop downwards heapification + i = ma + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + func heapSort(nums: inout [Int]) { + // Build heap operation: heapify all nodes except leaves + for i in stride(from: nums.count / 2 - 1, through: 0, by: -1) { + siftDown(nums: &nums, n: nums.count, i: i) + } + // Extract the largest element from the heap and repeat for n-1 rounds + for i in nums.indices.dropFirst().reversed() { + // Delete node + nums.swapAt(0, i) + // Start heapifying the root node, from top to bottom + siftDown(nums: &nums, n: i, i: 0) + } + } ``` === "JS" ```javascript title="heap_sort.js" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + function siftDown(nums, n, i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + } + // Swap two nodes + if (ma === i) { + break; + } + // Swap two nodes + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // Loop downwards heapification + i = ma; + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + function heapSort(nums) { + // Build heap operation: heapify all nodes except leaves + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (let i = nums.length - 1; i > 0; i--) { + // Delete node + [nums[0], nums[i]] = [nums[i], nums[0]]; + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0); + } + } ``` === "TS" ```typescript title="heap_sort.ts" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + function siftDown(nums: number[], n: number, i: number): void { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + } + // Swap two nodes + if (ma === i) { + break; + } + // Swap two nodes + [nums[i], nums[ma]] = [nums[ma], nums[i]]; + // Loop downwards heapification + i = ma; + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + function heapSort(nums: number[]): void { + // Build heap operation: heapify all nodes except leaves + for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (let i = nums.length - 1; i > 0; i--) { + // Delete node + [nums[0], nums[i]] = [nums[i], nums[0]]; + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0); + } + } ``` === "Dart" ```dart title="heap_sort.dart" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + void siftDown(List nums, int n, int i) { + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma == i) break; + // Swap two nodes + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // Loop downwards heapification + i = ma; + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + void heapSort(List nums) { + // Build heap operation: heapify all nodes except leaves + for (int i = nums.length ~/ 2 - 1; i >= 0; i--) { + siftDown(nums, nums.length, i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (int i = nums.length - 1; i > 0; i--) { + // Delete node + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0); + } + } ``` === "Rust" ```rust title="heap_sort.rs" - [class]{}-[func]{sift_down} + /* Heap length is n, start heapifying node i, from top to bottom */ + fn sift_down(nums: &mut [i32], n: usize, mut i: usize) { + loop { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + } + // Swap two nodes + if ma == i { + break; + } + // Swap two nodes + nums.swap(i, ma); + // Loop downwards heapification + i = ma; + } + } - [class]{}-[func]{heap_sort} + /* Heap sort */ + fn heap_sort(nums: &mut [i32]) { + // Build heap operation: heapify all nodes except leaves + for i in (0..nums.len() / 2).rev() { + sift_down(nums, nums.len(), i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for i in (1..nums.len()).rev() { + // Delete node + nums.swap(0, i); + // Start heapifying the root node, from top to bottom + sift_down(nums, i, 0); + } + } ``` === "C" ```c title="heap_sort.c" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + void siftDown(int nums[], int n, int i) { + while (1) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + 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; + // Swap two nodes + if (ma == i) { + break; + } + // Swap two nodes + int temp = nums[i]; + nums[i] = nums[ma]; + nums[ma] = temp; + // Loop downwards heapification + i = ma; + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + void heapSort(int nums[], int n) { + // Build heap operation: heapify all nodes except leaves + for (int i = n / 2 - 1; i >= 0; --i) { + siftDown(nums, n, i); + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (int i = n - 1; i > 0; --i) { + // Delete node + int tmp = nums[0]; + nums[0] = nums[i]; + nums[i] = tmp; + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0); + } + } ``` === "Kotlin" ```kotlin title="heap_sort.kt" - [class]{}-[func]{siftDown} + /* Heap length is n, start heapifying node i, from top to bottom */ + fun siftDown(nums: IntArray, n: Int, li: Int) { + var i = li + while (true) { + // If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + val l = 2 * i + 1 + val r = 2 * i + 2 + var ma = i + if (l < n && nums[l] > nums[ma]) + ma = l + if (r < n && nums[r] > nums[ma]) + ma = r + // Swap two nodes + if (ma == i) + break + // Swap two nodes + val temp = nums[i] + nums[i] = nums[ma] + nums[ma] = temp + // Loop downwards heapification + i = ma + } + } - [class]{}-[func]{heapSort} + /* Heap sort */ + fun heapSort(nums: IntArray) { + // Build heap operation: heapify all nodes except leaves + for (i in nums.size / 2 - 1 downTo 0) { + siftDown(nums, nums.size, i) + } + // Extract the largest element from the heap and repeat for n-1 rounds + for (i in nums.size - 1 downTo 1) { + // Delete node + val temp = nums[0] + nums[0] = nums[i] + nums[i] = temp + // Start heapifying the root node, from top to bottom + siftDown(nums, i, 0) + } + } ``` === "Ruby" ```ruby title="heap_sort.rb" - [class]{}-[func]{sift_down} + ### Heap length is n, heapify from node i, top to bottom ### + def sift_down(nums, n, i) + while true + # If node i is largest or indices l, r are out of bounds, no need to continue heapify, break + l = 2 * i + 1 + r = 2 * i + 2 + ma = i + ma = l if l < n && nums[l] > nums[ma] + ma = r if r < n && nums[r] > nums[ma] + # Swap two nodes + break if ma == i + # Swap two nodes + nums[i], nums[ma] = nums[ma], nums[i] + # Loop downwards heapification + i = ma + end + end - [class]{}-[func]{heap_sort} + ### Heap sort ### + def heap_sort(nums) + # Build heap operation: heapify all nodes except leaves + (nums.length / 2 - 1).downto(0) do |i| + sift_down(nums, nums.length, i) + end + # Extract the largest element from the heap and repeat for n-1 rounds + (nums.length - 1).downto(1) do |i| + # Delete node + nums[0], nums[i] = nums[i], nums[0] + # Start heapifying the root node, from top to bottom + sift_down(nums, i, 0) + end + end ``` -=== "Zig" +## 11.7.2   Algorithm Characteristics - ```zig title="heap_sort.zig" - [class]{}-[func]{siftDown} - - [class]{}-[func]{heapSort} - ``` - -## 11.7.2   Algorithm characteristics - -- **Time complexity is $O(n \log n)$, non-adaptive sort**: The heap creation uses $O(n)$ time. Extracting the largest element from the heap takes $O(\log n)$ time, looping for $n - 1$ rounds. -- **Space complexity is $O(1)$, in-place sort**: A few pointer variables use $O(1)$ space. The element swapping and heapifying operations are performed on the original array. -- **Non-stable sort**: The relative positions of equal elements may change during the swapping of the heap's top and bottom elements. +- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The build heap operation uses $O(n)$ time. Extracting the largest element from the heap has a time complexity of $O(\log n)$, looping a total of $n - 1$ rounds. +- **Space complexity of $O(1)$, in-place sorting**: A few pointer variables use $O(1)$ space. Element swapping and heapify operations are both performed on the original array. +- **Non-stable sorting**: When swapping the heap top element and heap bottom element, the relative positions of equal elements may change. diff --git a/en/docs/chapter_sorting/index.md b/en/docs/chapter_sorting/index.md index 5439c0b24..71a4428a4 100644 --- a/en/docs/chapter_sorting/index.md +++ b/en/docs/chapter_sorting/index.md @@ -9,20 +9,20 @@ icon: material/sort-ascending !!! abstract - Sorting is like a magical key that turns chaos into order, enabling us to understand and handle data more efficiently. + Sorting is like a magic key that transforms chaos into order, enabling us to understand and process data more efficiently. - Whether it's simple ascending order or complex categorical arrangements, sorting reveals the harmonious beauty of data. + Whether it's simple ascending order or complex categorized arrangements, sorting demonstrates the harmonious beauty of data. ## Chapter contents -- [11.1   Sorting algorithms](sorting_algorithm.md) -- [11.2   Selection sort](selection_sort.md) -- [11.3   Bubble sort](bubble_sort.md) -- [11.4   Insertion sort](insertion_sort.md) -- [11.5   Quick sort](quick_sort.md) -- [11.6   Merge sort](merge_sort.md) -- [11.7   Heap sort](heap_sort.md) -- [11.8   Bucket sort](bucket_sort.md) -- [11.9   Counting sort](counting_sort.md) -- [11.10   Radix sort](radix_sort.md) +- [11.1   Sorting Algorithms](sorting_algorithm.md) +- [11.2   Selection Sort](selection_sort.md) +- [11.3   Bubble Sort](bubble_sort.md) +- [11.4   Insertion Sort](insertion_sort.md) +- [11.5   Quick Sort](quick_sort.md) +- [11.6   Merge Sort](merge_sort.md) +- [11.7   Heap Sort](heap_sort.md) +- [11.8   Bucket Sort](bucket_sort.md) +- [11.9   Counting Sort](counting_sort.md) +- [11.10   Radix Sort](radix_sort.md) - [11.11   Summary](summary.md) diff --git a/en/docs/chapter_sorting/insertion_sort.md b/en/docs/chapter_sorting/insertion_sort.md index b94bdcdb2..a208c08e5 100644 --- a/en/docs/chapter_sorting/insertion_sort.md +++ b/en/docs/chapter_sorting/insertion_sort.md @@ -2,30 +2,30 @@ comments: true --- -# 11.4   Insertion sort +# 11.4   Insertion Sort -Insertion sort is a simple sorting algorithm that works very much like the process of manually sorting a deck of cards. +Insertion sort (insertion sort) is a simple sorting algorithm that works very similarly to the process of manually organizing a deck of cards. -Specifically, we select a base element from the unsorted interval, compare it with the elements in the sorted interval to its left, and insert the element into the correct position. +Specifically, we select a base element from the unsorted interval, compare the element with elements in the sorted interval to its left one by one, and insert the element into the correct position. -Figure 11-6 illustrates how an element is inserted into the array. Assuming the base element is `base`, we need to shift all elements from the target index up to `base` one position to the right, then assign `base` to the target index. +Figure 11-6 shows the operation flow of inserting an element into the array. Let the base element be `base`. We need to move all elements from the target index to `base` one position to the right, and then assign `base` to the target index. ![Single insertion operation](insertion_sort.assets/insertion_operation.png){ class="animation-figure" }

Figure 11-6   Single insertion operation

-## 11.4.1   Algorithm process +## 11.4.1   Algorithm Flow -The overall process of insertion sort is shown in Figure 11-7. +The overall flow of insertion sort is shown in Figure 11-7. -1. Consider the first element of the array as sorted. -2. Select the second element as `base`, insert it into its correct position, **leaving the first two elements sorted**. -3. Select the third element as `base`, insert it into its correct position, **leaving the first three elements sorted**. -4. Continuing in this manner, in the final iteration, the last element is taken as `base`, and after inserting it into the correct position, **all elements are sorted**. +1. Initially, the first element of the array has completed sorting. +2. Select the second element of the array as `base`, and after inserting it into the correct position, **the first 2 elements of the array are sorted**. +3. Select the third element as `base`, and after inserting it into the correct position, **the first 3 elements of the array are sorted**. +4. And so on. In the last round, select the last element as `base`, and after inserting it into the correct position, **all elements are sorted**. -![Insertion sort process](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } +![Insertion sort flow](insertion_sort.assets/insertion_sort_overview.png){ class="animation-figure" } -

Figure 11-7   Insertion sort process

+

Figure 11-7   Insertion sort flow

Example code is as follows: @@ -34,11 +34,11 @@ Example code is as follows: ```python title="insertion_sort.py" def insertion_sort(nums: list[int]): """Insertion sort""" - # Outer loop: sorted range is [0, i-1] + # Outer loop: sorted interval is [0, i-1] for i in range(1, len(nums)): base = nums[i] j = i - 1 - # Inner loop: insert base into the correct position within the sorted range [0, i-1] + # Inner loop: insert base into the correct position within the sorted interval [0, i-1] while j >= 0 and nums[j] > base: nums[j + 1] = nums[j] # Move nums[j] to the right by one position j -= 1 @@ -50,10 +50,10 @@ Example code is as follows: ```cpp title="insertion_sort.cpp" /* Insertion sort */ void insertionSort(vector &nums) { - // Outer loop: sorted range is [0, i-1] + // Outer loop: sorted interval is [0, i-1] for (int i = 1; i < nums.size(); i++) { int base = nums[i], j = i - 1; - // Inner loop: insert base into the correct position within the sorted range [0, i-1] + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] while (j >= 0 && nums[j] > base) { nums[j + 1] = nums[j]; // Move nums[j] to the right by one position j--; @@ -68,10 +68,10 @@ Example code is as follows: ```java title="insertion_sort.java" /* Insertion sort */ void insertionSort(int[] nums) { - // Outer loop: sorted range is [0, i-1] + // Outer loop: sorted interval is [0, i-1] for (int i = 1; i < nums.length; i++) { int base = nums[i], j = i - 1; - // Inner loop: insert base into the correct position within the sorted range [0, i-1] + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] while (j >= 0 && nums[j] > base) { nums[j + 1] = nums[j]; // Move nums[j] to the right by one position j--; @@ -84,85 +84,208 @@ Example code is as follows: === "C#" ```csharp title="insertion_sort.cs" - [class]{insertion_sort}-[func]{InsertionSort} + /* Insertion sort */ + void InsertionSort(int[] nums) { + // Outer loop: sorted interval is [0, i-1] + for (int i = 1; i < nums.Length; i++) { + int bas = nums[i], j = i - 1; + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > bas) { + nums[j + 1] = nums[j]; // Move nums[j] to the right by one position + j--; + } + nums[j + 1] = bas; // Assign base to the correct position + } + } ``` === "Go" ```go title="insertion_sort.go" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + func insertionSort(nums []int) { + // Outer loop: sorted interval is [0, i-1] + for i := 1; i < len(nums); i++ { + base := nums[i] + j := i - 1 + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + for j >= 0 && nums[j] > base { + nums[j+1] = nums[j] // Move nums[j] to the right by one position + j-- + } + nums[j+1] = base // Assign base to the correct position + } + } ``` === "Swift" ```swift title="insertion_sort.swift" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + func insertionSort(nums: inout [Int]) { + // Outer loop: sorted interval is [0, i-1] + for i in nums.indices.dropFirst() { + let base = nums[i] + var j = i - 1 + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while j >= 0, nums[j] > base { + nums[j + 1] = nums[j] // Move nums[j] to the right by one position + j -= 1 + } + nums[j + 1] = base // Assign base to the correct position + } + } ``` === "JS" ```javascript title="insertion_sort.js" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + function insertionSort(nums) { + // Outer loop: sorted interval is [0, i-1] + for (let i = 1; i < nums.length; i++) { + let base = nums[i], + j = i - 1; + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // Move nums[j] to the right by one position + j--; + } + nums[j + 1] = base; // Assign base to the correct position + } + } ``` === "TS" ```typescript title="insertion_sort.ts" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + function insertionSort(nums: number[]): void { + // Outer loop: sorted interval is [0, i-1] + for (let i = 1; i < nums.length; i++) { + const base = nums[i]; + let j = i - 1; + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // Move nums[j] to the right by one position + j--; + } + nums[j + 1] = base; // Assign base to the correct position + } + } ``` === "Dart" ```dart title="insertion_sort.dart" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + void insertionSort(List nums) { + // Outer loop: sorted interval is [0, i-1] + for (int i = 1; i < nums.length; i++) { + int base = nums[i], j = i - 1; + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j]; // Move nums[j] to the right by one position + j--; + } + nums[j + 1] = base; // Assign base to the correct position + } + } ``` === "Rust" ```rust title="insertion_sort.rs" - [class]{}-[func]{insertion_sort} + /* Insertion sort */ + fn insertion_sort(nums: &mut [i32]) { + // Outer loop: sorted interval is [0, i-1] + for i in 1..nums.len() { + let (base, mut j) = (nums[i], (i - 1) as i32); + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while j >= 0 && nums[j as usize] > base { + nums[(j + 1) as usize] = nums[j as usize]; // Move nums[j] to the right by one position + j -= 1; + } + nums[(j + 1) as usize] = base; // Assign base to the correct position + } + } ``` === "C" ```c title="insertion_sort.c" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + void insertionSort(int nums[], int size) { + // Outer loop: sorted interval is [0, i-1] + for (int i = 1; i < size; i++) { + int base = nums[i], j = i - 1; + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > base) { + // Move nums[j] to the right by one position + nums[j + 1] = nums[j]; + j--; + } + // Assign base to the correct position + nums[j + 1] = base; + } + } ``` === "Kotlin" ```kotlin title="insertion_sort.kt" - [class]{}-[func]{insertionSort} + /* Insertion sort */ + fun insertionSort(nums: IntArray) { + // Outer loop: sorted elements are 1, 2, ..., n + for (i in nums.indices) { + val base = nums[i] + var j = i - 1 + // Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while (j >= 0 && nums[j] > base) { + nums[j + 1] = nums[j] // Move nums[j] to the right by one position + j-- + } + nums[j + 1] = base // Assign base to the correct position + } + } ``` === "Ruby" ```ruby title="insertion_sort.rb" - [class]{}-[func]{insertion_sort} + ### Insertion sort ### + def insertion_sort(nums) + n = nums.length + # Outer loop: sorted interval is [0, i-1] + for i in 1...n + base = nums[i] + j = i - 1 + # Inner loop: insert base into the correct position within the sorted interval [0, i-1] + while j >= 0 && nums[j] > base + nums[j + 1] = nums[j] # Move nums[j] to the right by one position + j -= 1 + end + nums[j + 1] = base # Assign base to the correct position + end + end ``` -=== "Zig" +## 11.4.2   Algorithm Characteristics - ```zig title="insertion_sort.zig" - [class]{}-[func]{insertionSort} - ``` +- **Time complexity of $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires loops of $n - 1$, $n-2$, $\dots$, $2$, $1$, summing to $(n - 1) n / 2$, so the time complexity is $O(n^2)$. When encountering ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best-case time complexity of $O(n)$. +- **Space complexity of $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. +- **Stable sorting**: During the insertion operation process, we insert elements to the right of equal elements, without changing their order. -## 11.4.2   Algorithm characteristics +## 11.4.3   Advantages of Insertion Sort -- **Time complexity is $O(n^2)$, adaptive sorting**: In the worst case, each insertion operation requires $n - 1$, $n-2$, ..., $2$, $1$ loops, summing up to $(n - 1) n / 2$, thus the time complexity is $O(n^2)$. In the case of ordered data, the insertion operation will terminate early. When the input array is completely ordered, insertion sort achieves the best time complexity of $O(n)$. -- **Space complexity is $O(1)$, in-place sorting**: Pointers $i$ and $j$ use a constant amount of extra space. -- **Stable sorting**: During the insertion operation, we insert elements to the right of equal elements, not changing their order. +The time complexity of insertion sort is $O(n^2)$, while the time complexity of quick sort, which we will learn about next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **insertion sort is usually faster for smaller data volumes**. -## 11.4.3   Advantages of insertion sort +This conclusion is similar to the applicable situations of linear search and binary search. Algorithms like quick sort with $O(n \log n)$ complexity are sorting algorithms based on divide-and-conquer strategy and often contain more unit computation operations. When the data volume is small, $n^2$ and $n \log n$ are numerically close, and complexity does not dominate; the number of unit operations per round plays a decisive role. -The time complexity of insertion sort is $O(n^2)$, while the time complexity of quicksort, which we will study next, is $O(n \log n)$. Although insertion sort has a higher time complexity, **it is usually faster in small input sizes**. +In fact, the built-in sorting functions in many programming languages (such as Java) adopt insertion sort. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategy, such as quick sort; for short arrays, directly use insertion sort. -This conclusion is similar to that for linear and binary search. Algorithms like quicksort that have a time complexity of $O(n \log n)$ and are based on the divide-and-conquer strategy often involve more unit operations. For small input sizes, the numerical values of $n^2$ and $n \log n$ are close, and complexity does not dominate, with the number of unit operations per round playing a decisive role. +Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in actual situations, **insertion sort is used significantly more frequently than bubble sort and selection sort**, mainly for the following reasons. -In fact, many programming languages (such as Java) use insertion sort within their built-in sorting functions. The general approach is: for long arrays, use sorting algorithms based on divide-and-conquer strategies, such as quicksort; for short arrays, use insertion sort directly. - -Although bubble sort, selection sort, and insertion sort all have a time complexity of $O(n^2)$, in practice, **insertion sort is commonly used than bubble sort and selection sort**, mainly for the following reasons. - -- Bubble sort is based on element swapping, which requires the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is generally higher than that of insertion sort**. -- The time complexity of selection sort is always $O(n^2)$. **Given a set of partially ordered data, insertion sort is usually more efficient than selection sort**. +- Bubble sort is based on element swapping, requiring the use of a temporary variable, involving 3 unit operations; insertion sort is based on element assignment, requiring only 1 unit operation. Therefore, **the computational overhead of bubble sort is usually higher than that of insertion sort**. +- Selection sort has a time complexity of $O(n^2)$ in any case. **If given a set of partially ordered data, insertion sort is usually more efficient than selection sort**. - Selection sort is unstable and cannot be applied to multi-level sorting. diff --git a/en/docs/chapter_sorting/merge_sort.md b/en/docs/chapter_sorting/merge_sort.md index 89263b8e9..6143108d5 100644 --- a/en/docs/chapter_sorting/merge_sort.md +++ b/en/docs/chapter_sorting/merge_sort.md @@ -2,28 +2,28 @@ comments: true --- -# 11.6   Merge sort +# 11.6   Merge Sort -Merge sort is a sorting algorithm based on the divide-and-conquer strategy, involving the "divide" and "merge" phases shown in Figure 11-10. +Merge sort (merge sort) is a sorting algorithm based on the divide-and-conquer strategy, which includes the "divide" and "merge" phases shown in Figure 11-10. -1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into shorter arrays. -2. **Merge phase**: Stop dividing when the length of the sub-array is 1, and then begin merging. The two shorter sorted arrays are continuously merged into a longer sorted array until the process is complete. +1. **Divide phase**: Recursively split the array from the midpoint, transforming the sorting problem of a long array into the sorting problems of shorter arrays. +2. **Merge phase**: When the sub-array length is 1, terminate the division and start merging, continuously merging two shorter sorted arrays into one longer sorted array until the process is complete. -![The divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } +![Divide and merge phases of merge sort](merge_sort.assets/merge_sort_overview.png){ class="animation-figure" } -

Figure 11-10   The divide and merge phases of merge sort

+

Figure 11-10   Divide and merge phases of merge sort

-## 11.6.1   Algorithm workflow +## 11.6.1   Algorithm Flow As shown in Figure 11-11, the "divide phase" recursively splits the array from the midpoint into two sub-arrays from top to bottom. -1. Calculate the midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and the right sub-array (interval `[mid + 1, right]`). -2. Continue with step `1.` recursively until sub-array length becomes 1, then stops. +1. Calculate the array midpoint `mid`, recursively divide the left sub-array (interval `[left, mid]`) and right sub-array (interval `[mid + 1, right]`). +2. Recursively execute step `1.` until the sub-array interval length is 1, then terminate. -The "merge phase" combines the left and right sub-arrays into a sorted array from bottom to top. It is important to note that, merging starts with sub-arrays of length 1, and each sub-array is sorted during the merge phase. +The "merge phase" merges the left sub-array and right sub-array into a sorted array from bottom to top. Note that merging starts from sub-arrays of length 1, and each sub-array in the merge phase is sorted. === "<1>" - ![Merge sort process](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } + ![Merge sort steps](merge_sort.assets/merge_sort_step1.png){ class="animation-figure" } === "<2>" ![merge_sort_step2](merge_sort.assets/merge_sort_step2.png){ class="animation-figure" } @@ -52,14 +52,14 @@ The "merge phase" combines the left and right sub-arrays into a sorted array fro === "<10>" ![merge_sort_step10](merge_sort.assets/merge_sort_step10.png){ class="animation-figure" } -

Figure 11-11   Merge sort process

+

Figure 11-11   Merge sort steps

-It can be observed that the order of recursion in merge sort is consistent with the post-order traversal of a binary tree. +It can be observed that the recursive order of merge sort is consistent with the post-order traversal of a binary tree. -- **Post-order traversal**: First recursively traverse the left subtree, then the right subtree, and finally process the root node. -- **Merge sort**: First recursively process the left sub-array, then the right sub-array, and finally perform the merge. +- **Post-order traversal**: First recursively traverse the left subtree, then recursively traverse the right subtree, and finally process the root node. +- **Merge sort**: First recursively process the left sub-array, then recursively process the right sub-array, and finally perform the merge. -The implementation of merge sort is shown in the following code. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`. +The implementation of merge sort is shown in the code below. Note that the interval to be merged in `nums` is `[left, right]`, while the corresponding interval in `tmp` is `[0, right - left]`. === "Python" @@ -98,8 +98,8 @@ The implementation of merge sort is shown in the following code. Note that the i # Termination condition if left >= right: return # Terminate recursion when subarray length is 1 - # Partition stage - mid = left + (right - left) // 2 # Calculate midpoint + # Divide and conquer stage + mid = (left + right) // 2 # Calculate midpoint merge_sort(nums, left, mid) # Recursively process the left subarray merge_sort(nums, mid + 1, right) # Recursively process the right subarray # Merge stage @@ -141,7 +141,7 @@ The implementation of merge sort is shown in the following code. Note that the i // Termination condition if (left >= right) return; // Terminate recursion when subarray length is 1 - // Partition stage + // Divide and conquer stage int mid = left + (right - left) / 2; // Calculate midpoint mergeSort(nums, left, mid); // Recursively process the left subarray mergeSort(nums, mid + 1, right); // Recursively process the right subarray @@ -185,7 +185,7 @@ The implementation of merge sort is shown in the following code. Note that the i // Termination condition if (left >= right) return; // Terminate recursion when subarray length is 1 - // Partition stage + // Divide and conquer stage int mid = left + (right - left) / 2; // Calculate midpoint mergeSort(nums, left, mid); // Recursively process the left subarray mergeSort(nums, mid + 1, right); // Recursively process the right subarray @@ -197,102 +197,499 @@ The implementation of merge sort is shown in the following code. Note that the i === "C#" ```csharp title="merge_sort.cs" - [class]{merge_sort}-[func]{Merge} + /* Merge left subarray and right subarray */ + void Merge(int[] nums, int left, int mid, int right) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + int[] tmp = new int[right - left + 1]; + // Initialize the start indices of the left and right subarrays + int i = left, j = mid + 1, k = 0; + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (k = 0; k < tmp.Length; ++k) { + nums[left + k] = tmp[k]; + } + } - [class]{merge_sort}-[func]{MergeSort} + /* Merge sort */ + void MergeSort(int[] nums, int left, int right) { + // Termination condition + if (left >= right) return; // Terminate recursion when subarray length is 1 + // Divide and conquer stage + int mid = left + (right - left) / 2; // Calculate midpoint + MergeSort(nums, left, mid); // Recursively process the left subarray + MergeSort(nums, mid + 1, right); // Recursively process the right subarray + // Merge stage + Merge(nums, left, mid, right); + } ``` === "Go" ```go title="merge_sort.go" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + func merge(nums []int, left, mid, right int) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + tmp := make([]int, right-left+1) + // Initialize the start indices of the left and right subarrays + i, j, k := left, mid+1, 0 + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + for i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i++ + } else { + tmp[k] = nums[j] + j++ + } + k++ + } + // Copy the remaining elements of the left and right subarrays into the temporary array + for i <= mid { + tmp[k] = nums[i] + i++ + k++ + } + for j <= right { + tmp[k] = nums[j] + j++ + k++ + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for k := 0; k < len(tmp); k++ { + nums[left+k] = tmp[k] + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + func mergeSort(nums []int, left, right int) { + // Termination condition + if left >= right { + return + } + // Divide and conquer stage + mid := left + (right - left) / 2 + mergeSort(nums, left, mid) + mergeSort(nums, mid+1, right) + // Merge stage + merge(nums, left, mid, right) + } ``` === "Swift" ```swift title="merge_sort.swift" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + func merge(nums: inout [Int], left: Int, mid: Int, right: Int) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + var tmp = Array(repeating: 0, count: right - left + 1) + // Initialize the start indices of the left and right subarrays + var i = left, j = mid + 1, k = 0 + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while i <= mid, j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i] + i += 1 + } else { + tmp[k] = nums[j] + j += 1 + } + k += 1 + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while i <= mid { + tmp[k] = nums[i] + i += 1 + k += 1 + } + while j <= right { + tmp[k] = nums[j] + j += 1 + k += 1 + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for k in tmp.indices { + nums[left + k] = tmp[k] + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + func mergeSort(nums: inout [Int], left: Int, right: Int) { + // Termination condition + if left >= right { // Terminate recursion when subarray length is 1 + return + } + // Divide and conquer stage + let mid = left + (right - left) / 2 // Calculate midpoint + mergeSort(nums: &nums, left: left, right: mid) // Recursively process the left subarray + mergeSort(nums: &nums, left: mid + 1, right: right) // Recursively process the right subarray + // Merge stage + merge(nums: &nums, left: left, mid: mid, right: right) + } ``` === "JS" ```javascript title="merge_sort.js" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + function merge(nums, left, mid, right) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + const tmp = new Array(right - left + 1); + // Initialize the start indices of the left and right subarrays + let i = left, + j = mid + 1, + k = 0; + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + function mergeSort(nums, left, right) { + // Termination condition + if (left >= right) return; // Terminate recursion when subarray length is 1 + // Divide and conquer stage + let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint + mergeSort(nums, left, mid); // Recursively process the left subarray + mergeSort(nums, mid + 1, right); // Recursively process the right subarray + // Merge stage + merge(nums, left, mid, right); + } ``` === "TS" ```typescript title="merge_sort.ts" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + function merge(nums: number[], left: number, mid: number, right: number): void { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + const tmp = new Array(right - left + 1); + // Initialize the start indices of the left and right subarrays + let i = left, + j = mid + 1, + k = 0; + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + function mergeSort(nums: number[], left: number, right: number): void { + // Termination condition + if (left >= right) return; // Terminate recursion when subarray length is 1 + // Divide and conquer stage + let mid = Math.floor(left + (right - left) / 2); // Calculate midpoint + mergeSort(nums, left, mid); // Recursively process the left subarray + mergeSort(nums, mid + 1, right); // Recursively process the right subarray + // Merge stage + merge(nums, left, mid, right); + } ``` === "Dart" ```dart title="merge_sort.dart" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + void merge(List nums, int left, int mid, int right) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + List tmp = List.filled(right - left + 1, 0); + // Initialize the start indices of the left and right subarrays + int i = left, j = mid + 1, k = 0; + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++]; + else + tmp[k++] = nums[j++]; + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (k = 0; k < tmp.length; k++) { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + void mergeSort(List nums, int left, int right) { + // Termination condition + if (left >= right) return; // Terminate recursion when subarray length is 1 + // Divide and conquer stage + int mid = left + (right - left) ~/ 2; // Calculate midpoint + mergeSort(nums, left, mid); // Recursively process the left subarray + mergeSort(nums, mid + 1, right); // Recursively process the right subarray + // Merge stage + merge(nums, left, mid, right); + } ``` === "Rust" ```rust title="merge_sort.rs" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + fn merge(nums: &mut [i32], left: usize, mid: usize, right: usize) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + let tmp_size = right - left + 1; + let mut tmp = vec![0; tmp_size]; + // Initialize the start indices of the left and right subarrays + let (mut i, mut j, mut k) = (left, mid + 1, 0); + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while i <= mid && j <= right { + if nums[i] <= nums[j] { + tmp[k] = nums[i]; + i += 1; + } else { + tmp[k] = nums[j]; + j += 1; + } + k += 1; + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while i <= mid { + tmp[k] = nums[i]; + k += 1; + i += 1; + } + while j <= right { + tmp[k] = nums[j]; + k += 1; + j += 1; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for k in 0..tmp_size { + nums[left + k] = tmp[k]; + } + } - [class]{}-[func]{merge_sort} + /* Merge sort */ + fn merge_sort(nums: &mut [i32], left: usize, right: usize) { + // Termination condition + if left >= right { + return; // Terminate recursion when subarray length is 1 + } + + // Divide and conquer stage + let mid = left + (right - left) / 2; // Calculate midpoint + merge_sort(nums, left, mid); // Recursively process the left subarray + merge_sort(nums, mid + 1, right); // Recursively process the right subarray + + // Merge stage + merge(nums, left, mid, right); + } ``` === "C" ```c title="merge_sort.c" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + void merge(int *nums, int left, int mid, int right) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + int tmpSize = right - left + 1; + int *tmp = (int *)malloc(tmpSize * sizeof(int)); + // Initialize the start indices of the left and right subarrays + int i = left, j = mid + 1, k = 0; + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) { + tmp[k++] = nums[i++]; + } else { + tmp[k++] = nums[j++]; + } + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++]; + } + while (j <= right) { + tmp[k++] = nums[j++]; + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (k = 0; k < tmpSize; ++k) { + nums[left + k] = tmp[k]; + } + // Free memory + free(tmp); + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + void mergeSort(int *nums, int left, int right) { + // Termination condition + if (left >= right) + return; // Terminate recursion when subarray length is 1 + // Divide and conquer stage + int mid = left + (right - left) / 2; // Calculate midpoint + mergeSort(nums, left, mid); // Recursively process the left subarray + mergeSort(nums, mid + 1, right); // Recursively process the right subarray + // Merge stage + merge(nums, left, mid, right); + } ``` === "Kotlin" ```kotlin title="merge_sort.kt" - [class]{}-[func]{merge} + /* Merge left subarray and right subarray */ + fun merge(nums: IntArray, left: Int, mid: Int, right: Int) { + // Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + // Create a temporary array tmp to store the merged results + val tmp = IntArray(right - left + 1) + // Initialize the start indices of the left and right subarrays + var i = left + var j = mid + 1 + var k = 0 + // While both subarrays still have elements, compare and copy the smaller element into the temporary array + while (i <= mid && j <= right) { + if (nums[i] <= nums[j]) + tmp[k++] = nums[i++] + else + tmp[k++] = nums[j++] + } + // Copy the remaining elements of the left and right subarrays into the temporary array + while (i <= mid) { + tmp[k++] = nums[i++] + } + while (j <= right) { + tmp[k++] = nums[j++] + } + // Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + for (l in tmp.indices) { + nums[left + l] = tmp[l] + } + } - [class]{}-[func]{mergeSort} + /* Merge sort */ + fun mergeSort(nums: IntArray, left: Int, right: Int) { + // Termination condition + if (left >= right) return // Terminate recursion when subarray length is 1 + // Divide and conquer stage + val mid = left + (right - left) / 2 // Calculate midpoint + mergeSort(nums, left, mid) // Recursively process the left subarray + mergeSort(nums, mid + 1, right) // Recursively process the right subarray + // Merge stage + merge(nums, left, mid, right) + } ``` === "Ruby" ```ruby title="merge_sort.rb" - [class]{}-[func]{merge} + ### Merge left and right subarrays ### + def merge(nums, left, mid, right) + # Left subarray interval is [left, mid], right subarray interval is [mid+1, right] + # Create temporary array tmp to store merged result + tmp = Array.new(right - left + 1, 0) + # Initialize the start indices of the left and right subarrays + i, j, k = left, mid + 1, 0 + # While both subarrays still have elements, compare and copy the smaller element into the temporary array + while i <= mid && j <= right + if nums[i] <= nums[j] + tmp[k] = nums[i] + i += 1 + else + tmp[k] = nums[j] + j += 1 + end + k += 1 + end + # Copy the remaining elements of the left and right subarrays into the temporary array + while i <= mid + tmp[k] = nums[i] + i += 1 + k += 1 + end + while j <= right + tmp[k] = nums[j] + j += 1 + k += 1 + end + # Copy the elements from the temporary array tmp back to the original array nums at the corresponding interval + (0...tmp.length).each do |k| + nums[left + k] = tmp[k] + end + end - [class]{}-[func]{merge_sort} + ### Merge sort ### + def merge_sort(nums, left, right) + # Termination condition + # Terminate recursion when subarray length is 1 + return if left >= right + # Divide and conquer stage + mid = left + (right - left) / 2 # Calculate midpoint + merge_sort(nums, left, mid) # Recursively process the left subarray + merge_sort(nums, mid + 1, right) # Recursively process the right subarray + # Merge stage + merge(nums, left, mid, right) + end ``` -=== "Zig" +## 11.6.2   Algorithm Characteristics - ```zig title="merge_sort.zig" - [class]{}-[func]{merge} +- **Time complexity of $O(n \log n)$, non-adaptive sorting**: The division produces a recursion tree of height $\log n$, and the total number of merge operations at each level is $n$, so the overall time complexity is $O(n \log n)$. +- **Space complexity of $O(n)$, non-in-place sorting**: The recursion depth is $\log n$, using $O(\log n)$ size of stack frame space. The merge operation requires the aid of an auxiliary array, using $O(n)$ size of additional space. +- **Stable sorting**: In the merge process, the order of equal elements remains unchanged. - [class]{}-[func]{mergeSort} - ``` +## 11.6.3   Linked List Sorting -## 11.6.2   Algorithm characteristics +For linked lists, merge sort has significant advantages over other sorting algorithms, **and can optimize the space complexity of linked list sorting tasks to $O(1)$**. -- **Time complexity of $O(n \log n)$, non-adaptive sort**: The division creates a recursion tree of height $\log n$, with each layer merging a total of $n$ operations, resulting in an overall time complexity of $O(n \log n)$. -- **Space complexity of $O(n)$, non-in-place sort**: The recursion depth is $\log n$, using $O(\log n)$ stack frame space. The merging operation requires auxiliary arrays, using an additional space of $O(n)$. -- **Stable sort**: During the merging process, the order of equal elements remains unchanged. +- **Divide phase**: "Iteration" can be used instead of "recursion" to implement linked list division work, thus saving the stack frame space used by recursion. +- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by just changing references (pointers), so there is no need to create additional linked lists during the merge phase (merging two short ordered linked lists into one long ordered linked list). -## 11.6.3   Linked List sorting - -For linked lists, merge sort has significant advantages over other sorting algorithms. **It can optimize the space complexity of the linked list sorting task to $O(1)$**. - -- **Divide phase**: "Iteration" can be used instead of "recursion" to perform the linked list division work, thus saving the stack frame space used by recursion. -- **Merge phase**: In linked lists, node insertion and deletion operations can be achieved by changing references (pointers), so no extra lists need to be created during the merge phase (combining two short ordered lists into one long ordered list). - -The implementation details are relatively complex, and interested readers can consult related materials for learning. +The specific implementation details are quite complex, and interested readers can consult related materials for learning. diff --git a/en/docs/chapter_sorting/quick_sort.md b/en/docs/chapter_sorting/quick_sort.md index 2371dbba3..7c78d86ac 100644 --- a/en/docs/chapter_sorting/quick_sort.md +++ b/en/docs/chapter_sorting/quick_sort.md @@ -2,18 +2,18 @@ comments: true --- -# 11.5   Quick sort +# 11.5   Quick Sort -Quick sort is a sorting algorithm based on the divide-and-conquer strategy, known for its efficiency and wide application. +Quick sort (quick sort) is a sorting algorithm based on the divide-and-conquer strategy, which operates efficiently and is widely applied. -The core operation of quick sort is "pivot partitioning," which aims to select an element from the array as the "pivot" and move all elements less than the pivot to its left side, while moving all elements greater than the pivot to its right side. Specifically, the process of pivot partitioning is illustrated in Figure 11-8. +The core operation of quick sort is "sentinel partitioning", which aims to: select a certain element in the array as the "pivot", move all elements smaller than the pivot to its left, and move elements larger than the pivot to its right. Specifically, the flow of sentinel partitioning is shown in Figure 11-8. -1. Select the leftmost element of the array as the pivot, and initialize two pointers `i` and `j` to point to the two ends of the array respectively. -2. Set up a loop where each round uses `i` (`j`) to search for the first element larger (smaller) than the pivot, then swap these two elements. -3. Repeat step `2.` until `i` and `j` meet, finally swap the pivot to the boundary between the two sub-arrays. +1. Select the leftmost element of the array as the pivot, and initialize two pointers `i` and `j` pointing to the two ends of the array. +2. Set up a loop in which `i` (`j`) is used in each round to find the first element larger (smaller) than the pivot, and then swap these two elements. +3. Loop through step `2.` until `i` and `j` meet, and finally swap the pivot to the boundary line of the two sub-arrays. === "<1>" - ![Pivot division process](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } + ![Sentinel partitioning steps](quick_sort.assets/pivot_division_step1.png){ class="animation-figure" } === "<2>" ![pivot_division_step2](quick_sort.assets/pivot_division_step2.png){ class="animation-figure" } @@ -39,19 +39,19 @@ The core operation of quick sort is "pivot partitioning," which aims to select a === "<9>" ![pivot_division_step9](quick_sort.assets/pivot_division_step9.png){ class="animation-figure" } -

Figure 11-8   Pivot division process

+

Figure 11-8   Sentinel partitioning steps

-After the pivot partitioning, the original array is divided into three parts: left sub-array, pivot, and right sub-array, satisfying "any element in the left sub-array $\leq$ pivot $\leq$ any element in the right sub-array." Therefore, we then only need to sort these two sub-arrays. +After sentinel partitioning is complete, the original array is divided into three parts: left sub-array, pivot, right sub-array, satisfying "any element in left sub-array $\leq$ pivot $\leq$ any element in right sub-array". Therefore, we next only need to sort these two sub-arrays. -!!! note "Divide-and-conquer strategy for quick sort" +!!! note "Divide-and-conquer strategy of quick sort" - The essence of pivot partitioning is to simplify the sorting problem of a longer array into two shorter arrays. + The essence of sentinel partitioning is to simplify the sorting problem of a longer array into the sorting problems of two shorter arrays. === "Python" ```python title="quick_sort.py" def partition(self, nums: list[int], left: int, right: int) -> int: - """Partition""" + """Sentinel partition""" # Use nums[left] as the pivot i, j = left, right while i < j: @@ -69,19 +69,19 @@ After the pivot partitioning, the original array is divided into three parts: le === "C++" ```cpp title="quick_sort.cpp" - /* Partition */ + /* Sentinel partition */ int partition(vector &nums, int left, int right) { // Use nums[left] as the pivot int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // Search from right to left for the first element smaller than the pivot + j--; // Search from right to left for the first element smaller than the pivot while (i < j && nums[i] <= nums[left]) - i++; // Search from left to right for the first element greater than the pivot - swap(nums, i, j); // Swap these two elements + i++; // Search from left to right for the first element greater than the pivot + swap(nums[i], nums[j]); // Swap these two elements } - swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays - return i; // Return the index of the pivot + swap(nums[i], nums[left]); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot } ``` @@ -95,7 +95,7 @@ After the pivot partitioning, the original array is divided into three parts: le nums[j] = tmp; } - /* Partition */ + /* Sentinel partition */ int partition(int[] nums, int left, int right) { // Use nums[left] as the pivot int i = left, j = right; @@ -114,94 +114,269 @@ After the pivot partitioning, the original array is divided into three parts: le === "C#" ```csharp title="quick_sort.cs" - [class]{quickSort}-[func]{Swap} + /* Swap elements */ + void Swap(int[] nums, int i, int j) { + (nums[j], nums[i]) = (nums[i], nums[j]); + } - [class]{quickSort}-[func]{Partition} + /* Sentinel partition */ + int Partition(int[] nums, int left, int right) { + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) + i++; // Search from left to right for the first element greater than the pivot + Swap(nums, i, j); // Swap these two elements + } + Swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSort}-[func]{partition} + /* Sentinel partition */ + func (q *quickSort) partition(nums []int, left, right int) int { + // Use nums[left] as the pivot + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // Search from right to left for the first element smaller than the pivot + } + for i < j && nums[i] <= nums[left] { + i++ // Search from left to right for the first element greater than the pivot + } + // Swap elements + nums[i], nums[j] = nums[j], nums[i] + } + // Swap the pivot to the boundary between the two subarrays + nums[i], nums[left] = nums[left], nums[i] + return i // Return the index of the pivot + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{partition} + /* Sentinel partition */ + func partition(nums: inout [Int], left: Int, right: Int) -> Int { + // Use nums[left] as the pivot + var i = left + var j = right + while i < j { + while i < j, nums[j] >= nums[left] { + j -= 1 // Search from right to left for the first element smaller than the pivot + } + while i < j, nums[i] <= nums[left] { + i += 1 // Search from left to right for the first element greater than the pivot + } + nums.swapAt(i, j) // Swap these two elements + } + nums.swapAt(i, left) // Swap the pivot to the boundary between the two subarrays + return i // Return the index of the pivot + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSort}-[func]{swap} + /* Swap elements */ + swap(nums, i, j) { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{partition} + /* Sentinel partition */ + partition(nums, left, right) { + // Use nums[left] as the pivot + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // Search from right to left for the first element smaller than the pivot + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // Search from left to right for the first element greater than the pivot + } + // Swap elements + this.swap(nums, i, j); // Swap these two elements + } + this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSort}-[func]{swap} + /* Swap elements */ + swap(nums: number[], i: number, j: number): void { + let tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{partition} + /* Sentinel partition */ + partition(nums: number[], left: number, right: number): number { + // Use nums[left] as the pivot + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j -= 1; // Search from right to left for the first element smaller than the pivot + } + while (i < j && nums[i] <= nums[left]) { + i += 1; // Search from left to right for the first element greater than the pivot + } + // Swap elements + this.swap(nums, i, j); // Swap these two elements + } + this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSort}-[func]{_swap} + /* Swap elements */ + void _swap(List nums, int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{QuickSort}-[func]{_partition} + /* Sentinel partition */ + int _partition(List nums, int left, int right) { + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot + _swap(nums, i, j); // Swap these two elements + } + _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSort}-[func]{partition} + /* Sentinel partition */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // Use nums[left] as the pivot + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // Search from right to left for the first element smaller than the pivot + } + while i < j && nums[i] <= nums[left] { + i += 1; // Search from left to right for the first element greater than the pivot + } + nums.swap(i, j); // Swap these two elements + } + nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays + i // Return the index of the pivot + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{swap} + /* Swap elements */ + void swap(int nums[], int i, int j) { + int tmp = nums[i]; + nums[i] = nums[j]; + nums[j] = tmp; + } - [class]{}-[func]{partition} + /* Sentinel partition */ + int partition(int nums[], int left, int right) { + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j--; // Search from right to left for the first element smaller than the pivot + } + while (i < j && nums[i] <= nums[left]) { + i++; // Search from left to right for the first element greater than the pivot + } + // Swap these two elements + swap(nums, i, j); + } + // Swap the pivot to the boundary between the two subarrays + swap(nums, i, left); + // Return the index of the pivot + return i; + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{swap} + /* Swap elements */ + fun swap(nums: IntArray, i: Int, j: Int) { + val temp = nums[i] + nums[i] = nums[j] + nums[j] = temp + } - [class]{}-[func]{partition} + /* Sentinel partition */ + fun partition(nums: IntArray, left: Int, right: Int): Int { + // Use nums[left] as the pivot + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) + i++ // Search from left to right for the first element greater than the pivot + swap(nums, i, j) // Swap these two elements + } + swap(nums, i, left) // Swap the pivot to the boundary between the two subarrays + return i // Return the index of the pivot + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSort}-[func]{partition} + ### Sentinel partition ### + def partition(nums, left, right) + # Use nums[left] as the pivot + i, j = left, right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # Search from right to left for the first element smaller than the pivot + end + while i < j && nums[i] <= nums[left] + i += 1 # Search from left to right for the first element greater than the pivot + end + # Swap elements + nums[i], nums[j] = nums[j], nums[i] + end + # Swap the pivot to the boundary between the two subarrays + nums[i], nums[left] = nums[left], nums[i] + i # Return the index of the pivot + end ``` -=== "Zig" +## 11.5.1   Algorithm Flow - ```zig title="quick_sort.zig" - [class]{QuickSort}-[func]{swap} +The overall flow of quick sort is shown in Figure 11-9. - [class]{QuickSort}-[func]{partition} - ``` +1. First, perform one "sentinel partitioning" on the original array to obtain the unsorted left sub-array and right sub-array. +2. Then, recursively perform "sentinel partitioning" on the left sub-array and right sub-array respectively. +3. Continue recursively until the sub-array length is 1, at which point sorting of the entire array is complete. -## 11.5.1   Algorithm process +![Quick sort flow](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" } -The overall process of quick sort is shown in Figure 11-9. - -1. First, perform a "pivot partitioning" on the original array to obtain the unsorted left and right sub-arrays. -2. Then, recursively perform "pivot partitioning" on the left and right sub-arrays separately. -3. Continue recursively until the length of sub-array is 1, thus completing the sorting of the entire array. - -![Quick sort process](quick_sort.assets/quick_sort_overview.png){ class="animation-figure" } - -

Figure 11-9   Quick sort process

+

Figure 11-9   Quick sort flow

=== "Python" @@ -211,7 +386,7 @@ The overall process of quick sort is shown in Figure 11-9. # Terminate recursion when subarray length is 1 if left >= right: return - # Partition + # Sentinel partition pivot = self.partition(nums, left, right) # Recursively process the left subarray and right subarray self.quick_sort(nums, left, pivot - 1) @@ -226,7 +401,7 @@ The overall process of quick sort is shown in Figure 11-9. // Terminate recursion when subarray length is 1 if (left >= right) return; - // Partition + // Sentinel partition int pivot = partition(nums, left, right); // Recursively process the left subarray and right subarray quickSort(nums, left, pivot - 1); @@ -242,7 +417,7 @@ The overall process of quick sort is shown in Figure 11-9. // Terminate recursion when subarray length is 1 if (left >= right) return; - // Partition + // Sentinel partition int pivot = partition(nums, left, right); // Recursively process the left subarray and right subarray quickSort(nums, left, pivot - 1); @@ -253,94 +428,191 @@ The overall process of quick sort is shown in Figure 11-9. === "C#" ```csharp title="quick_sort.cs" - [class]{quickSort}-[func]{QuickSort} + /* Quick sort */ + void QuickSort(int[] nums, int left, int right) { + // Terminate recursion when subarray length is 1 + if (left >= right) + return; + // Sentinel partition + int pivot = Partition(nums, left, right); + // Recursively process the left subarray and right subarray + QuickSort(nums, left, pivot - 1); + QuickSort(nums, pivot + 1, right); + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSort}-[func]{quickSort} + /* Quick sort */ + func (q *quickSort) quickSort(nums []int, left, right int) { + // Terminate recursion when subarray length is 1 + if left >= right { + return + } + // Sentinel partition + pivot := q.partition(nums, left, right) + // Recursively process the left subarray and right subarray + q.quickSort(nums, left, pivot-1) + q.quickSort(nums, pivot+1, right) + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{quickSort} + /* Quick sort */ + func quickSort(nums: inout [Int], left: Int, right: Int) { + // Terminate recursion when subarray length is 1 + if left >= right { + return + } + // Sentinel partition + let pivot = partition(nums: &nums, left: left, right: right) + // Recursively process the left subarray and right subarray + quickSort(nums: &nums, left: left, right: pivot - 1) + quickSort(nums: &nums, left: pivot + 1, right: right) + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSort}-[func]{quickSort} + /* Quick sort */ + quickSort(nums, left, right) { + // Terminate recursion when subarray length is 1 + if (left >= right) return; + // Sentinel partition + const pivot = this.partition(nums, left, right); + // Recursively process the left subarray and right subarray + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSort}-[func]{quickSort} + /* Quick sort */ + quickSort(nums: number[], left: number, right: number): void { + // Terminate recursion when subarray length is 1 + if (left >= right) { + return; + } + // Sentinel partition + const pivot = this.partition(nums, left, right); + // Recursively process the left subarray and right subarray + this.quickSort(nums, left, pivot - 1); + this.quickSort(nums, pivot + 1, right); + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSort}-[func]{quickSort} + /* Quick sort */ + void quickSort(List nums, int left, int right) { + // Terminate recursion when subarray length is 1 + if (left >= right) return; + // Sentinel partition + int pivot = _partition(nums, left, right); + // Recursively process the left subarray and right subarray + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSort}-[func]{quick_sort} + /* Quick sort */ + pub fn quick_sort(left: i32, right: i32, nums: &mut [i32]) { + // Terminate recursion when subarray length is 1 + if left >= right { + return; + } + // Sentinel partition + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // Recursively process the left subarray and right subarray + Self::quick_sort(left, pivot - 1, nums); + Self::quick_sort(pivot + 1, right, nums); + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{quickSort} + /* Quick sort */ + void quickSort(int nums[], int left, int right) { + // Terminate recursion when subarray length is 1 + if (left >= right) { + return; + } + // Sentinel partition + int pivot = partition(nums, left, right); + // Recursively process the left subarray and right subarray + quickSort(nums, left, pivot - 1); + quickSort(nums, pivot + 1, right); + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{quickSort} + /* Quick sort */ + fun quickSort(nums: IntArray, left: Int, right: Int) { + // Terminate recursion when subarray length is 1 + if (left >= right) return + // Sentinel partition + val pivot = partition(nums, left, right) + // Recursively process the left subarray and right subarray + quickSort(nums, left, pivot - 1) + quickSort(nums, pivot + 1, right) + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSort}-[func]{quick_sort} + ### Quick sort class ### + def quick_sort(nums, left, right) + # Recurse when subarray length is not 1 + if left < right + # Sentinel partition + pivot = partition(nums, left, right) + # Recursively process the left subarray and right subarray + quick_sort(nums, left, pivot - 1) + quick_sort(nums, pivot + 1, right) + end + nums + end ``` -=== "Zig" +## 11.5.2   Algorithm Characteristics - ```zig title="quick_sort.zig" - [class]{QuickSort}-[func]{quickSort} - ``` +- **Time complexity of $O(n \log n)$, non-adaptive sorting**: In the average case, the number of recursive levels of sentinel partitioning is $\log n$, and the total number of loops at each level is $n$, using $O(n \log n)$ time overall. In the worst case, each round of sentinel partitioning divides an array of length $n$ into two sub-arrays of length $0$ and $n - 1$, at which point the number of recursive levels reaches $n$, the number of loops at each level is $n$, and the total time used is $O(n^2)$. +- **Space complexity of $O(n)$, in-place sorting**: In the case where the input array is completely reversed, the worst recursive depth reaches $n$, using $O(n)$ stack frame space. The sorting operation is performed on the original array without the aid of an additional array. +- **Non-stable sorting**: In the last step of sentinel partitioning, the pivot may be swapped to the right of equal elements. -## 11.5.2   Algorithm features +## 11.5.3   Why Is Quick Sort Fast -- **Time complexity of $O(n \log n)$, non-adaptive sorting**: In average cases, the recursive levels of pivot partitioning are $\log n$, and the total number of loops per level is $n$, using $O(n \log n)$ time overall. In the worst case, each round of pivot partitioning divides an array of length $n$ into two sub-arrays of lengths $0$ and $n - 1$, when the number of recursive levels reaches $n$, the number of loops in each level is $n$, and the total time used is $O(n^2)$. -- **Space complexity of $O(n)$, in-place sorting**: In the case where the input array is completely reversed, the worst recursive depth reaches $n$, using $O(n)$ stack frame space. The sorting operation is performed on the original array without the aid of additional arrays. -- **Non-stable sorting**: In the final step of pivot partitioning, the pivot may be swapped to the right of equal elements. +From the name, we can see that quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as "merge sort" and "heap sort", quick sort is usually more efficient, mainly for the following reasons. -## 11.5.3   Why is quick sort fast +- **The probability of the worst case occurring is very low**: Although the worst-case time complexity of quick sort is $O(n^2)$, which is not as stable as merge sort, in the vast majority of cases, quick sort can run with a time complexity of $O(n \log n)$. +- **High cache utilization**: When performing sentinel partitioning operations, the system can load the entire sub-array into the cache, so element access efficiency is relatively high. Algorithms like "heap sort" require jump-style access to elements, thus lacking this characteristic. +- **Small constant coefficient of complexity**: Among the three algorithms mentioned above, quick sort has the smallest total number of operations such as comparisons, assignments, and swaps. This is similar to the reason why "insertion sort" is faster than "bubble sort". -As the name suggests, quick sort should have certain advantages in terms of efficiency. Although the average time complexity of quick sort is the same as that of "merge sort" and "heap sort," it is generally more efficient for the following reasons. +## 11.5.4   Pivot Optimization -- **Low probability of worst-case scenarios**: Although the worst time complexity of quick sort is $O(n^2)$, less stable than merge sort, in most cases, quick sort can operate under a time complexity of $O(n \log n)$. -- **High cache utilization**: During the pivot partitioning operation, the system can load the entire sub-array into the cache, thus accessing elements more efficiently. In contrast, algorithms like "heap sort" need to access elements in a jumping manner, lacking this feature. -- **Small constant coefficient of complexity**: Among the three algorithms mentioned above, quick sort has the least total number of operations such as comparisons, assignments, and swaps. This is similar to why "insertion sort" is faster than "bubble sort." +**Quick sort may have reduced time efficiency for certain inputs**. Take an extreme example: suppose the input array is completely reversed. Since we select the leftmost element as the pivot, after sentinel partitioning is complete, the pivot is swapped to the rightmost end of the array, causing the left sub-array length to be $n - 1$ and the right sub-array length to be $0$. If we recurse down like this, each round of sentinel partitioning will have a sub-array length of $0$, the divide-and-conquer strategy fails, and quick sort degrades to a form approximate to "bubble sort". -## 11.5.4   Pivot optimization +To avoid this situation as much as possible, **we can optimize the pivot selection strategy in sentinel partitioning**. For example, we can randomly select an element as the pivot. However, if luck is not good and we select a non-ideal pivot every time, efficiency is still not satisfactory. -**Quick sort's time efficiency may degrade under certain inputs**. For example, if the input array is completely reversed, since we select the leftmost element as the pivot, after the pivot partitioning, the pivot is swapped to the array's right end, causing the left sub-array length to be $n - 1$ and the right sub-array length to be $0$. Continuing this way, each round of pivot partitioning will have a sub-array length of $0$, and the divide-and-conquer strategy fails, degrading quick sort to a form similar to "bubble sort." +It should be noted that programming languages usually generate "pseudo-random numbers". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade. -To avoid this situation, **we can optimize the pivot selection strategy in the pivot partitioning**. For instance, we can randomly select an element as the pivot. However, if luck is not on our side, and we consistently select suboptimal pivots, the efficiency is still not satisfactory. +For further improvement, we can select three candidate elements in the array (usually the first, last, and middle elements of the array), **and use the median of these three candidate elements as the pivot**. In this way, the probability that the pivot is "neither too small nor too large" will be greatly increased. Of course, we can also select more candidate elements to further improve the robustness of the algorithm. After adopting this method, the probability of time complexity degrading to $O(n^2)$ is greatly reduced. -It's important to note that programming languages usually generate "pseudo-random numbers". If we construct a specific test case for a pseudo-random number sequence, the efficiency of quick sort may still degrade. - -For further improvement, we can select three candidate elements (usually the first, last, and midpoint elements of the array), **and use the median of these three candidate elements as the pivot**. This way, the probability that the pivot is "neither too small nor too large" will be greatly increased. Of course, we can also select more candidate elements to further enhance robustness of the algorithm. With this method, the probability of the time complexity degrading to $O(n^2)$ is greatly reduced. - -Sample code is as follows: +Example code is as follows: === "Python" @@ -355,7 +627,7 @@ Sample code is as follows: return right def partition(self, nums: list[int], left: int, right: int) -> int: - """Partition (median of three)""" + """Sentinel partition (median of three)""" # Use nums[left] as the pivot med = self.median_three(nums, left, (left + right) // 2, right) # Swap the median to the array's leftmost position @@ -387,23 +659,23 @@ Sample code is as follows: return right; } - /* Partition (median of three) */ + /* Sentinel partition (median of three) */ int partition(vector &nums, int left, int right) { // Select the median of three candidate elements int med = medianThree(nums, left, (left + right) / 2, right); // Swap the median to the array's leftmost position - swap(nums, left, med); + swap(nums[left], nums[med]); // Use nums[left] as the pivot int i = left, j = right; while (i < j) { while (i < j && nums[j] >= nums[left]) - j--; // Search from right to left for the first element smaller than the pivot + j--; // Search from right to left for the first element smaller than the pivot while (i < j && nums[i] <= nums[left]) - i++; // Search from left to right for the first element greater than the pivot - swap(nums, i, j); // Swap these two elements + i++; // Search from left to right for the first element greater than the pivot + swap(nums[i], nums[j]); // Swap these two elements } - swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays - return i; // Return the index of the pivot + swap(nums[i], nums[left]); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot } ``` @@ -420,7 +692,7 @@ Sample code is as follows: return right; } - /* Partition (median of three) */ + /* Sentinel partition (median of three) */ int partition(int[] nums, int left, int right) { // Select the median of three candidate elements int med = medianThree(nums, left, (left + right) / 2, right); @@ -443,106 +715,377 @@ Sample code is as follows: === "C#" ```csharp title="quick_sort.cs" - [class]{QuickSortMedian}-[func]{MedianThree} + /* Select the median of three candidate elements */ + int MedianThree(int[] nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m is between l and r + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l is between m and r + return right; + } - [class]{QuickSortMedian}-[func]{Partition} + /* Sentinel partition (median of three) */ + int Partition(int[] nums, int left, int right) { + // Select the median of three candidate elements + int med = MedianThree(nums, left, (left + right) / 2, right); + // Swap the median to the array's leftmost position + Swap(nums, left, med); + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) + i++; // Search from left to right for the first element greater than the pivot + Swap(nums, i, j); // Swap these two elements + } + Swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSortMedian}-[func]{medianThree} + /* Select the median of three candidate elements */ + func (q *quickSortMedian) medianThree(nums []int, left, mid, right int) int { + l, m, r := nums[left], nums[mid], nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m is between l and r + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l is between m and r + } + return right + } - [class]{quickSortMedian}-[func]{partition} + /* Sentinel partition (median of three) */ + func (q *quickSortMedian) partition(nums []int, left, right int) int { + // Use nums[left] as the pivot + med := q.medianThree(nums, left, (left+right)/2, right) + // Swap the median to the array's leftmost position + nums[left], nums[med] = nums[med], nums[left] + // Use nums[left] as the pivot + i, j := left, right + for i < j { + for i < j && nums[j] >= nums[left] { + j-- // Search from right to left for the first element smaller than the pivot + } + for i < j && nums[i] <= nums[left] { + i++ // Search from left to right for the first element greater than the pivot + } + // Swap elements + nums[i], nums[j] = nums[j], nums[i] + } + // Swap the pivot to the boundary between the two subarrays + nums[i], nums[left] = nums[left], nums[i] + return i // Return the index of the pivot + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{medianThree} + /* Select the median of three candidate elements */ + func medianThree(nums: [Int], left: Int, mid: Int, right: Int) -> Int { + let l = nums[left] + let m = nums[mid] + let r = nums[right] + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid // m is between l and r + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left // l is between m and r + } + return right + } - [class]{}-[func]{partitionMedian} + /* Sentinel partition (median of three) */ + func partitionMedian(nums: inout [Int], left: Int, right: Int) -> Int { + // Select the median of three candidate elements + let med = medianThree(nums: nums, left: left, mid: left + (right - left) / 2, right: right) + // Swap the median to the array's leftmost position + nums.swapAt(left, med) + return partition(nums: &nums, left: left, right: right) + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSortMedian}-[func]{medianThree} + /* Select the median of three candidate elements */ + medianThree(nums, left, mid, right) { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m is between l and r + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l is between m and r + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } - [class]{QuickSortMedian}-[func]{partition} + /* Sentinel partition (median of three) */ + partition(nums, left, right) { + // Select the median of three candidate elements + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // Swap the median to the array's leftmost position + this.swap(nums, left, med); + // Use nums[left] as the pivot + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot + this.swap(nums, i, j); // Swap these two elements + } + this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSortMedian}-[func]{medianThree} + /* Select the median of three candidate elements */ + medianThree( + nums: number[], + left: number, + mid: number, + right: number + ): number { + let l = nums[left], + m = nums[mid], + r = nums[right]; + // m is between l and r + if ((l <= m && m <= r) || (r <= m && m <= l)) return mid; + // l is between m and r + if ((m <= l && l <= r) || (r <= l && l <= m)) return left; + return right; + } - [class]{QuickSortMedian}-[func]{partition} + /* Sentinel partition (median of three) */ + partition(nums: number[], left: number, right: number): number { + // Select the median of three candidate elements + let med = this.medianThree( + nums, + left, + Math.floor((left + right) / 2), + right + ); + // Swap the median to the array's leftmost position + this.swap(nums, left, med); + // Use nums[left] as the pivot + let i = left, + j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) { + j--; // Search from right to left for the first element smaller than the pivot + } + while (i < j && nums[i] <= nums[left]) { + i++; // Search from left to right for the first element greater than the pivot + } + this.swap(nums, i, j); // Swap these two elements + } + this.swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSortMedian}-[func]{_medianThree} + /* Select the median of three candidate elements */ + int _medianThree(List nums, int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m is between l and r + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l is between m and r + return right; + } - [class]{QuickSortMedian}-[func]{_partition} + /* Sentinel partition (median of three) */ + int _partition(List nums, int left, int right) { + // Select the median of three candidate elements + int med = _medianThree(nums, left, (left + right) ~/ 2, right); + // Swap the median to the array's leftmost position + _swap(nums, left, med); + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) i++; // Search from left to right for the first element greater than the pivot + _swap(nums, i, j); // Swap these two elements + } + _swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSortMedian}-[func]{median_three} + /* Select the median of three candidate elements */ + fn median_three(nums: &mut [i32], left: usize, mid: usize, right: usize) -> usize { + let (l, m, r) = (nums[left], nums[mid], nums[right]); + if (l <= m && m <= r) || (r <= m && m <= l) { + return mid; // m is between l and r + } + if (m <= l && l <= r) || (r <= l && l <= m) { + return left; // l is between m and r + } + right + } - [class]{QuickSortMedian}-[func]{partition} + /* Sentinel partition (median of three) */ + fn partition(nums: &mut [i32], left: usize, right: usize) -> usize { + // Select the median of three candidate elements + let med = Self::median_three(nums, left, (left + right) / 2, right); + // Swap the median to the array's leftmost position + nums.swap(left, med); + // Use nums[left] as the pivot + let (mut i, mut j) = (left, right); + while i < j { + while i < j && nums[j] >= nums[left] { + j -= 1; // Search from right to left for the first element smaller than the pivot + } + while i < j && nums[i] <= nums[left] { + i += 1; // Search from left to right for the first element greater than the pivot + } + nums.swap(i, j); // Swap these two elements + } + nums.swap(i, left); // Swap the pivot to the boundary between the two subarrays + i // Return the index of the pivot + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{medianThree} + /* Select the median of three candidate elements */ + int medianThree(int nums[], int left, int mid, int right) { + int l = nums[left], m = nums[mid], r = nums[right]; + if ((l <= m && m <= r) || (r <= m && m <= l)) + return mid; // m is between l and r + if ((m <= l && l <= r) || (r <= l && l <= m)) + return left; // l is between m and r + return right; + } - [class]{}-[func]{partitionMedian} + /* Sentinel partition (median of three) */ + int partitionMedian(int nums[], int left, int right) { + // Select the median of three candidate elements + int med = medianThree(nums, left, (left + right) / 2, right); + // Swap the median to the array's leftmost position + swap(nums, left, med); + // Use nums[left] as the pivot + int i = left, j = right; + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j--; // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) + i++; // Search from left to right for the first element greater than the pivot + swap(nums, i, j); // Swap these two elements + } + swap(nums, i, left); // Swap the pivot to the boundary between the two subarrays + return i; // Return the index of the pivot + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{medianThree} + /* Select the median of three candidate elements */ + fun medianThree(nums: IntArray, left: Int, mid: Int, right: Int): Int { + val l = nums[left] + val m = nums[mid] + val r = nums[right] + if ((m in l..r) || (m in r..l)) + return mid // m is between l and r + if ((l in m..r) || (l in r..m)) + return left // l is between m and r + return right + } - [class]{}-[func]{partitionMedian} + /* Sentinel partition (median of three) */ + fun partitionMedian(nums: IntArray, left: Int, right: Int): Int { + // Select the median of three candidate elements + val med = medianThree(nums, left, (left + right) / 2, right) + // Swap the median to the array's leftmost position + swap(nums, left, med) + // Use nums[left] as the pivot + var i = left + var j = right + while (i < j) { + while (i < j && nums[j] >= nums[left]) + j-- // Search from right to left for the first element smaller than the pivot + while (i < j && nums[i] <= nums[left]) + i++ // Search from left to right for the first element greater than the pivot + swap(nums, i, j) // Swap these two elements + } + swap(nums, i, left) // Swap the pivot to the boundary between the two subarrays + return i // Return the index of the pivot + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSortMedian}-[func]{median_three} + ### Select median of three candidate elements ### + def median_three(nums, left, mid, right) + # Select the median of three candidate elements + _l, _m, _r = nums[left], nums[mid], nums[right] + # m is between l and r + return mid if (_l <= _m && _m <= _r) || (_r <= _m && _m <= _l) + # l is between m and r + return left if (_m <= _l && _l <= _r) || (_r <= _l && _l <= _m) + return right + end - [class]{QuickSortMedian}-[func]{partition} + ### Sentinel partition (median of three) ### + def partition(nums, left, right) + ### Use nums[left] as pivot + med = median_three(nums, left, (left + right) / 2, right) + # Swap median to leftmost position of array + nums[left], nums[med] = nums[med], nums[left] + i, j = left, right + while i < j + while i < j && nums[j] >= nums[left] + j -= 1 # Search from right to left for the first element smaller than the pivot + end + while i < j && nums[i] <= nums[left] + i += 1 # Search from left to right for the first element greater than the pivot + end + # Swap elements + nums[i], nums[j] = nums[j], nums[i] + end + # Swap the pivot to the boundary between the two subarrays + nums[i], nums[left] = nums[left], nums[i] + i # Return the index of the pivot + end ``` -=== "Zig" +## 11.5.5   Recursive Depth Optimization - ```zig title="quick_sort.zig" - [class]{QuickSortMedian}-[func]{medianThree} +**For certain inputs, quick sort may occupy more space**. Taking a completely ordered input array as an example, let the length of the sub-array in recursion be $m$. Each round of sentinel partitioning will produce a left sub-array of length $0$ and a right sub-array of length $m - 1$, which means that the problem scale reduced per recursive call is very small (only one element is reduced), and the height of the recursion tree will reach $n - 1$, at which point $O(n)$ size of stack frame space is required. - [class]{QuickSortMedian}-[func]{partition} - ``` - -## 11.5.5   Tail recursion optimization - -**Under certain inputs, quick sort may occupy more space**. For example, consider a completely ordered input array. Let the length of the sub-array in the recursion be $m$. In each round of pivot partitioning, a left sub-array of length $0$ and a right sub-array of length $m - 1$ are produced. This means that the problem size is reduced by only one element per recursive call, resulting in a very small reduction at each level of recursion. -As a result, the height of the recursion tree can reach $n − 1$ , which requires $O(n)$ of stack frame space. - -To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of pivot sorting, **and only recursively sort the shorter sub-array**. Since the length of the shorter sub-array will not exceed $n / 2$, this method ensures that the recursion depth does not exceed $\log n$, thus optimizing the worst space complexity to $O(\log n)$. The code is as follows: +To prevent the accumulation of stack frame space, we can compare the lengths of the two sub-arrays after each round of sentinel sorting is complete, **and only recurse on the shorter sub-array**. Since the length of the shorter sub-array will not exceed $n / 2$, this method can ensure that the recursion depth does not exceed $\log n$, thus optimizing the worst-case space complexity to $O(\log n)$. The code is as follows: === "Python" ```python title="quick_sort.py" def quick_sort(self, nums: list[int], left: int, right: int): - """Quick sort (tail recursion optimization)""" + """Quick sort (recursion depth optimization)""" # Terminate when subarray length is 1 while left < right: - # Partition operation + # Sentinel partition operation pivot = self.partition(nums, left, right) # Perform quick sort on the shorter of the two subarrays if pivot - left < right - pivot: @@ -556,11 +1099,11 @@ To prevent the accumulation of stack frame space, we can compare the lengths of === "C++" ```cpp title="quick_sort.cpp" - /* Quick sort (tail recursion optimization) */ + /* Quick sort (recursion depth optimization) */ void quickSort(vector &nums, int left, int right) { // Terminate when subarray length is 1 while (left < right) { - // Partition operation + // Sentinel partition operation int pivot = partition(nums, left, right); // Perform quick sort on the shorter of the two subarrays if (pivot - left < right - pivot) { @@ -577,11 +1120,11 @@ To prevent the accumulation of stack frame space, we can compare the lengths of === "Java" ```java title="quick_sort.java" - /* Quick sort (tail recursion optimization) */ + /* Quick sort (recursion depth optimization) */ void quickSort(int[] nums, int left, int right) { // Terminate when subarray length is 1 while (left < right) { - // Partition operation + // Sentinel partition operation int pivot = partition(nums, left, right); // Perform quick sort on the shorter of the two subarrays if (pivot - left < right - pivot) { @@ -598,65 +1141,217 @@ To prevent the accumulation of stack frame space, we can compare the lengths of === "C#" ```csharp title="quick_sort.cs" - [class]{QuickSortTailCall}-[func]{QuickSort} + /* Quick sort (recursion depth optimization) */ + void QuickSort(int[] nums, int left, int right) { + // Terminate when subarray length is 1 + while (left < right) { + // Sentinel partition operation + int pivot = Partition(nums, left, right); + // Perform quick sort on the shorter of the two subarrays + if (pivot - left < right - pivot) { + QuickSort(nums, left, pivot - 1); // Recursively sort the left subarray + left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right] + } else { + QuickSort(nums, pivot + 1, right); // Recursively sort the right subarray + right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "Go" ```go title="quick_sort.go" - [class]{quickSortTailCall}-[func]{quickSort} + /* Quick sort (recursion depth optimization) */ + func (q *quickSortTailCall) quickSort(nums []int, left, right int) { + // Terminate when subarray length is 1 + for left < right { + // Sentinel partition operation + pivot := q.partition(nums, left, right) + // Perform quick sort on the shorter of the two subarrays + if pivot-left < right-pivot { + q.quickSort(nums, left, pivot-1) // Recursively sort the left subarray + left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right] + } else { + q.quickSort(nums, pivot+1, right) // Recursively sort the right subarray + right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "Swift" ```swift title="quick_sort.swift" - [class]{}-[func]{quickSortTailCall} + /* Quick sort (recursion depth optimization) */ + func quickSortTailCall(nums: inout [Int], left: Int, right: Int) { + var left = left + var right = right + // Terminate when subarray length is 1 + while left < right { + // Sentinel partition operation + let pivot = partition(nums: &nums, left: left, right: right) + // Perform quick sort on the shorter of the two subarrays + if (pivot - left) < (right - pivot) { + quickSortTailCall(nums: &nums, left: left, right: pivot - 1) // Recursively sort the left subarray + left = pivot + 1 // Remaining unsorted interval is [pivot + 1, right] + } else { + quickSortTailCall(nums: &nums, left: pivot + 1, right: right) // Recursively sort the right subarray + right = pivot - 1 // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "JS" ```javascript title="quick_sort.js" - [class]{QuickSortTailCall}-[func]{quickSort} + /* Quick sort (recursion depth optimization) */ + quickSort(nums, left, right) { + // Terminate when subarray length is 1 + while (left < right) { + // Sentinel partition operation + let pivot = this.partition(nums, left, right); + // Perform quick sort on the shorter of the two subarrays + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray + left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray + right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "TS" ```typescript title="quick_sort.ts" - [class]{QuickSortTailCall}-[func]{quickSort} + /* Quick sort (recursion depth optimization) */ + quickSort(nums: number[], left: number, right: number): void { + // Terminate when subarray length is 1 + while (left < right) { + // Sentinel partition operation + let pivot = this.partition(nums, left, right); + // Perform quick sort on the shorter of the two subarrays + if (pivot - left < right - pivot) { + this.quickSort(nums, left, pivot - 1); // Recursively sort the left subarray + left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right] + } else { + this.quickSort(nums, pivot + 1, right); // Recursively sort the right subarray + right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "Dart" ```dart title="quick_sort.dart" - [class]{QuickSortTailCall}-[func]{quickSort} + /* Quick sort (recursion depth optimization) */ + void quickSort(List nums, int left, int right) { + // Terminate when subarray length is 1 + while (left < right) { + // Sentinel partition operation + int pivot = _partition(nums, left, right); + // Perform quick sort on the shorter of the two subarrays + if (pivot - left < right - pivot) { + quickSort(nums, left, pivot - 1); // Recursively sort the left subarray + left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, right); // Recursively sort the right subarray + right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "Rust" ```rust title="quick_sort.rs" - [class]{QuickSortTailCall}-[func]{quick_sort} + /* Quick sort (recursion depth optimization) */ + pub fn quick_sort(mut left: i32, mut right: i32, nums: &mut [i32]) { + // Terminate when subarray length is 1 + while left < right { + // Sentinel partition operation + let pivot = Self::partition(nums, left as usize, right as usize) as i32; + // Perform quick sort on the shorter of the two subarrays + if pivot - left < right - pivot { + Self::quick_sort(left, pivot - 1, nums); // Recursively sort the left subarray + left = pivot + 1; // Remaining unsorted interval is [pivot + 1, right] + } else { + Self::quick_sort(pivot + 1, right, nums); // Recursively sort the right subarray + right = pivot - 1; // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "C" ```c title="quick_sort.c" - [class]{}-[func]{quickSortTailCall} + /* Quick sort (recursion depth optimization) */ + void quickSortTailCall(int nums[], int left, int right) { + // Terminate when subarray length is 1 + while (left < right) { + // Sentinel partition operation + int pivot = partition(nums, left, right); + // Perform quick sort on the shorter of the two subarrays + if (pivot - left < right - pivot) { + // Recursively sort the left subarray + quickSortTailCall(nums, left, pivot - 1); + // Remaining unsorted interval is [pivot + 1, right] + left = pivot + 1; + } else { + // Recursively sort the right subarray + quickSortTailCall(nums, pivot + 1, right); + // Remaining unsorted interval is [left, pivot - 1] + right = pivot - 1; + } + } + } ``` === "Kotlin" ```kotlin title="quick_sort.kt" - [class]{}-[func]{quickSortTailCall} + /* Quick sort (recursion depth optimization) */ + fun quickSortTailCall(nums: IntArray, left: Int, right: Int) { + // Terminate when subarray length is 1 + var l = left + var r = right + while (l < r) { + // Sentinel partition operation + val pivot = partition(nums, l, r) + // Perform quick sort on the shorter of the two subarrays + if (pivot - l < r - pivot) { + quickSort(nums, l, pivot - 1) // Recursively sort the left subarray + l = pivot + 1 // Remaining unsorted interval is [pivot + 1, right] + } else { + quickSort(nums, pivot + 1, r) // Recursively sort the right subarray + r = pivot - 1 // Remaining unsorted interval is [left, pivot - 1] + } + } + } ``` === "Ruby" ```ruby title="quick_sort.rb" - [class]{QuickSortTailCall}-[func]{quick_sort} - ``` - -=== "Zig" - - ```zig title="quick_sort.zig" - [class]{QuickSortTailCall}-[func]{quickSort} + ### Quick sort (recursion depth optimization) ### + def quick_sort(nums, left, right) + # Recurse when subarray length is not 1 + while left < right + # Sentinel partition + pivot = partition(nums, left, right) + # Perform quick sort on the shorter of the two subarrays + if pivot - left < right - pivot + quick_sort(nums, left, pivot - 1) + left = pivot + 1 # Remaining unsorted interval is [pivot + 1, right] + else + quick_sort(nums, pivot + 1, right) + right = pivot - 1 # Remaining unsorted interval is [left, pivot - 1] + end + end + end ``` diff --git a/en/docs/chapter_sorting/radix_sort.md b/en/docs/chapter_sorting/radix_sort.md index c43ba76e4..e24c04bc4 100644 --- a/en/docs/chapter_sorting/radix_sort.md +++ b/en/docs/chapter_sorting/radix_sort.md @@ -2,33 +2,33 @@ comments: true --- -# 11.10   Radix sort +# 11.10   Radix Sort -The previous section introduced counting sort, which is suitable for scenarios where the data size $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, where each ID is an $8$-digit number. This means the data range $m = 10^8$ is very large. Using counting sort in this case would require significant memory space. Radix sort can avoid this situation. +The previous section introduced counting sort, which is suitable for situations where the data volume $n$ is large but the data range $m$ is small. Suppose we need to sort $n = 10^6$ student IDs, and the student ID is an 8-digit number, which means the data range $m = 10^8$ is very large. Using counting sort would require allocating a large amount of memory space, whereas radix sort can avoid this situation. -Radix sort shares the same core concept as counting sort, which also sorts by counting the frequency of elements. Meanwhile, radix sort builds upon this by utilizing the progressive relationship between the digits of numbers. It processes and sorts the digits one at a time, achieving the final sorted order. +Radix sort (radix sort) has a core idea consistent with counting sort, which also achieves sorting by counting quantities. Building on this, radix sort utilizes the progressive relationship between the digits of numbers, sorting each digit in turn to obtain the final sorting result. -## 11.10.1   Algorithm process +## 11.10.1   Algorithm Flow -Taking the student ID data as an example, assume the least significant digit is the $1^{st}$ and the most significant is the $8^{th}$, the radix sort process is illustrated in Figure 11-18. +Taking student ID data as an example, assume the lowest digit is the $1$st digit and the highest digit is the $8$th digit. The flow of radix sort is shown in Figure 11-18. -1. Initialize digit $k = 1$. -2. Perform "counting sort" on the $k^{th}$ digit of the student IDs. After completion, the data will be sorted from smallest to largest based on the $k^{th}$ digit. -3. Increment $k$ by $1$, then return to step `2.` and continue iterating until all digits have been sorted, at which point the process ends. +1. Initialize the digit $k = 1$. +2. Perform "counting sort" on the $k$th digit of the student IDs. After completion, the data will be sorted from smallest to largest according to the $k$th digit. +3. Increase $k$ by $1$, then return to step `2.` and continue iterating until all digits are sorted, at which point the process ends. -![Radix sort algorithm process](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" } +![Radix sort algorithm flow](radix_sort.assets/radix_sort_overview.png){ class="animation-figure" } -

Figure 11-18   Radix sort algorithm process

+

Figure 11-18   Radix sort algorithm flow

-Below we dissect the code implementation. For a number $x$ in base $d$, to obtain its $k^{th}$ digit $x_k$, the following calculation formula can be used: +Below we analyze the code implementation. For a $d$-base number $x$, to get its $k$th digit $x_k$, the following calculation formula can be used: $$ x_k = \lfloor\frac{x}{d^{k-1}}\rfloor \bmod d $$ -Where $\lfloor a \rfloor$ denotes rounding down the floating point number $a$, and $\bmod \: d$ denotes taking the modulus of $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. +Where $\lfloor a \rfloor$ denotes rounding down the floating-point number $a$, and $\bmod \: d$ denotes taking the modulo (remainder) with respect to $d$. For student ID data, $d = 10$ and $k \in [1, 8]$. -Additionally, we need to slightly modify the counting sort code to allow sorting based on the $k^{th}$ digit: +Additionally, we need to slightly modify the counting sort code to make it sort based on the $k$th digit of the number: === "Python" @@ -183,121 +183,547 @@ Additionally, we need to slightly modify the counting sort code to allow sorting === "C#" ```csharp title="radix_sort.cs" - [class]{radix_sort}-[func]{Digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + int Digit(int num, int exp) { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return (num / exp) % 10; + } - [class]{radix_sort}-[func]{CountingSortDigit} + /* Counting sort (based on nums k-th digit) */ + void CountingSortDigit(int[] nums, int exp) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + int[] counter = new int[10]; + int n = nums.Length; + // Count the occurrence of digits 0~9 + for (int i = 0; i < n; i++) { + int d = Digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d + counter[d]++; // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d]--; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for (int i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{radix_sort}-[func]{RadixSort} + /* Radix sort */ + void RadixSort(int[] nums) { + // Get the maximum element of the array, used to determine the maximum number of digits + int m = int.MinValue; + foreach (int num in nums) { + if (num > m) m = num; + } + // Traverse from the lowest to the highest digit + for (int exp = 1; exp <= m; exp *= 10) { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + CountingSortDigit(nums, exp); + } + } ``` === "Go" ```go title="radix_sort.go" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + func digit(num, exp int) int { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + func countingSortDigit(nums []int, exp int) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + counter := make([]int, 10) + n := len(nums) + // Count the occurrence of digits 0~9 + for i := 0; i < n; i++ { + d := digit(nums[i], exp) // Get the k-th digit of nums[i], noted as d + counter[d]++ // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for i := 1; i < 10; i++ { + counter[i] += counter[i-1] + } + // Traverse in reverse, based on bucket statistics, place each element into res + res := make([]int, n) + for i := n - 1; i >= 0; i-- { + d := digit(nums[i], exp) + j := counter[d] - 1 // Get the index j for d in the array + res[j] = nums[i] // Place the current element at index j + counter[d]-- // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for i := 0; i < n; i++ { + nums[i] = res[i] + } + } - [class]{}-[func]{radixSort} + /* Radix sort */ + func radixSort(nums []int) { + // Get the maximum element of the array, used to determine the maximum number of digits + max := math.MinInt + for _, num := range nums { + if num > max { + max = num + } + } + // Traverse from the lowest to the highest digit + for exp := 1; max >= exp; exp *= 10 { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, exp) + } + } ``` === "Swift" ```swift title="radix_sort.swift" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + func digit(num: Int, exp: Int) -> Int { + // Passing exp instead of k can avoid repeated expensive exponentiation here + (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + func countingSortDigit(nums: inout [Int], exp: Int) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + var counter = Array(repeating: 0, count: 10) + // Count the occurrence of digits 0~9 + for i in nums.indices { + let d = digit(num: nums[i], exp: exp) // Get the k-th digit of nums[i], noted as d + counter[d] += 1 // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for i in 1 ..< 10 { + counter[i] += counter[i - 1] + } + // Traverse in reverse, based on bucket statistics, place each element into res + var res = Array(repeating: 0, count: nums.count) + for i in nums.indices.reversed() { + let d = digit(num: nums[i], exp: exp) + let j = counter[d] - 1 // Get the index j for d in the array + res[j] = nums[i] // Place the current element at index j + counter[d] -= 1 // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for i in nums.indices { + nums[i] = res[i] + } + } - [class]{}-[func]{radixSort} + /* Radix sort */ + func radixSort(nums: inout [Int]) { + // Get the maximum element of the array, used to determine the maximum number of digits + var m = Int.min + for num in nums { + if num > m { + m = num + } + } + // Traverse from the lowest to the highest digit + for exp in sequence(first: 1, next: { m >= ($0 * 10) ? $0 * 10 : nil }) { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums: &nums, exp: exp) + } + } ``` === "JS" ```javascript title="radix_sort.js" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + function digit(num, exp) { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + function countingSortDigit(nums, exp) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + const counter = new Array(10).fill(0); + const n = nums.length; + // Count the occurrence of digits 0~9 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d + counter[d]++; // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d]--; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* Radix sort */ + function radixSort(nums) { + // Get the maximum element of the array, used to determine the maximum number of digits + let m = Math.max(... nums); + // Traverse from the lowest to the highest digit + for (let exp = 1; exp <= m; exp *= 10) { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "TS" ```typescript title="radix_sort.ts" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + function digit(num: number, exp: number): number { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return Math.floor(num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + function countingSortDigit(nums: number[], exp: number): void { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + const counter = new Array(10).fill(0); + const n = nums.length; + // Count the occurrence of digits 0~9 + for (let i = 0; i < n; i++) { + const d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d + counter[d]++; // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for (let i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d]--; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for (let i = 0; i < n; i++) { + nums[i] = res[i]; + } + } - [class]{}-[func]{radixSort} + /* Radix sort */ + function radixSort(nums: number[]): void { + // Get the maximum element of the array, used to determine the maximum number of digits + let m: number = Math.max(... nums); + // Traverse from the lowest to the highest digit + for (let exp = 1; exp <= m; exp *= 10) { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, exp); + } + } ``` === "Dart" ```dart title="radix_sort.dart" - [class]{}-[func]{digit} + /* Get k-th digit of element _num, where exp = 10^(k-1) */ + int digit(int _num, int exp) { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return (_num ~/ exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + void countingSortDigit(List nums, int exp) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + List counter = List.filled(10, 0); + int n = nums.length; + // Count the occurrence of digits 0~9 + for (int i = 0; i < n; i++) { + int d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d + counter[d]++; // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d]--; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for (int i = 0; i < n; i++) nums[i] = res[i]; + } - [class]{}-[func]{radixSort} + /* Radix sort */ + void radixSort(List nums) { + // Get the maximum element of the array, used to determine the maximum number of digits + // In Dart, int length is 64 bits + int m = -1 << 63; + for (int _num in nums) if (_num > m) m = _num; + // Traverse from the lowest to the highest digit + for (int exp = 1; exp <= m; exp *= 10) + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, exp); + } ``` === "Rust" ```rust title="radix_sort.rs" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + fn digit(num: i32, exp: i32) -> usize { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return ((num / exp) % 10) as usize; + } - [class]{}-[func]{counting_sort_digit} + /* Counting sort (based on nums k-th digit) */ + fn counting_sort_digit(nums: &mut [i32], exp: i32) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + let mut counter = [0; 10]; + let n = nums.len(); + // Count the occurrence of digits 0~9 + for i in 0..n { + let d = digit(nums[i], exp); // Get the k-th digit of nums[i], noted as d + counter[d] += 1; // Count the occurrence of digit d + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for i in 1..10 { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into res + let mut res = vec![0; n]; + for i in (0..n).rev() { + let d = digit(nums[i], exp); + let j = counter[d] - 1; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d] -= 1; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + nums.copy_from_slice(&res); + } - [class]{}-[func]{radix_sort} + /* Radix sort */ + fn radix_sort(nums: &mut [i32]) { + // Get the maximum element of the array, used to determine the maximum number of digits + let m = *nums.into_iter().max().unwrap(); + // Traverse from the lowest to the highest digit + let mut exp = 1; + while exp <= m { + counting_sort_digit(nums, exp); + exp *= 10; + } + } ``` === "C" ```c title="radix_sort.c" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + int digit(int num, int exp) { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return (num / exp) % 10; + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + void countingSortDigit(int nums[], int size, int exp) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + int *counter = (int *)malloc((sizeof(int) * 10)); + memset(counter, 0, sizeof(int) * 10); // Initialize to 0 to support subsequent memory release + // Count the occurrence of digits 0~9 + for (int i = 0; i < size; i++) { + // Get the k-th digit of nums[i], noted as d + int d = digit(nums[i], exp); + // Count the occurrence of digit d + counter[d]++; + } + // Calculate prefix sum, converting "occurrence count" into "array index" + for (int i = 1; i < 10; i++) { + counter[i] += counter[i - 1]; + } + // Traverse in reverse, based on bucket statistics, place each element into 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; // Get the index j for d in the array + res[j] = nums[i]; // Place the current element at index j + counter[d]--; // Decrease the count of d by 1 + } + // Use result to overwrite the original array nums + for (int i = 0; i < size; i++) { + nums[i] = res[i]; + } + // Free memory + free(res); + free(counter); + } - [class]{}-[func]{radixSort} + /* Radix sort */ + void radixSort(int nums[], int size) { + // Get the maximum element of the array, used to determine the maximum number of digits + int max = INT32_MIN; + for (int i = 0; i < size; i++) { + if (nums[i] > max) { + max = nums[i]; + } + } + // Traverse from the lowest to the highest digit + for (int exp = 1; max >= exp; exp *= 10) + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, size, exp); + } ``` === "Kotlin" ```kotlin title="radix_sort.kt" - [class]{}-[func]{digit} + /* Get the k-th digit of element num, where exp = 10^(k-1) */ + fun digit(num: Int, exp: Int): Int { + // Passing exp instead of k can avoid repeated expensive exponentiation here + return (num / exp) % 10 + } - [class]{}-[func]{countingSortDigit} + /* Counting sort (based on nums k-th digit) */ + fun countingSortDigit(nums: IntArray, exp: Int) { + // Decimal digit range is 0~9, therefore need a bucket array of length 10 + val counter = IntArray(10) + val n = nums.size + // Count the occurrence of digits 0~9 + for (i in 0.. m) m = num + var exp = 1 + // Traverse from the lowest to the highest digit + while (exp <= m) { + // Perform counting sort on the k-th digit of array elements + // k = 1 -> exp = 1 + // k = 2 -> exp = 10 + // i.e., exp = 10^(k-1) + countingSortDigit(nums, exp) + exp *= 10 + } + } ``` === "Ruby" ```ruby title="radix_sort.rb" - [class]{}-[func]{digit} + ### Get k-th digit of element num, where exp = 10^(k-1) ### + def digit(num, exp) + # Passing exp instead of k avoids expensive exponentiation calculations + (num / exp) % 10 + end - [class]{}-[func]{counting_sort_digit} + ### Counting sort (sort by k-th digit of nums) ### + def counting_sort_digit(nums, exp) + # Decimal digit range is 0~9, therefore need a bucket array of length 10 + counter = Array.new(10, 0) + n = nums.length + # Count the occurrence of digits 0~9 + for i in 0...n + d = digit(nums[i], exp) # Get the k-th digit of nums[i], noted as d + counter[d] += 1 # Count the occurrence of digit d + end + # Calculate prefix sum, converting "occurrence count" into "array index" + (1...10).each { |i| counter[i] += counter[i - 1] } + # Traverse in reverse, based on bucket statistics, place each element into res + res = Array.new(n, 0) + for i in (n - 1).downto(0) + d = digit(nums[i], exp) + j = counter[d] - 1 # Get the index j for d in the array + res[j] = nums[i] # Place the current element at index j + counter[d] -= 1 # Decrease the count of d by 1 + end + # Use result to overwrite the original array nums + (0...n).each { |i| nums[i] = res[i] } + end - [class]{}-[func]{radix_sort} + ### Radix sort ### + def radix_sort(nums) + # Get the maximum element of the array, used to determine the maximum number of digits + m = nums.max + # Traverse from the lowest to the highest digit + exp = 1 + while exp <= m + # Perform counting sort on the k-th digit of array elements + # k = 1 -> exp = 1 + # k = 2 -> exp = 10 + # i.e., exp = 10^(k-1) + counting_sort_digit(nums, exp) + exp *= 10 + end + end ``` -=== "Zig" +!!! question "Why start sorting from the lowest digit?" - ```zig title="radix_sort.zig" - [class]{}-[func]{digit} + In successive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the first round result is $a < b$, while the second round result is $a > b$, then the second round's result will replace the first round's result. Since higher-order digits have higher priority than lower-order digits, we should sort the lower digits first and then sort the higher digits. - [class]{}-[func]{countingSortDigit} +## 11.10.2   Algorithm Characteristics - [class]{}-[func]{radixSort} - ``` +Compared to counting sort, radix sort is suitable for larger numerical ranges, **but the prerequisite is that the data must be representable in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are not suitable for radix sort because their number of digits $k$ may be too large, potentially leading to time complexity $O(nk) \gg O(n^2)$. -!!! question "Why start sorting from the least significant digit?" - - In consecutive sorting rounds, the result of a later round will override the result of an earlier round. For example, if the result of the first round is $a < b$ and the second round is $a > b$, the second round's result will replace the first round's result. Since higher-order digits take precedence over lower-order digits, it makes sense to sort the lower digits before the higher digits. - -## 11.10.2   Algorithm characteristics - -Compared to counting sort, radix sort is suitable for larger numerical ranges, **but it assumes that the data can be represented in a fixed number of digits, and the number of digits should not be too large**. For example, floating-point numbers are unsuitable for radix sort, as their digit count $k$ may be large, potentially leading to a time complexity $O(nk) \gg O(n^2)$. - -- **Time complexity is $O(nk)$, non-adaptive sorting**: Assuming the data size is $n$, the data is in base $d$, and the maximum number of digits is $k$, then sorting a single digit takes $O(n + d)$ time, and sorting all $k$ digits takes $O((n + d)k)$ time. Generally, both $d$ and $k$ are relatively small, leading to a time complexity approaching $O(n)$. -- **Space complexity is $O(n + d)$, non-in-place sorting**: Like counting sort, radix sort relies on arrays `res` and `counter` of lengths $n$ and $d$ respectively. -- **Stable sorting**: When counting sort is stable, radix sort is also stable; if counting sort is unstable, radix sort cannot ensure a correct sorting order. +- **Time complexity of $O(nk)$, non-adaptive sorting**: Let the data volume be $n$, the data be in base $d$, and the maximum number of digits be $k$. Then performing counting sort on a certain digit uses $O(n + d)$ time, and sorting all $k$ digits uses $O((n + d)k)$ time. Typically, both $d$ and $k$ are relatively small, and the time complexity approaches $O(n)$. +- **Space complexity of $O(n + d)$, non-in-place sorting**: Same as counting sort, radix sort requires auxiliary arrays `res` and `counter` of lengths $n$ and $d$. +- **Stable sorting**: When counting sort is stable, radix sort is also stable; when counting sort is unstable, radix sort cannot guarantee obtaining correct sorting results. diff --git a/en/docs/chapter_sorting/selection_sort.md b/en/docs/chapter_sorting/selection_sort.md index 36cd01276..c62a1654e 100644 --- a/en/docs/chapter_sorting/selection_sort.md +++ b/en/docs/chapter_sorting/selection_sort.md @@ -2,20 +2,20 @@ comments: true --- -# 11.2   Selection sort +# 11.2   Selection Sort -Selection sort works on a very simple principle: it uses a loop where each iteration selects the smallest element from the unsorted interval and moves it to the end of the sorted section. +Selection sort (selection sort) works very simply: it opens a loop, and in each round, selects the smallest element from the unsorted interval and places it at the end of the sorted interval. -Suppose the length of the array is $n$, the steps of selection sort is shown in Figure 11-2. +Assume the array has length $n$. The algorithm flow of selection sort is shown in Figure 11-2. 1. Initially, all elements are unsorted, i.e., the unsorted (index) interval is $[0, n-1]$. -2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After this, the first element of the array is sorted. -3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After this, the first two elements of the array are sorted. -4. Continue in this manner. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements are sorted. -5. The only remaining element is subsequently the largest element and does not need sorting, thus the array is sorted. +2. Select the smallest element in the interval $[0, n-1]$ and swap it with the element at index $0$. After completion, the first element of the array is sorted. +3. Select the smallest element in the interval $[1, n-1]$ and swap it with the element at index $1$. After completion, the first 2 elements of the array are sorted. +4. And so on. After $n - 1$ rounds of selection and swapping, the first $n - 1$ elements of the array are sorted. +5. The only remaining element must be the largest element, requiring no sorting, so the array sorting is complete. === "<1>" - ![Selection sort process](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } + ![Selection sort steps](selection_sort.assets/selection_sort_step1.png){ class="animation-figure" } === "<2>" ![selection_sort_step2](selection_sort.assets/selection_sort_step2.png){ class="animation-figure" } @@ -47,7 +47,7 @@ Suppose the length of the array is $n$, the steps of selection sort is shown in === "<11>" ![selection_sort_step11](selection_sort.assets/selection_sort_step11.png){ class="animation-figure" } -

Figure 11-2   Selection sort process

+

Figure 11-2   Selection sort steps

In the code, we use $k$ to record the smallest element within the unsorted interval: @@ -57,14 +57,14 @@ In the code, we use $k$ to record the smallest element within the unsorted inter def selection_sort(nums: list[int]): """Selection sort""" n = len(nums) - # Outer loop: unsorted range is [i, n-1] + # Outer loop: unsorted interval is [i, n-1] for i in range(n - 1): - # Inner loop: find the smallest element within the unsorted range + # Inner loop: find the smallest element within the unsorted interval k = i for j in range(i + 1, n): if nums[j] < nums[k]: k = j # Record the index of the smallest element - # Swap the smallest element with the first element of the unsorted range + # Swap the smallest element with the first element of the unsorted interval nums[i], nums[k] = nums[k], nums[i] ``` @@ -74,15 +74,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter /* Selection sort */ void selectionSort(vector &nums) { int n = nums.size(); - // Outer loop: unsorted range is [i, n-1] + // Outer loop: unsorted interval is [i, n-1] for (int i = 0; i < n - 1; i++) { - // Inner loop: find the smallest element within the unsorted range + // Inner loop: find the smallest element within the unsorted interval int k = i; for (int j = i + 1; j < n; j++) { if (nums[j] < nums[k]) k = j; // Record the index of the smallest element } - // Swap the smallest element with the first element of the unsorted range + // Swap the smallest element with the first element of the unsorted interval swap(nums[i], nums[k]); } } @@ -94,15 +94,15 @@ In the code, we use $k$ to record the smallest element within the unsorted inter /* Selection sort */ void selectionSort(int[] nums) { int n = nums.length; - // Outer loop: unsorted range is [i, n-1] + // Outer loop: unsorted interval is [i, n-1] for (int i = 0; i < n - 1; i++) { - // Inner loop: find the smallest element within the unsorted range + // Inner loop: find the smallest element within the unsorted interval int k = i; for (int j = i + 1; j < n; j++) { if (nums[j] < nums[k]) k = j; // Record the index of the smallest element } - // Swap the smallest element with the first element of the unsorted range + // Swap the smallest element with the first element of the unsorted interval int temp = nums[i]; nums[i] = nums[k]; nums[k] = temp; @@ -113,75 +113,223 @@ In the code, we use $k$ to record the smallest element within the unsorted inter === "C#" ```csharp title="selection_sort.cs" - [class]{selection_sort}-[func]{SelectionSort} + /* Selection sort */ + void SelectionSort(int[] nums) { + int n = nums.Length; + // Outer loop: unsorted interval is [i, n-1] + for (int i = 0; i < n - 1; i++) { + // Inner loop: find the smallest element within the unsorted interval + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // Record the index of the smallest element + } + // Swap the smallest element with the first element of the unsorted interval + (nums[k], nums[i]) = (nums[i], nums[k]); + } + } ``` === "Go" ```go title="selection_sort.go" - [class]{}-[func]{selectionSort} + /* Selection sort */ + func selectionSort(nums []int) { + n := len(nums) + // Outer loop: unsorted interval is [i, n-1] + for i := 0; i < n-1; i++ { + // Inner loop: find the smallest element within the unsorted interval + k := i + for j := i + 1; j < n; j++ { + if nums[j] < nums[k] { + // Record the index of the smallest element + k = j + } + } + // Swap the smallest element with the first element of the unsorted interval + nums[i], nums[k] = nums[k], nums[i] + + } + } ``` === "Swift" ```swift title="selection_sort.swift" - [class]{}-[func]{selectionSort} + /* Selection sort */ + func selectionSort(nums: inout [Int]) { + // Outer loop: unsorted interval is [i, n-1] + for i in nums.indices.dropLast() { + // Inner loop: find the smallest element within the unsorted interval + var k = i + for j in nums.indices.dropFirst(i + 1) { + if nums[j] < nums[k] { + k = j // Record the index of the smallest element + } + } + // Swap the smallest element with the first element of the unsorted interval + nums.swapAt(i, k) + } + } ``` === "JS" ```javascript title="selection_sort.js" - [class]{}-[func]{selectionSort} + /* Selection sort */ + function selectionSort(nums) { + let n = nums.length; + // Outer loop: unsorted interval is [i, n-1] + for (let i = 0; i < n - 1; i++) { + // Inner loop: find the smallest element within the unsorted interval + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // Record the index of the smallest element + } + } + // Swap the smallest element with the first element of the unsorted interval + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } ``` === "TS" ```typescript title="selection_sort.ts" - [class]{}-[func]{selectionSort} + /* Selection sort */ + function selectionSort(nums: number[]): void { + let n = nums.length; + // Outer loop: unsorted interval is [i, n-1] + for (let i = 0; i < n - 1; i++) { + // Inner loop: find the smallest element within the unsorted interval + let k = i; + for (let j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) { + k = j; // Record the index of the smallest element + } + } + // Swap the smallest element with the first element of the unsorted interval + [nums[i], nums[k]] = [nums[k], nums[i]]; + } + } ``` === "Dart" ```dart title="selection_sort.dart" - [class]{}-[func]{selectionSort} + /* Selection sort */ + void selectionSort(List nums) { + int n = nums.length; + // Outer loop: unsorted interval is [i, n-1] + for (int i = 0; i < n - 1; i++) { + // Inner loop: find the smallest element within the unsorted interval + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) k = j; // Record the index of the smallest element + } + // Swap the smallest element with the first element of the unsorted interval + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } ``` === "Rust" ```rust title="selection_sort.rs" - [class]{}-[func]{selection_sort} + /* Selection sort */ + fn selection_sort(nums: &mut [i32]) { + if nums.is_empty() { + return; + } + let n = nums.len(); + // Outer loop: unsorted interval is [i, n-1] + for i in 0..n - 1 { + // Inner loop: find the smallest element within the unsorted interval + let mut k = i; + for j in i + 1..n { + if nums[j] < nums[k] { + k = j; // Record the index of the smallest element + } + } + // Swap the smallest element with the first element of the unsorted interval + nums.swap(i, k); + } + } ``` === "C" ```c title="selection_sort.c" - [class]{}-[func]{selectionSort} + /* Selection sort */ + void selectionSort(int nums[], int n) { + // Outer loop: unsorted interval is [i, n-1] + for (int i = 0; i < n - 1; i++) { + // Inner loop: find the smallest element within the unsorted interval + int k = i; + for (int j = i + 1; j < n; j++) { + if (nums[j] < nums[k]) + k = j; // Record the index of the smallest element + } + // Swap the smallest element with the first element of the unsorted interval + int temp = nums[i]; + nums[i] = nums[k]; + nums[k] = temp; + } + } ``` === "Kotlin" ```kotlin title="selection_sort.kt" - [class]{}-[func]{selectionSort} + /* Selection sort */ + fun selectionSort(nums: IntArray) { + val n = nums.size + // Outer loop: unsorted interval is [i, n-1] + for (i in 0.. Figure 11-3   Selection sort instability example

+

Figure 11-3   Selection sort non-stability example

diff --git a/en/docs/chapter_sorting/sorting_algorithm.md b/en/docs/chapter_sorting/sorting_algorithm.md index 401d11245..eabba69bf 100644 --- a/en/docs/chapter_sorting/sorting_algorithm.md +++ b/en/docs/chapter_sorting/sorting_algorithm.md @@ -2,28 +2,28 @@ comments: true --- -# 11.1   Sorting algorithms +# 11.1   Sorting Algorithm -Sorting algorithms are used to arrange a set of data in a specific order. Sorting algorithms have a wide range of applications because ordered data can usually be searched, analyzed, and processed more efficiently. +Sorting algorithm (sorting algorithm) is used to arrange a group of data in a specific order. Sorting algorithms have extensive applications because ordered data can usually be searched, analyzed, and processed more efficiently. -As shown in Figure 11-1, the data types in sorting algorithms can be integers, floating point numbers, characters, or strings, etc. Sorting criterion can be set according to needs, such as numerical size, character ASCII order, or custom criterion. +As shown in Figure 11-1, data types in sorting algorithms can be integers, floating-point numbers, characters, or strings, etc. The sorting criterion can be set according to requirements, such as numerical size, character ASCII code order, or custom rules. -![Data types and comparator examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } +![Data type and criterion examples](sorting_algorithm.assets/sorting_examples.png){ class="animation-figure" } -

Figure 11-1   Data types and comparator examples

+

Figure 11-1   Data type and criterion examples

-## 11.1.1   Evaluation dimensions +## 11.1.1   Evaluation Dimensions -**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, as well as a lower number of overall operations (lowering the constant term of time complexity). For large data volumes, execution efficiency is particularly important. +**Execution efficiency**: We expect the time complexity of sorting algorithms to be as low as possible, with a smaller total number of operations (reducing the constant factor in time complexity). For large data volumes, execution efficiency is particularly important. -**In-place property**: As the name implies, in-place sorting is achieved by directly manipulating the original array, without the need for additional helper arrays, thus saving memory. Generally, in-place sorting involves fewer data moving operations and is faster. +**In-place property**: As the name implies, in-place sorting achieves sorting by operating directly on the original array without requiring additional auxiliary arrays, thus saving memory. Typically, in-place sorting involves fewer data movement operations and runs faster. -**Stability**: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting. +**Stability**: Stable sorting ensures that the relative order of equal elements in the array does not change after sorting is completed. -Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose we have a table storing student information, with the first and second columns being name and age, respectively. In this case, unstable sorting might lead to a loss of order in the input data: +Stable sorting is a necessary condition for multi-level sorting scenarios. Suppose we have a table storing student information, where column 1 and column 2 are name and age, respectively. In this case, unstable sorting may cause the ordered nature of the input data to be lost: ```shell -# Input data is sorted by name +# Input Data Is Sorted by Name # (name, age) ('A', 19) ('B', 18) @@ -31,9 +31,9 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose ('D', 19) ('E', 23) -# Assuming an unstable sorting algorithm is used to sort the list by age, -# the result changes the relative position of ('D', 19) and ('A', 19), -# and the property of the input data being sorted by name is lost +# Assuming We Use an Unstable Sorting Algorithm to Sort the List by Age, +# In the Result, the Relative Positions of ('D', 19) and ('A', 19) Are Changed, +# And the Property That the Input Data Is Sorted by Name Is Lost ('B', 18) ('D', 19) ('A', 19) @@ -41,12 +41,12 @@ Stable sorting is a necessary condition for multi-key sorting scenarios. Suppose ('E', 23) ``` -**Adaptability**: Adaptive sorting leverages existing order information within the input data to reduce computational effort, achieving more optimal time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than their average-case time complexity. +**Adaptability**: Adaptive sorting can utilize the existing order information in the input data to reduce the amount of computation, achieving better time efficiency. The best-case time complexity of adaptive sorting algorithms is typically better than the average time complexity. -**Comparison or non-comparison-based**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements and thus sort the entire array, with the theoretical optimal time complexity being $O(n \log n)$. Meanwhile, non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively poor. +**Comparison-based or not**: Comparison-based sorting relies on comparison operators ($<$, $=$, $>$) to determine the relative order of elements, thereby sorting the entire array, with a theoretical optimal time complexity of $O(n \log n)$. Non-comparison sorting does not use comparison operators and can achieve a time complexity of $O(n)$, but its versatility is relatively limited. -## 11.1.2   Ideal sorting algorithm +## 11.1.2   Ideal Sorting Algorithm -**Fast execution, in-place, stable, adaptive, and versatile**. Clearly, no sorting algorithm that combines all these features has been found to date. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. +**Fast execution, in-place, stable, adaptive, good versatility**. Clearly, no sorting algorithm has been discovered to date that combines all of these characteristics. Therefore, when selecting a sorting algorithm, it is necessary to decide based on the specific characteristics of the data and the requirements of the problem. -Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each based on the above evaluation dimensions. +Next, we will learn about various sorting algorithms together and analyze the advantages and disadvantages of each sorting algorithm based on the above evaluation dimensions. diff --git a/en/docs/chapter_sorting/summary.md b/en/docs/chapter_sorting/summary.md index f65ae09af..2b531fa8b 100644 --- a/en/docs/chapter_sorting/summary.md +++ b/en/docs/chapter_sorting/summary.md @@ -4,50 +4,50 @@ comments: true # 11.11   Summary -### 1.   Key review +### 1.   Key Review -- Bubble sort works by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$. -- Insertion sort sorts each round by inserting elements from the unsorted interval into the correct position in the sorted interval. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in sorting small amounts of data due to relatively fewer operations per unit. -- Quick sort is based on sentinel partitioning operations. In sentinel partitioning, it's possible to always pick the worst pivot, leading to a time complexity degradation to $O(n^2)$. Introducing median or random pivots can reduce the probability of such degradation. Tail recursion effectively reduce the recursion depth, optimizing the space complexity to $O(\log n)$. -- Merge sort includes dividing and merging two phases, typically embodying the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, resulting in a space complexity of $O(n)$; however, the space complexity for sorting a list can be optimized to $O(1)$. -- Bucket sort consists of three steps: distributing data into buckets, sorting within each bucket, and merging results in bucket order. It also embodies the divide-and-conquer strategy, suitable for very large datasets. The key to bucket sort is the even distribution of data. -- Counting sort is a variant of bucket sort, which sorts by counting the occurrences of each data point. Counting sort is suitable for large datasets with a limited range of data and requires data conversion to positive integers. -- Radix sort processes data by sorting it digit by digit, requiring data to be represented as fixed-length numbers. -- Overall, we seek sorting algorithm that has high efficiency, stability, in-place operation, and adaptability. However, like other data structures and algorithms, no sorting algorithm can meet all these conditions simultaneously. In practical applications, we need to choose the appropriate sorting algorithm based on the characteristics of the data. -- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place nature, and adaptability. +- Bubble sort achieves sorting by swapping adjacent elements. By adding a flag to enable early return, we can optimize the best-case time complexity of bubble sort to $O(n)$. +- Insertion sort completes sorting by inserting elements from the unsorted interval into the correct position in the sorted interval each round. Although the time complexity of insertion sort is $O(n^2)$, it is very popular in small data volume sorting tasks because it involves relatively few unit operations. +- Quick sort is implemented based on sentinel partitioning operations. In sentinel partitioning, it is possible to select the worst pivot every time, causing the time complexity to degrade to $O(n^2)$. Introducing median pivot or random pivot can reduce the probability of such degradation. By preferentially recursing on the shorter sub-interval, the recursion depth can be effectively reduced, optimizing the space complexity to $O(\log n)$. +- Merge sort includes two phases: divide and merge, which typically embody the divide-and-conquer strategy. In merge sort, sorting an array requires creating auxiliary arrays, with a space complexity of $O(n)$; however, the space complexity of sorting a linked list can be optimized to $O(1)$. +- Bucket sort consists of three steps: distributing data into buckets, sorting within buckets, and merging results. It also embodies the divide-and-conquer strategy and is suitable for very large data volumes. The key to bucket sort is distributing data evenly. +- Counting sort is a special case of bucket sort, which achieves sorting by counting the number of occurrences of data. Counting sort is suitable for situations where the data volume is large but the data range is limited, and requires that data can be converted to positive integers. +- Radix sort achieves data sorting by sorting digit by digit, requiring that data can be represented as fixed-digit numbers. +- Overall, we hope to find a sorting algorithm that is efficient, stable, in-place, and adaptive, with good versatility. However, just like other data structures and algorithms, no sorting algorithm has been found so far that simultaneously possesses all these characteristics. In practical applications, we need to select the appropriate sorting algorithm based on the specific characteristics of the data. +- Figure 11-19 compares mainstream sorting algorithms in terms of efficiency, stability, in-place property, and adaptability. -![Sorting Algorithm Comparison](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } +![Sorting algorithm comparison](summary.assets/sorting_algorithms_comparison.png){ class="animation-figure" } -

Figure 11-19   Sorting Algorithm Comparison

+

Figure 11-19   Sorting algorithm comparison

### 2.   Q & A -**Q**: When is the stability of sorting algorithms necessary? +**Q**: In what situations is the stability of sorting algorithms necessary? -In reality, we might sort based on one attribute of an object. For example, students have names and heights as attributes, and we aim to implement multi-level sorting: first by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then by height. Because the sorting algorithm is unstable, we might end up with `(D, 170) (C, 170) (A, 180) (B, 185)`. +In reality, we may sort based on a certain attribute of objects. For example, students have two attributes: name and height. We want to implement multi-level sorting: first sort by name to get `(A, 180) (B, 185) (C, 170) (D, 170)`; then sort by height. Because the sorting algorithm is unstable, we may get `(D, 170) (C, 170) (A, 180) (B, 185)`. -It can be seen that the positions of students D and C have been swapped, disrupting the orderliness of the names, which is undesirable. +It can be seen that the positions of students D and C have been swapped, and the orderliness of names has been disrupted, which is something we don't want to see. **Q**: Can the order of "searching from right to left" and "searching from left to right" in sentinel partitioning be swapped? -No, when using the leftmost element as the pivot, we must first "search from right to left" then "search from left to right". This conclusion is somewhat counterintuitive, so let's analyze the reason. +No. When we use the leftmost element as the pivot, we must first "search from right to left" and then "search from left to right". This conclusion is somewhat counterintuitive; let's analyze the reason. -The last step of the sentinel partition `partition()` is to swap `nums[left]` and `nums[i]`. After the swap, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we "search from left to right" first, and if no element larger than the pivot is found, **we will exit the loop when `i == j`, possibly with `nums[j] == nums[i] > nums[left]`**. In other words, the final swap operation will exchange an element larger than the pivot to the left end of the array, causing the sentinel partition to fail. +The last step of sentinel partitioning `partition()` is to swap `nums[left]` and `nums[i]`. After the swap is complete, the elements to the left of the pivot are all `<=` the pivot, **which requires that `nums[left] >= nums[i]` must hold before the last swap**. Suppose we first "search from left to right", then if we cannot find an element larger than the pivot, **we will exit the loop when `i == j`, at which point it may be that `nums[j] == nums[i] > nums[left]`**. In other words, the last swap operation will swap an element larger than the pivot to the leftmost end of the array, causing sentinel partitioning to fail. -For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after the sentinel partition is `[1, 0, 0, 0, 0]`, which is incorrect. +For example, given the array `[0, 0, 0, 0, 1]`, if we first "search from left to right", the array after sentinel partitioning is `[1, 0, 0, 0, 0]`, which is incorrect. -Upon further consideration, if we choose `nums[right]` as the pivot, then exactly the opposite, we must first "search from left to right". +Thinking deeper, if we select `nums[right]` as the pivot, then it's exactly the opposite - we must first "search from left to right". -**Q**: Regarding tail recursion optimization, why does choosing the shorter array ensure that the recursion depth does not exceed $\log n$? +**Q**: Regarding the optimization of recursion depth in quick sort, why can selecting the shorter array ensure that the recursion depth does not exceed $\log n$? -The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partition divides the original array into two subarrays. With tail recursion optimization, the length of the subarray to be recursively followed is at most half of the original array length. Assuming the worst case always halves the length, the final recursion depth will be $\log n$. +The recursion depth is the number of currently unreturned recursive methods. Each round of sentinel partitioning divides the original array into two sub-arrays. After recursion depth optimization, the length of the sub-array to be recursively processed is at most half of the original array length. Assuming the worst case is always half the length, the final recursion depth will be $\log n$. -Reviewing the original quicksort, we might continuously recursively process larger arrays, in the worst case from $n$, $n - 1$, ..., $2$, $1$, with a recursion depth of $n$. Tail recursion optimization can avoid this scenario. +Reviewing the original quick sort, we may continuously recurse on the longer array. In the worst case, it would be $n$, $n - 1$, $\dots$, $2$, $1$, with a recursion depth of $n$. Recursion depth optimization can avoid this situation. -**Q**: When all elements in the array are equal, is the time complexity of quicksort $O(n^2)$? How should this degenerate case be handled? +**Q**: When all elements in the array are equal, is the time complexity of quick sort $O(n^2)$? How should this degenerate case be handled? -Yes. For this situation, consider using sentinel partitioning to divide the array into three parts: less than, equal to, and greater than the pivot. Only recursively proceed with the less than and greater than parts. In this method, an array where all input elements are equal can be sorted in just one round of sentinel partitioning. +Yes. For this situation, consider partitioning the array into three parts through sentinel partitioning: less than, equal to, and greater than the pivot. Only recursively process the less than and greater than parts. Under this method, an array where all input elements are equal can complete sorting in just one round of sentinel partitioning. **Q**: Why is the worst-case time complexity of bucket sort $O(n^2)$? -In the worst case, all elements are placed in the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$. +In the worst case, all elements are distributed into the same bucket. If we use an $O(n^2)$ algorithm to sort these elements, the time complexity will be $O(n^2)$. diff --git a/en/docs/chapter_stack_and_queue/deque.md b/en/docs/chapter_stack_and_queue/deque.md index 0898ba90d..60fe34257 100644 --- a/en/docs/chapter_stack_and_queue/deque.md +++ b/en/docs/chapter_stack_and_queue/deque.md @@ -2,329 +2,329 @@ comments: true --- -# 5.3   Double-ended queue +# 5.3   Deque -In a queue, we can only delete elements from the head or add elements to the tail. As shown in Figure 5-7, a double-ended queue (deque) offers more flexibility, allowing the addition or removal of elements at both the head and the tail. +In a queue, we can only remove elements from the front or add elements at the rear. As shown in Figure 5-7, a double-ended queue (deque) provides greater flexibility, allowing the addition or removal of elements at both the front and rear. -![Operations in double-ended queue](deque.assets/deque_operations.png){ class="animation-figure" } +![Operations of deque](deque.assets/deque_operations.png){ class="animation-figure" } -

Figure 5-7   Operations in double-ended queue

+

Figure 5-7   Operations of deque

-## 5.3.1   Common operations in double-ended queue +## 5.3.1   Common Deque Operations -The common operations in a double-ended queue are listed below, and the names of specific methods depend on the programming language used. +The common operations on a deque are shown in Table 5-3. The specific method names depend on the programming language used. -

Table 5-3   Efficiency of double-ended queue operations

+

Table 5-3   Efficiency of Deque Operations

-| Method Name | Description | Time Complexity | -| ------------- | -------------------------- | --------------- | -| `pushFirst()` | Add an element to the head | $O(1)$ | -| `pushLast()` | Add an element to the tail | $O(1)$ | -| `popFirst()` | Remove the first element | $O(1)$ | -| `popLast()` | Remove the last element | $O(1)$ | -| `peekFirst()` | Access the first element | $O(1)$ | -| `peekLast()` | Access the last element | $O(1)$ | +| Method | Description | Time Complexity | +| -------------- | ------------------------- | --------------- | +| `push_first()` | Add element to front | $O(1)$ | +| `push_last()` | Add element to rear | $O(1)$ | +| `pop_first()` | Remove front element | $O(1)$ | +| `pop_last()` | Remove rear element | $O(1)$ | +| `peek_first()` | Access front element | $O(1)$ | +| `peek_last()` | Access rear element | $O(1)$ |
-Similarly, we can directly use the double-ended queue classes implemented in programming languages: +Similarly, we can directly use the deque classes already implemented in programming languages: === "Python" ```python title="deque.py" from collections import deque - # Initialize the deque + # Initialize deque deq: deque[int] = deque() # Enqueue elements - deq.append(2) # Add to the tail + deq.append(2) # Add to rear deq.append(5) deq.append(4) - deq.appendleft(3) # Add to the head + deq.appendleft(3) # Add to front deq.appendleft(1) # Access elements - front: int = deq[0] # The first element - rear: int = deq[-1] # The last element + front: int = deq[0] # Front element + rear: int = deq[-1] # Rear element # Dequeue elements - pop_front: int = deq.popleft() # The first element dequeued - pop_rear: int = deq.pop() # The last element dequeued + pop_front: int = deq.popleft() # Front element dequeue + pop_rear: int = deq.pop() # Rear element dequeue - # Get the length of the deque + # Get deque length size: int = len(deq) - # Check if the deque is empty + # Check if deque is empty is_empty: bool = len(deq) == 0 ``` === "C++" ```cpp title="deque.cpp" - /* Initialize the deque */ + /* Initialize deque */ deque deque; /* Enqueue elements */ - deque.push_back(2); // Add to the tail + deque.push_back(2); // Add to rear deque.push_back(5); deque.push_back(4); - deque.push_front(3); // Add to the head + deque.push_front(3); // Add to front deque.push_front(1); /* Access elements */ - int front = deque.front(); // The first element - int back = deque.back(); // The last element + int front = deque.front(); // Front element + int back = deque.back(); // Rear element /* Dequeue elements */ - deque.pop_front(); // The first element dequeued - deque.pop_back(); // The last element dequeued + deque.pop_front(); // Front element dequeue + deque.pop_back(); // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ int size = deque.size(); - /* Check if the deque is empty */ + /* Check if deque is empty */ bool empty = deque.empty(); ``` === "Java" ```java title="deque.java" - /* Initialize the deque */ + /* Initialize deque */ Deque deque = new LinkedList<>(); /* Enqueue elements */ - deque.offerLast(2); // Add to the tail + deque.offerLast(2); // Add to rear deque.offerLast(5); deque.offerLast(4); - deque.offerFirst(3); // Add to the head + deque.offerFirst(3); // Add to front deque.offerFirst(1); /* Access elements */ - int peekFirst = deque.peekFirst(); // The first element - int peekLast = deque.peekLast(); // The last element + int peekFirst = deque.peekFirst(); // Front element + int peekLast = deque.peekLast(); // Rear element /* Dequeue elements */ - int popFirst = deque.pollFirst(); // The first element dequeued - int popLast = deque.pollLast(); // The last element dequeued + int popFirst = deque.pollFirst(); // Front element dequeue + int popLast = deque.pollLast(); // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ int size = deque.size(); - /* Check if the deque is empty */ + /* Check if deque is empty */ boolean isEmpty = deque.isEmpty(); ``` === "C#" ```csharp title="deque.cs" - /* Initialize the deque */ - // In C#, LinkedList is used as a deque + /* Initialize deque */ + // In C#, use LinkedList as a deque LinkedList deque = new(); /* Enqueue elements */ - deque.AddLast(2); // Add to the tail + deque.AddLast(2); // Add to rear deque.AddLast(5); deque.AddLast(4); - deque.AddFirst(3); // Add to the head + deque.AddFirst(3); // Add to front deque.AddFirst(1); /* Access elements */ - int peekFirst = deque.First.Value; // The first element - int peekLast = deque.Last.Value; // The last element + int peekFirst = deque.First.Value; // Front element + int peekLast = deque.Last.Value; // Rear element /* Dequeue elements */ - deque.RemoveFirst(); // The first element dequeued - deque.RemoveLast(); // The last element dequeued + deque.RemoveFirst(); // Front element dequeue + deque.RemoveLast(); // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ int size = deque.Count; - /* Check if the deque is empty */ + /* Check if deque is empty */ bool isEmpty = deque.Count == 0; ``` === "Go" ```go title="deque_test.go" - /* Initialize the deque */ + /* Initialize deque */ // In Go, use list as a deque deque := list.New() /* Enqueue elements */ - deque.PushBack(2) // Add to the tail + deque.PushBack(2) // Add to rear deque.PushBack(5) deque.PushBack(4) - deque.PushFront(3) // Add to the head + deque.PushFront(3) // Add to front deque.PushFront(1) /* Access elements */ - front := deque.Front() // The first element - rear := deque.Back() // The last element + front := deque.Front() // Front element + rear := deque.Back() // Rear element /* Dequeue elements */ - deque.Remove(front) // The first element dequeued - deque.Remove(rear) // The last element dequeued + deque.Remove(front) // Front element dequeue + deque.Remove(rear) // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ size := deque.Len() - /* Check if the deque is empty */ + /* Check if deque is empty */ isEmpty := deque.Len() == 0 ``` === "Swift" ```swift title="deque.swift" - /* Initialize the deque */ - // Swift does not have a built-in deque class, so Array can be used as a deque + /* Initialize deque */ + // Swift does not have a built-in deque class, can use Array as a deque var deque: [Int] = [] /* Enqueue elements */ - deque.append(2) // Add to the tail + deque.append(2) // Add to rear deque.append(5) deque.append(4) - deque.insert(3, at: 0) // Add to the head + deque.insert(3, at: 0) // Add to front deque.insert(1, at: 0) /* Access elements */ - let peekFirst = deque.first! // The first element - let peekLast = deque.last! // The last element + let peekFirst = deque.first! // Front element + let peekLast = deque.last! // Rear element /* Dequeue elements */ - // Using Array, popFirst has a complexity of O(n) - let popFirst = deque.removeFirst() // The first element dequeued - let popLast = deque.removeLast() // The last element dequeued + // When using Array simulation, popFirst has O(n) complexity + let popFirst = deque.removeFirst() // Front element dequeue + let popLast = deque.removeLast() // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ let size = deque.count - /* Check if the deque is empty */ + /* Check if deque is empty */ let isEmpty = deque.isEmpty ``` === "JS" ```javascript title="deque.js" - /* Initialize the deque */ - // JavaScript does not have a built-in deque, so Array is used as a deque + /* Initialize deque */ + // JavaScript does not have a built-in deque, can only use Array as a deque const deque = []; /* Enqueue elements */ deque.push(2); deque.push(5); deque.push(4); - // Note that unshift() has a time complexity of O(n) as it's an array + // Please note that since it's an array, unshift() has O(n) time complexity deque.unshift(3); deque.unshift(1); /* Access elements */ - const peekFirst = deque[0]; // The first element - const peekLast = deque[deque.length - 1]; // The last element + const peekFirst = deque[0]; + const peekLast = deque[deque.length - 1]; /* Dequeue elements */ - // Note that shift() has a time complexity of O(n) as it's an array - const popFront = deque.shift(); // The first element dequeued - const popBack = deque.pop(); // The last element dequeued + // Please note that since it's an array, shift() has O(n) time complexity + const popFront = deque.shift(); + const popBack = deque.pop(); - /* Get the length of the deque */ + /* Get deque length */ const size = deque.length; - /* Check if the deque is empty */ + /* Check if deque is empty */ const isEmpty = size === 0; ``` === "TS" ```typescript title="deque.ts" - /* Initialize the deque */ - // TypeScript does not have a built-in deque, so Array is used as a deque + /* Initialize deque */ + // TypeScript does not have a built-in deque, can only use Array as a deque const deque: number[] = []; /* Enqueue elements */ deque.push(2); deque.push(5); deque.push(4); - // Note that unshift() has a time complexity of O(n) as it's an array + // Please note that since it's an array, unshift() has O(n) time complexity deque.unshift(3); deque.unshift(1); /* Access elements */ - const peekFirst: number = deque[0]; // The first element - const peekLast: number = deque[deque.length - 1]; // The last element + const peekFirst: number = deque[0]; + const peekLast: number = deque[deque.length - 1]; /* Dequeue elements */ - // Note that shift() has a time complexity of O(n) as it's an array - const popFront: number = deque.shift() as number; // The first element dequeued - const popBack: number = deque.pop() as number; // The last element dequeued + // Please note that since it's an array, shift() has O(n) time complexity + const popFront: number = deque.shift() as number; + const popBack: number = deque.pop() as number; - /* Get the length of the deque */ + /* Get deque length */ const size: number = deque.length; - /* Check if the deque is empty */ + /* Check if deque is empty */ const isEmpty: boolean = size === 0; ``` === "Dart" ```dart title="deque.dart" - /* Initialize the deque */ + /* Initialize deque */ // In Dart, Queue is defined as a deque Queue deque = Queue(); /* Enqueue elements */ - deque.addLast(2); // Add to the tail + deque.addLast(2); // Add to rear deque.addLast(5); deque.addLast(4); - deque.addFirst(3); // Add to the head + deque.addFirst(3); // Add to front deque.addFirst(1); /* Access elements */ - int peekFirst = deque.first; // The first element - int peekLast = deque.last; // The last element + int peekFirst = deque.first; // Front element + int peekLast = deque.last; // Rear element /* Dequeue elements */ - int popFirst = deque.removeFirst(); // The first element dequeued - int popLast = deque.removeLast(); // The last element dequeued + int popFirst = deque.removeFirst(); // Front element dequeue + int popLast = deque.removeLast(); // Rear element dequeue - /* Get the length of the deque */ + /* Get deque length */ int size = deque.length; - /* Check if the deque is empty */ + /* Check if deque is empty */ bool isEmpty = deque.isEmpty; ``` === "Rust" ```rust title="deque.rs" - /* Initialize the deque */ + /* Initialize deque */ let mut deque: VecDeque = VecDeque::new(); /* Enqueue elements */ - deque.push_back(2); // Add to the tail + deque.push_back(2); // Add to rear deque.push_back(5); deque.push_back(4); - deque.push_front(3); // Add to the head + deque.push_front(3); // Add to front deque.push_front(1); /* Access elements */ - if let Some(front) = deque.front() { // The first element + if let Some(front) = deque.front() { // Front element } - if let Some(rear) = deque.back() { // The last element + if let Some(rear) = deque.back() { // Rear element } /* Dequeue elements */ - if let Some(pop_front) = deque.pop_front() { // The first element dequeued + if let Some(pop_front) = deque.pop_front() { // Front element dequeue } - if let Some(pop_rear) = deque.pop_back() { // The last element dequeued + if let Some(pop_rear) = deque.pop_back() { // Rear element dequeue } - /* Get the length of the deque */ + /* Get deque length */ let size = deque.len(); - /* Check if the deque is empty */ + /* Check if deque is empty */ let is_empty = deque.is_empty(); ``` @@ -337,64 +337,112 @@ Similarly, we can directly use the double-ended queue classes implemented in pro === "Kotlin" ```kotlin title="deque.kt" + /* Initialize deque */ + val deque = LinkedList() + /* Enqueue elements */ + deque.offerLast(2) // Add to rear + deque.offerLast(5) + deque.offerLast(4) + deque.offerFirst(3) // Add to front + deque.offerFirst(1) + + /* Access elements */ + val peekFirst = deque.peekFirst() // Front element + val peekLast = deque.peekLast() // Rear element + + /* Dequeue elements */ + val popFirst = deque.pollFirst() // Front element dequeue + val popLast = deque.pollLast() // Rear element dequeue + + /* Get deque length */ + val size = deque.size + + /* Check if deque is empty */ + val isEmpty = deque.isEmpty() ``` -=== "Zig" +=== "Ruby" - ```zig title="deque.zig" + ```ruby title="deque.rb" + # Initialize deque + # Ruby does not have a built-in deque, can only use Array as a deque + deque = [] + # Enqueue elements + deque << 2 + deque << 5 + deque << 4 + # Please note that since it's an array, Array#unshift has O(n) time complexity + deque.unshift(3) + deque.unshift(1) + + # Access elements + peek_first = deque.first + peek_last = deque.last + + # Dequeue elements + # Please note that since it's an array, Array#shift has O(n) time complexity + pop_front = deque.shift + pop_back = deque.pop + + # Get deque length + size = deque.length + + # Check if deque is empty + is_empty = size.zero? ``` -??? pythontutor "Visualizing Code" +??? pythontutor "Code Visualization" - https://pythontutor.com/render.html#code=from%20collections%20import%20deque%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%0A%20%20%20%20deq%20%3D%20deque%28%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E9%98%9F%0A%20%20%20%20deq.append%282%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E5%B0%BE%0A%20%20%20%20deq.append%285%29%0A%20%20%20%20deq.append%284%29%0A%20%20%20%20deq.appendleft%283%29%20%20%23%20%E6%B7%BB%E5%8A%A0%E8%87%B3%E9%98%9F%E9%A6%96%0A%20%20%20%20deq.appendleft%281%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20deq%5B0%5D%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%20front%20%3D%22,%20front%29%0A%20%20%20%20rear%20%3D%20deq%5B-1%5D%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%20rear%20%3D%22,%20rear%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop_front%20%3D%20deq.popleft%28%29%20%20%23%20%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_front%20%3D%22,%20pop_front%29%0A%20%20%20%20print%28%22%E9%98%9F%E9%A6%96%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%20%20%20%20pop_rear%20%3D%20deq.pop%28%29%20%20%23%20%E9%98%9F%E5%B0%BE%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20%20pop_rear%20%3D%22,%20pop_rear%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%B0%BE%E5%87%BA%E9%98%9F%E5%90%8E%20deque%20%3D%22,%20deq%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28deq%29%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28deq%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +
+ -## 5.3.2   Implementing a double-ended queue * +## 5.3.2   Deque Implementation * -The implementation of a double-ended queue is similar to that of a regular queue, it can be based on either a linked list or an array as the underlying data structure. +The implementation of a deque is similar to that of a queue. You can choose either a linked list or an array as the underlying data structure. -### 1.   Implementation based on doubly linked list +### 1.   Doubly Linked List Implementation -Recall from the previous section that we used a regular singly linked list to implement a queue, as it conveniently allows for deleting from the head (corresponding to the dequeue operation) and adding new elements after the tail (corresponding to the enqueue operation). +Reviewing the previous section, we used a regular singly linked list to implement a queue because it conveniently allows deleting the head node (corresponding to dequeue) and adding new nodes after the tail node (corresponding to enqueue). -For a double-ended queue, both the head and the tail can perform enqueue and dequeue operations. In other words, a double-ended queue needs to implement operations in the opposite direction as well. For this, we use a "doubly linked list" as the underlying data structure of the double-ended queue. +For a deque, both the front and rear can perform enqueue and dequeue operations. In other words, a deque needs to implement operations in the opposite direction as well. For this reason, we use a "doubly linked list" as the underlying data structure for the deque. -As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the double-ended queue, respectively, and implement the functionality to add and remove nodes at both ends. +As shown in Figure 5-8, we treat the head and tail nodes of the doubly linked list as the front and rear of the deque, implementing functionality to add and remove nodes at both ends. === "LinkedListDeque" - ![Implementing Double-Ended Queue with Doubly Linked List for Enqueue and Dequeue Operations](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } + ![Enqueue and dequeue operations in linked list implementation of deque](deque.assets/linkedlist_deque_step1.png){ class="animation-figure" } -=== "pushLast()" +=== "push_last()" ![linkedlist_deque_push_last](deque.assets/linkedlist_deque_step2_push_last.png){ class="animation-figure" } -=== "pushFirst()" +=== "push_first()" ![linkedlist_deque_push_first](deque.assets/linkedlist_deque_step3_push_first.png){ class="animation-figure" } -=== "popLast()" +=== "pop_last()" ![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_step4_pop_last.png){ class="animation-figure" } -=== "popFirst()" +=== "pop_first()" ![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_step5_pop_first.png){ class="animation-figure" } -

Figure 5-8   Implementing Double-Ended Queue with Doubly Linked List for Enqueue and Dequeue Operations

+

Figure 5-8   Enqueue and dequeue operations in linked list implementation of deque

-The implementation code is as follows: +The implementation code is shown below: === "Python" ```python title="linkedlist_deque.py" class ListNode: - """Double-linked list node""" + """Doubly linked list node""" def __init__(self, val: int): """Constructor""" self.val: int = val - self.next: ListNode | None = None # Reference to successor node - self.prev: ListNode | None = None # Reference to predecessor node + self.next: ListNode | None = None # Successor node reference + self.prev: ListNode | None = None # Predecessor node reference class LinkedListDeque: - """Double-ended queue class based on double-linked list""" + """Double-ended queue based on doubly linked list implementation""" def __init__(self): """Constructor""" @@ -407,54 +455,54 @@ The implementation code is as follows: return self._size def is_empty(self) -> bool: - """Determine if the double-ended queue is empty""" + """Check if the double-ended queue is empty""" return self._size == 0 def push(self, num: int, is_front: bool): """Enqueue operation""" node = ListNode(num) - # If the list is empty, make front and rear both point to node + # If the linked list is empty, make both front and rear point to node if self.is_empty(): self._front = self._rear = node - # Front enqueue operation + # Front of the queue enqueue operation elif is_front: - # Add node to the head of the list + # Add node to the head of the linked list self._front.prev = node node.next = self._front self._front = node # Update head node - # Rear enqueue operation + # Rear of the queue enqueue operation else: - # Add node to the tail of the list + # Add node to the tail of the linked list self._rear.next = node node.prev = self._rear self._rear = node # Update tail node self._size += 1 # Update queue length def push_first(self, num: int): - """Front enqueue""" + """Front of the queue enqueue""" self.push(num, True) def push_last(self, num: int): - """Rear enqueue""" + """Rear of the queue enqueue""" self.push(num, False) def pop(self, is_front: bool) -> int: """Dequeue operation""" if self.is_empty(): raise IndexError("Double-ended queue is empty") - # Front dequeue operation + # Front of the queue dequeue operation if is_front: - val: int = self._front.val # Temporarily store the head node value - # Remove head node + val: int = self._front.val # Temporarily store head node value + # Delete head node fnext: ListNode | None = self._front.next if fnext is not None: fnext.prev = None self._front.next = None self._front = fnext # Update head node - # Rear dequeue operation + # Rear of the queue dequeue operation else: - val: int = self._rear.val # Temporarily store the tail node value - # Remove tail node + val: int = self._rear.val # Temporarily store tail node value + # Delete tail node rprev: ListNode | None = self._rear.prev if rprev is not None: rprev.next = None @@ -464,21 +512,21 @@ The implementation code is as follows: return val def pop_first(self) -> int: - """Front dequeue""" + """Front of the queue dequeue""" return self.pop(True) def pop_last(self) -> int: - """Rear dequeue""" + """Rear of the queue dequeue""" return self.pop(False) def peek_first(self) -> int: - """Access front element""" + """Access front of the queue element""" if self.is_empty(): raise IndexError("Double-ended queue is empty") return self._front.val def peek_last(self) -> int: - """Access rear element""" + """Access rear of the queue element""" if self.is_empty(): raise IndexError("Double-ended queue is empty") return self._rear.val @@ -496,19 +544,19 @@ The implementation code is as follows: === "C++" ```cpp title="linkedlist_deque.cpp" - /* Double-linked list node */ + /* Doubly linked list node */ struct DoublyListNode { int val; // Node value - DoublyListNode *next; // Pointer to successor node - DoublyListNode *prev; // Pointer to predecessor node + DoublyListNode *next; // Successor node pointer + DoublyListNode *prev; // Predecessor node pointer DoublyListNode(int val) : val(val), prev(nullptr), next(nullptr) { } }; - /* Double-ended queue class based on double-linked list */ + /* Double-ended queue based on doubly linked list implementation */ class LinkedListDeque { private: - DoublyListNode *front, *rear; // Front node front, back node rear + DoublyListNode *front, *rear; // Head node front, tail node rear int queSize = 0; // Length of the double-ended queue public: @@ -518,7 +566,7 @@ The implementation code is as follows: /* Destructor */ ~LinkedListDeque() { - // Traverse the linked list, remove nodes, free memory + // Traverse linked list to delete nodes and free memory DoublyListNode *pre, *cur = front; while (cur != nullptr) { pre = cur; @@ -532,7 +580,7 @@ The implementation code is as follows: return queSize; } - /* Determine if the double-ended queue is empty */ + /* Check if the double-ended queue is empty */ bool isEmpty() { return size() == 0; } @@ -540,18 +588,18 @@ The implementation code is as follows: /* Enqueue operation */ void push(int num, bool isFront) { DoublyListNode *node = new DoublyListNode(num); - // If the list is empty, make front and rear both point to node + // If the linked list is empty, make both front and rear point to node if (isEmpty()) front = rear = node; - // Front enqueue operation + // Front of the queue enqueue operation else if (isFront) { - // Add node to the head of the list + // Add node to the head of the linked list front->prev = node; node->next = front; front = node; // Update head node - // Rear enqueue operation + // Rear of the queue enqueue operation } else { - // Add node to the tail of the list + // Add node to the tail of the linked list rear->next = node; node->prev = rear; rear = node; // Update tail node @@ -559,12 +607,12 @@ The implementation code is as follows: queSize++; // Update queue length } - /* Front enqueue */ + /* Front of the queue enqueue */ void pushFirst(int num) { push(num, true); } - /* Rear enqueue */ + /* Rear of the queue enqueue */ void pushLast(int num) { push(num, false); } @@ -574,10 +622,10 @@ The implementation code is as follows: if (isEmpty()) throw out_of_range("Queue is empty"); int val; - // Front dequeue operation + // Temporarily store head node value if (isFront) { - val = front->val; // Temporarily store the head node value - // Remove head node + val = front->val; // Delete head node + // Delete head node DoublyListNode *fNext = front->next; if (fNext != nullptr) { fNext->prev = nullptr; @@ -585,10 +633,10 @@ The implementation code is as follows: } delete front; front = fNext; // Update head node - // Rear dequeue operation + // Temporarily store tail node value } else { - val = rear->val; // Temporarily store the tail node value - // Remove tail node + val = rear->val; // Delete tail node + // Update tail node DoublyListNode *rPrev = rear->prev; if (rPrev != nullptr) { rPrev->next = nullptr; @@ -601,27 +649,27 @@ The implementation code is as follows: return val; } - /* Front dequeue */ + /* Rear of the queue dequeue */ int popFirst() { return pop(true); } - /* Rear dequeue */ + /* Access rear of the queue element */ int popLast() { return pop(false); } - /* Access front element */ + /* Return list for printing */ int peekFirst() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("Deque is empty"); return front->val; } - /* Access rear element */ + /* Driver Code */ int peekLast() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("Deque is empty"); return rear->val; } @@ -641,11 +689,11 @@ The implementation code is as follows: === "Java" ```java title="linkedlist_deque.java" - /* Double-linked list node */ + /* Doubly linked list node */ class ListNode { int val; // Node value - ListNode next; // Reference to successor node - ListNode prev; // Reference to predecessor node + ListNode next; // Successor node reference + ListNode prev; // Predecessor node reference ListNode(int val) { this.val = val; @@ -653,9 +701,9 @@ The implementation code is as follows: } } - /* Double-ended queue class based on double-linked list */ + /* Double-ended queue based on doubly linked list implementation */ class LinkedListDeque { - private ListNode front, rear; // Front node front, back node rear + private ListNode front, rear; // Head node front, tail node rear private int queSize = 0; // Length of the double-ended queue public LinkedListDeque() { @@ -667,7 +715,7 @@ The implementation code is as follows: return queSize; } - /* Determine if the double-ended queue is empty */ + /* Check if the double-ended queue is empty */ public boolean isEmpty() { return size() == 0; } @@ -675,18 +723,18 @@ The implementation code is as follows: /* Enqueue operation */ private void push(int num, boolean isFront) { ListNode node = new ListNode(num); - // If the list is empty, make front and rear both point to node + // If the linked list is empty, make both front and rear point to node if (isEmpty()) front = rear = node; - // Front enqueue operation + // Front of the queue enqueue operation else if (isFront) { - // Add node to the head of the list + // Add node to the head of the linked list front.prev = node; node.next = front; front = node; // Update head node - // Rear enqueue operation + // Rear of the queue enqueue operation } else { - // Add node to the tail of the list + // Add node to the tail of the linked list rear.next = node; node.prev = rear; rear = node; // Update tail node @@ -694,12 +742,12 @@ The implementation code is as follows: queSize++; // Update queue length } - /* Front enqueue */ + /* Front of the queue enqueue */ public void pushFirst(int num) { push(num, true); } - /* Rear enqueue */ + /* Rear of the queue enqueue */ public void pushLast(int num) { push(num, false); } @@ -709,20 +757,20 @@ The implementation code is as follows: if (isEmpty()) throw new IndexOutOfBoundsException(); int val; - // Front dequeue operation + // Temporarily store head node value if (isFront) { - val = front.val; // Temporarily store the head node value - // Remove head node + val = front.val; // Delete head node + // Delete head node ListNode fNext = front.next; if (fNext != null) { fNext.prev = null; front.next = null; } front = fNext; // Update head node - // Rear dequeue operation + // Temporarily store tail node value } else { - val = rear.val; // Temporarily store the tail node value - // Remove tail node + val = rear.val; // Delete tail node + // Update tail node ListNode rPrev = rear.prev; if (rPrev != null) { rPrev.next = null; @@ -734,24 +782,24 @@ The implementation code is as follows: return val; } - /* Front dequeue */ + /* Rear of the queue dequeue */ public int popFirst() { return pop(true); } - /* Rear dequeue */ + /* Access rear of the queue element */ public int popLast() { return pop(false); } - /* Access front element */ + /* Return list for printing */ public int peekFirst() { if (isEmpty()) throw new IndexOutOfBoundsException(); return front.val; } - /* Access rear element */ + /* Driver Code */ public int peekLast() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -774,117 +822,1366 @@ The implementation code is as follows: === "C#" ```csharp title="linkedlist_deque.cs" - [class]{ListNode}-[func]{} + /* Doubly linked list node */ + class ListNode(int val) { + public int val = val; // Node value + public ListNode? next = null; // Successor node reference + public ListNode? prev = null; // Predecessor node reference + } - [class]{LinkedListDeque}-[func]{} + /* Double-ended queue based on doubly linked list implementation */ + class LinkedListDeque { + ListNode? front, rear; // Head node front, tail node rear + int queSize = 0; // Length of the double-ended queue + + public LinkedListDeque() { + front = null; + rear = null; + } + + /* Get the length of the double-ended queue */ + public int Size() { + return queSize; + } + + /* Check if the double-ended queue is empty */ + public bool IsEmpty() { + return Size() == 0; + } + + /* Enqueue operation */ + void Push(int num, bool isFront) { + ListNode node = new(num); + // If the linked list is empty, make both front and rear point to node + if (IsEmpty()) { + front = node; + rear = node; + } + // Front of the queue enqueue operation + else if (isFront) { + // Add node to the head of the linked list + front!.prev = node; + node.next = front; + front = node; // Update head node + } + // Rear of the queue enqueue operation + else { + // Add node to the tail of the linked list + rear!.next = node; + node.prev = rear; + rear = node; // Update tail node + } + + queSize++; // Update queue length + } + + /* Front of the queue enqueue */ + public void PushFirst(int num) { + Push(num, true); + } + + /* Rear of the queue enqueue */ + public void PushLast(int num) { + Push(num, false); + } + + /* Dequeue operation */ + int? Pop(bool isFront) { + if (IsEmpty()) + throw new Exception(); + int? val; + // Temporarily store head node value + if (isFront) { + val = front?.val; // Delete head node + // Delete head node + ListNode? fNext = front?.next; + if (fNext != null) { + fNext.prev = null; + front!.next = null; + } + front = fNext; // Update head node + } + // Temporarily store tail node value + else { + val = rear?.val; // Delete tail node + // Update tail node + ListNode? rPrev = rear?.prev; + if (rPrev != null) { + rPrev.next = null; + rear!.prev = null; + } + rear = rPrev; // Update tail node + } + + queSize--; // Update queue length + return val; + } + + /* Rear of the queue dequeue */ + public int? PopFirst() { + return Pop(true); + } + + /* Access rear of the queue element */ + public int? PopLast() { + return Pop(false); + } + + /* Return list for printing */ + public int? PeekFirst() { + if (IsEmpty()) + throw new Exception(); + return front?.val; + } + + /* Driver Code */ + public int? PeekLast() { + if (IsEmpty()) + throw new Exception(); + return rear?.val; + } + + /* Return array for printing */ + 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" - [class]{linkedListDeque}-[func]{} + /* Double-ended queue based on doubly linked list implementation */ + type linkedListDeque struct { + // Use built-in package list + data *list.List + } + + /* Initialize deque */ + func newLinkedListDeque() *linkedListDeque { + return &linkedListDeque{ + data: list.New(), + } + } + + /* Front element enqueue */ + func (s *linkedListDeque) pushFirst(value any) { + s.data.PushFront(value) + } + + /* Rear element enqueue */ + func (s *linkedListDeque) pushLast(value any) { + s.data.PushBack(value) + } + + /* Check if the double-ended queue is empty */ + func (s *linkedListDeque) popFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* Rear element dequeue */ + func (s *linkedListDeque) popLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* Return list for printing */ + func (s *linkedListDeque) peekFirst() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* Driver Code */ + func (s *linkedListDeque) peekLast() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* Get the length of the queue */ + func (s *linkedListDeque) size() int { + return s.data.Len() + } + + /* Check if the queue is empty */ + func (s *linkedListDeque) isEmpty() bool { + return s.data.Len() == 0 + } + + /* Get List for printing */ + func (s *linkedListDeque) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_deque.swift" - [class]{ListNode}-[func]{} + /* Doubly linked list node */ + class ListNode { + var val: Int // Node value + var next: ListNode? // Successor node reference + weak var prev: ListNode? // Predecessor node reference - [class]{LinkedListDeque}-[func]{} + init(val: Int) { + self.val = val + } + } + + /* Double-ended queue based on doubly linked list implementation */ + class LinkedListDeque { + private var front: ListNode? // Head node front + private var rear: ListNode? // Tail node rear + private var _size: Int // Length of the double-ended queue + + init() { + _size = 0 + } + + /* Get the length of the double-ended queue */ + func size() -> Int { + _size + } + + /* Check if the double-ended queue is empty */ + func isEmpty() -> Bool { + size() == 0 + } + + /* Enqueue operation */ + private func push(num: Int, isFront: Bool) { + let node = ListNode(val: num) + // If the linked list is empty, make both front and rear point to node + if isEmpty() { + front = node + rear = node + } + // Front of the queue enqueue operation + else if isFront { + // Add node to the head of the linked list + front?.prev = node + node.next = front + front = node // Update head node + } + // Rear of the queue enqueue operation + else { + // Add node to the tail of the linked list + rear?.next = node + node.prev = rear + rear = node // Update tail node + } + _size += 1 // Update queue length + } + + /* Front of the queue enqueue */ + func pushFirst(num: Int) { + push(num: num, isFront: true) + } + + /* Rear of the queue enqueue */ + func pushLast(num: Int) { + push(num: num, isFront: false) + } + + /* Dequeue operation */ + private func pop(isFront: Bool) -> Int { + if isEmpty() { + fatalError("Deque is empty") + } + let val: Int + // Temporarily store head node value + if isFront { + val = front!.val // Delete head node + // Delete head node + let fNext = front?.next + if fNext != nil { + fNext?.prev = nil + front?.next = nil + } + front = fNext // Update head node + } + // Temporarily store tail node value + else { + val = rear!.val // Delete tail node + // Update tail node + let rPrev = rear?.prev + if rPrev != nil { + rPrev?.next = nil + rear?.prev = nil + } + rear = rPrev // Update tail node + } + _size -= 1 // Update queue length + return val + } + + /* Rear of the queue dequeue */ + func popFirst() -> Int { + pop(isFront: true) + } + + /* Access rear of the queue element */ + func popLast() -> Int { + pop(isFront: false) + } + + /* Return list for printing */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("Deque is empty") + } + return front!.val + } + + /* Driver Code */ + func peekLast() -> Int { + if isEmpty() { + fatalError("Deque is empty") + } + return rear!.val + } + + /* Return array for printing */ + 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}-[func]{} + /* Doubly linked list node */ + class ListNode { + prev; // Predecessor node reference (pointer) + next; // Successor node reference (pointer) + val; // Node value - [class]{LinkedListDeque}-[func]{} + constructor(val) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* Double-ended queue based on doubly linked list implementation */ + class LinkedListDeque { + #front; // Head node front + #rear; // Tail node rear + #queSize; // Length of the double-ended queue + + constructor() { + this.#front = null; + this.#rear = null; + this.#queSize = 0; + } + + /* Rear of the queue enqueue operation */ + pushLast(val) { + const node = new ListNode(val); + // If the linked list is empty, make both front and rear point to node + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // Add node to the tail of the linked list + this.#rear.next = node; + node.prev = this.#rear; + this.#rear = node; // Update tail node + } + this.#queSize++; + } + + /* Front of the queue enqueue operation */ + pushFirst(val) { + const node = new ListNode(val); + // If the linked list is empty, make both front and rear point to node + if (this.#queSize === 0) { + this.#front = node; + this.#rear = node; + } else { + // Add node to the head of the linked list + this.#front.prev = node; + node.next = this.#front; + this.#front = node; // Update head node + } + this.#queSize++; + } + + /* Temporarily store tail node value */ + popLast() { + if (this.#queSize === 0) { + return null; + } + const value = this.#rear.val; // Store tail node value + // Update tail node + let temp = this.#rear.prev; + if (temp !== null) { + temp.next = null; + this.#rear.prev = null; + } + this.#rear = temp; // Update tail node + this.#queSize--; + return value; + } + + /* Temporarily store head node value */ + popFirst() { + if (this.#queSize === 0) { + return null; + } + const value = this.#front.val; // Store tail node value + // Delete head node + let temp = this.#front.next; + if (temp !== null) { + temp.prev = null; + this.#front.next = null; + } + this.#front = temp; // Update head node + this.#queSize--; + return value; + } + + /* Driver Code */ + peekLast() { + return this.#queSize === 0 ? null : this.#rear.val; + } + + /* Return list for printing */ + peekFirst() { + return this.#queSize === 0 ? null : this.#front.val; + } + + /* Get the length of the double-ended queue */ + size() { + return this.#queSize; + } + + /* Check if the double-ended queue is empty */ + isEmpty() { + return this.#queSize === 0; + } + + /* Print deque */ + 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}-[func]{} + /* Doubly linked list node */ + class ListNode { + prev: ListNode; // Predecessor node reference (pointer) + next: ListNode; // Successor node reference (pointer) + val: number; // Node value - [class]{LinkedListDeque}-[func]{} + constructor(val: number) { + this.val = val; + this.next = null; + this.prev = null; + } + } + + /* Double-ended queue based on doubly linked list implementation */ + class LinkedListDeque { + private front: ListNode; // Head node front + private rear: ListNode; // Tail node rear + private queSize: number; // Length of the double-ended queue + + constructor() { + this.front = null; + this.rear = null; + this.queSize = 0; + } + + /* Rear of the queue enqueue operation */ + pushLast(val: number): void { + const node: ListNode = new ListNode(val); + // If the linked list is empty, make both front and rear point to node + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // Add node to the tail of the linked list + this.rear.next = node; + node.prev = this.rear; + this.rear = node; // Update tail node + } + this.queSize++; + } + + /* Front of the queue enqueue operation */ + pushFirst(val: number): void { + const node: ListNode = new ListNode(val); + // If the linked list is empty, make both front and rear point to node + if (this.queSize === 0) { + this.front = node; + this.rear = node; + } else { + // Add node to the head of the linked list + this.front.prev = node; + node.next = this.front; + this.front = node; // Update head node + } + this.queSize++; + } + + /* Temporarily store tail node value */ + popLast(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.rear.val; // Store tail node value + // Update tail node + let temp: ListNode = this.rear.prev; + if (temp !== null) { + temp.next = null; + this.rear.prev = null; + } + this.rear = temp; // Update tail node + this.queSize--; + return value; + } + + /* Temporarily store head node value */ + popFirst(): number { + if (this.queSize === 0) { + return null; + } + const value: number = this.front.val; // Store tail node value + // Delete head node + let temp: ListNode = this.front.next; + if (temp !== null) { + temp.prev = null; + this.front.next = null; + } + this.front = temp; // Update head node + this.queSize--; + return value; + } + + /* Driver Code */ + peekLast(): number { + return this.queSize === 0 ? null : this.rear.val; + } + + /* Return list for printing */ + peekFirst(): number { + return this.queSize === 0 ? null : this.front.val; + } + + /* Get the length of the double-ended queue */ + size(): number { + return this.queSize; + } + + /* Check if the double-ended queue is empty */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* Print deque */ + 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}-[func]{} + /* Doubly linked list node */ + class ListNode { + int val; // Node value + ListNode? next; // Successor node reference + ListNode? prev; // Predecessor node reference - [class]{LinkedListDeque}-[func]{} + ListNode(this.val, {this.next, this.prev}); + } + + /* Deque implemented based on doubly linked list */ + class LinkedListDeque { + late ListNode? _front; // Head node _front + late ListNode? _rear; // Tail node _rear + int _queSize = 0; // Length of the double-ended queue + + LinkedListDeque() { + this._front = null; + this._rear = null; + } + + /* Get deque length */ + int size() { + return this._queSize; + } + + /* Check if the double-ended queue is empty */ + bool isEmpty() { + return size() == 0; + } + + /* Enqueue operation */ + void push(int _num, bool isFront) { + final ListNode node = ListNode(_num); + if (isEmpty()) { + // If list is empty, let both _front and _rear point to node + _front = _rear = node; + } else if (isFront) { + // Front of the queue enqueue operation + // Add node to the head of the linked list + _front!.prev = node; + node.next = _front; + _front = node; // Update head node + } else { + // Rear of the queue enqueue operation + // Add node to the tail of the linked list + _rear!.next = node; + node.prev = _rear; + _rear = node; // Update tail node + } + _queSize++; // Update queue length + } + + /* Front of the queue enqueue */ + void pushFirst(int _num) { + push(_num, true); + } + + /* Rear of the queue enqueue */ + void pushLast(int _num) { + push(_num, false); + } + + /* Dequeue operation */ + int? pop(bool isFront) { + // If queue is empty, return null directly + if (isEmpty()) { + return null; + } + final int val; + if (isFront) { + // Temporarily store head node value + val = _front!.val; // Delete head node + // Delete head node + ListNode? fNext = _front!.next; + if (fNext != null) { + fNext.prev = null; + _front!.next = null; + } + _front = fNext; // Update head node + } else { + // Temporarily store tail node value + val = _rear!.val; // Delete tail node + // Update tail node + ListNode? rPrev = _rear!.prev; + if (rPrev != null) { + rPrev.next = null; + _rear!.prev = null; + } + _rear = rPrev; // Update tail node + } + _queSize--; // Update queue length + return val; + } + + /* Rear of the queue dequeue */ + int? popFirst() { + return pop(true); + } + + /* Access rear of the queue element */ + int? popLast() { + return pop(false); + } + + /* Return list for printing */ + int? peekFirst() { + return _front?.val; + } + + /* Driver Code */ + int? peekLast() { + return _rear?.val; + } + + /* Return array for printing */ + 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" - [class]{ListNode}-[func]{} + /* Doubly linked list node */ + pub struct ListNode { + pub val: T, // Node value + pub next: Option>>>, // Successor node pointer + pub prev: Option>>>, // Predecessor node pointer + } - [class]{LinkedListDeque}-[func]{} + impl ListNode { + pub fn new(val: T) -> Rc>> { + Rc::new(RefCell::new(ListNode { + val, + next: None, + prev: None, + })) + } + } + + /* Double-ended queue based on doubly linked list implementation */ + #[allow(dead_code)] + pub struct LinkedListDeque { + front: Option>>>, // Head node front + rear: Option>>>, // Tail node rear + que_size: usize, // Length of the double-ended queue + } + + impl LinkedListDeque { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* Get the length of the double-ended queue */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* Check if the double-ended queue is empty */ + pub fn is_empty(&self) -> bool { + return self.que_size == 0; + } + + /* Enqueue operation */ + fn push(&mut self, num: T, is_front: bool) { + let node = ListNode::new(num); + // Front of the queue enqueue operation + if is_front { + match self.front.take() { + // If the linked list is empty, make both front and rear point to node + None => { + self.rear = Some(node.clone()); + self.front = Some(node); + } + // Add node to the head of the linked list + Some(old_front) => { + old_front.borrow_mut().prev = Some(node.clone()); + node.borrow_mut().next = Some(old_front); + self.front = Some(node); // Update head node + } + } + } + // Rear of the queue enqueue operation + else { + match self.rear.take() { + // If the linked list is empty, make both front and rear point to node + None => { + self.front = Some(node.clone()); + self.rear = Some(node); + } + // Add node to the tail of the linked list + Some(old_rear) => { + old_rear.borrow_mut().next = Some(node.clone()); + node.borrow_mut().prev = Some(old_rear); + self.rear = Some(node); // Update tail node + } + } + } + self.que_size += 1; // Update queue length + } + + /* Front of the queue enqueue */ + pub fn push_first(&mut self, num: T) { + self.push(num, true); + } + + /* Rear of the queue enqueue */ + pub fn push_last(&mut self, num: T) { + self.push(num, false); + } + + /* Dequeue operation */ + fn pop(&mut self, is_front: bool) -> Option { + // If queue is empty, return None directly + if self.is_empty() { + return None; + }; + // Temporarily store head node value + 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); // Update head node + } + None => { + self.rear.take(); + } + } + self.que_size -= 1; // Update queue length + old_front.borrow().val + }) + } + // Temporarily store tail node value + 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); // Update tail node + } + None => { + self.front.take(); + } + } + self.que_size -= 1; // Update queue length + old_rear.borrow().val + }) + } + } + + /* Rear of the queue dequeue */ + pub fn pop_first(&mut self) -> Option { + return self.pop(true); + } + + /* Access rear of the queue element */ + pub fn pop_last(&mut self) -> Option { + return self.pop(false); + } + + /* Return list for printing */ + pub fn peek_first(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* Driver Code */ + pub fn peek_last(&self) -> Option<&Rc>>> { + self.rear.as_ref() + } + + /* Return array for printing */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + let mut res: Vec = Vec::new(); + fn recur(cur: Option<&Rc>>>, res: &mut Vec) { + if let Some(cur) = cur { + res.push(cur.borrow().val); + recur(cur.borrow().next.as_ref(), res); + } + } + + recur(head, &mut res); + res + } + } ``` === "C" ```c title="linkedlist_deque.c" - [class]{DoublyListNode}-[func]{} + /* Doubly linked list node */ + typedef struct DoublyListNode { + int val; // Node value + struct DoublyListNode *next; // Successor node + struct DoublyListNode *prev; // Predecessor node + } DoublyListNode; - [class]{LinkedListDeque}-[func]{} + /* Constructor */ + DoublyListNode *newDoublyListNode(int num) { + DoublyListNode *new = (DoublyListNode *)malloc(sizeof(DoublyListNode)); + new->val = num; + new->next = NULL; + new->prev = NULL; + return new; + } + + /* Destructor */ + void delDoublyListNode(DoublyListNode *node) { + free(node); + } + + /* Double-ended queue based on doubly linked list implementation */ + typedef struct { + DoublyListNode *front, *rear; // Head node front, tail node rear + int queSize; // Length of the double-ended queue + } LinkedListDeque; + + /* Constructor */ + LinkedListDeque *newLinkedListDeque() { + LinkedListDeque *deque = (LinkedListDeque *)malloc(sizeof(LinkedListDeque)); + deque->front = NULL; + deque->rear = NULL; + deque->queSize = 0; + return deque; + } + + /* Destructor */ + void delLinkedListdeque(LinkedListDeque *deque) { + // Free all nodes + for (int i = 0; i < deque->queSize && deque->front != NULL; i++) { + DoublyListNode *tmp = deque->front; + deque->front = deque->front->next; + free(tmp); + } + // Free deque structure + free(deque); + } + + /* Get the length of the queue */ + int size(LinkedListDeque *deque) { + return deque->queSize; + } + + /* Check if the queue is empty */ + bool empty(LinkedListDeque *deque) { + return (size(deque) == 0); + } + + /* Enqueue */ + void push(LinkedListDeque *deque, int num, bool isFront) { + DoublyListNode *node = newDoublyListNode(num); + // If list is empty, set both front and rear to node + if (empty(deque)) { + deque->front = deque->rear = node; + } + // Front of the queue enqueue operation + else if (isFront) { + // Add node to the head of the linked list + deque->front->prev = node; + node->next = deque->front; + deque->front = node; // Update head node + } + // Rear of the queue enqueue operation + else { + // Add node to the tail of the linked list + deque->rear->next = node; + node->prev = deque->rear; + deque->rear = node; + } + deque->queSize++; // Update queue length + } + + /* Front of the queue enqueue */ + void pushFirst(LinkedListDeque *deque, int num) { + push(deque, num, true); + } + + /* Rear of the queue enqueue */ + void pushLast(LinkedListDeque *deque, int num) { + push(deque, num, false); + } + + /* Return list for printing */ + int peekFirst(LinkedListDeque *deque) { + assert(size(deque) && deque->front); + return deque->front->val; + } + + /* Driver Code */ + int peekLast(LinkedListDeque *deque) { + assert(size(deque) && deque->rear); + return deque->rear->val; + } + + /* Dequeue */ + int pop(LinkedListDeque *deque, bool isFront) { + if (empty(deque)) + return -1; + int val; + // Temporarily store head node value + if (isFront) { + val = peekFirst(deque); // Delete head node + DoublyListNode *fNext = deque->front->next; + if (fNext) { + fNext->prev = NULL; + deque->front->next = NULL; + } + delDoublyListNode(deque->front); + deque->front = fNext; // Update head node + } + // Temporarily store tail node value + else { + val = peekLast(deque); // Delete tail node + DoublyListNode *rPrev = deque->rear->prev; + if (rPrev) { + rPrev->next = NULL; + deque->rear->prev = NULL; + } + delDoublyListNode(deque->rear); + deque->rear = rPrev; // Update tail node + } + deque->queSize--; // Update queue length + return val; + } + + /* Rear of the queue dequeue */ + int popFirst(LinkedListDeque *deque) { + return pop(deque, true); + } + + /* Access rear of the queue element */ + int popLast(LinkedListDeque *deque) { + return pop(deque, false); + } + + /* Print queue */ + void printLinkedListDeque(LinkedListDeque *deque) { + int *arr = malloc(sizeof(int) * deque->queSize); + // Copy data from list to array + int i; + DoublyListNode *node; + for (i = 0, node = deque->front; i < deque->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, deque->queSize); + free(arr); + } ``` === "Kotlin" ```kotlin title="linkedlist_deque.kt" - [class]{ListNode}-[func]{} + /* Doubly linked list node */ + class ListNode(var _val: Int) { + // Node value + var next: ListNode? = null // Successor node reference + var prev: ListNode? = null // Predecessor node reference + } - [class]{LinkedListDeque}-[func]{} + /* Double-ended queue based on doubly linked list implementation */ + class LinkedListDeque { + private var front: ListNode? = null // Head node front + private var rear: ListNode? = null // Tail node rear + private var queSize: Int = 0 // Length of the double-ended queue + + /* Get the length of the double-ended queue */ + fun size(): Int { + return queSize + } + + /* Check if the double-ended queue is empty */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* Enqueue operation */ + fun push(num: Int, isFront: Boolean) { + val node = ListNode(num) + // If the linked list is empty, make both front and rear point to node + if (isEmpty()) { + rear = node + front = rear + // Front of the queue enqueue operation + } else if (isFront) { + // Add node to the head of the linked list + front?.prev = node + node.next = front + front = node // Update head node + // Rear of the queue enqueue operation + } else { + // Add node to the tail of the linked list + rear?.next = node + node.prev = rear + rear = node // Update tail node + } + queSize++ // Update queue length + } + + /* Front of the queue enqueue */ + fun pushFirst(num: Int) { + push(num, true) + } + + /* Rear of the queue enqueue */ + fun pushLast(num: Int) { + push(num, false) + } + + /* Dequeue operation */ + fun pop(isFront: Boolean): Int { + if (isEmpty()) + throw IndexOutOfBoundsException() + val _val: Int + // Temporarily store head node value + if (isFront) { + _val = front!!._val // Delete head node + // Delete head node + val fNext = front!!.next + if (fNext != null) { + fNext.prev = null + front!!.next = null + } + front = fNext // Update head node + // Temporarily store tail node value + } else { + _val = rear!!._val // Delete tail node + // Update tail node + val rPrev = rear!!.prev + if (rPrev != null) { + rPrev.next = null + rear!!.prev = null + } + rear = rPrev // Update tail node + } + queSize-- // Update queue length + return _val + } + + /* Rear of the queue dequeue */ + fun popFirst(): Int { + return pop(true) + } + + /* Access rear of the queue element */ + fun popLast(): Int { + return pop(false) + } + + /* Return list for printing */ + fun peekFirst(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return front!!._val + } + + /* Driver Code */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return rear!!._val + } + + /* Return array for printing */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!._val + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_deque.rb" - [class]{ListNode}-[func]{} + =begin + File: linkedlist_deque.rb + Created Time: 2024-04-06 + Author: Xuan Khoa Tu Nguyen (ngxktuzkai2000@gmail.com) + =end - [class]{LinkedListDeque}-[func]{} + ### Doubly linked list node + class ListNode + attr_accessor :val + attr_accessor :next # Successor node reference + attr_accessor :prev # Predecessor node reference + + ### Constructor ### + def initialize(val) + @val = val + end + end + + ### Deque based on doubly linked list ### + class LinkedListDeque + ### Get deque length ### + attr_reader :size + + ### Constructor ### + def initialize + @front = nil # Head node front + @rear = nil # Tail node rear + @size = 0 # Length of the double-ended queue + end + + ### Check if deque is empty ### + def is_empty? + size.zero? + end + + ### Enqueue operation ### + def push(num, is_front) + node = ListNode.new(num) + # If list is empty, set both front and rear to node + if is_empty? + @front = @rear = node + # Front of the queue enqueue operation + elsif is_front + # Add node to the head of the linked list + @front.prev = node + node.next = @front + @front = node # Update head node + # Rear of the queue enqueue operation + else + # Add node to the tail of the linked list + @rear.next = node + node.prev = @rear + @rear = node # Update tail node + end + @size += 1 # Update queue length + end + + ### Enqueue at front ### + def push_first(num) + push(num, true) + end + + ### Enqueue at rear ### + def push_last(num) + push(num, false) + end + + ### Dequeue operation ### + def pop(is_front) + raise IndexError, 'Deque is empty' if is_empty? + + # Temporarily store head node value + if is_front + val = @front.val # Delete head node + # Delete head node + fnext = @front.next + unless fnext.nil? + fnext.prev = nil + @front.next = nil + end + @front = fnext # Update head node + # Temporarily store tail node value + else + val = @rear.val # Delete tail node + # Update tail node + rprev = @rear.prev + unless rprev.nil? + rprev.next = nil + @rear.prev = nil + end + @rear = rprev # Update tail node + end + @size -= 1 # Update queue length + + val + end + + ### Dequeue from front ### + def pop_first + pop(true) + end + + ### Dequeue from front ### + def pop_last + pop(false) + end + + ### Access front element ### + def peek_first + raise IndexError, 'Deque is empty' if is_empty? + + @front.val + end + + ### Access rear element ### + def peek_last + raise IndexError, 'Deque is empty' if is_empty? + + @rear.val + end + + ### Return array for printing ### + def to_array + node = @front + res = Array.new(size, 0) + for i in 0...size + res[i] = node.val + node = node.next + end + res + end + end ``` -=== "Zig" +### 2.   Array Implementation - ```zig title="linkedlist_deque.zig" - [class]{ListNode}-[func]{} - - [class]{LinkedListDeque}-[func]{} - ``` - -### 2.   Implementation based on array - -As shown in Figure 5-9, similar to implementing a queue with an array, we can also use a circular array to implement a double-ended queue. +As shown in Figure 5-9, similar to implementing a queue based on an array, we can also use a circular array to implement a deque. === "ArrayDeque" - ![Implementing Double-Ended Queue with Array for Enqueue and Dequeue Operations](deque.assets/array_deque_step1.png){ class="animation-figure" } + ![Enqueue and dequeue operations in array implementation of deque](deque.assets/array_deque_step1.png){ class="animation-figure" } -=== "pushLast()" +=== "push_last()" ![array_deque_push_last](deque.assets/array_deque_step2_push_last.png){ class="animation-figure" } -=== "pushFirst()" +=== "push_first()" ![array_deque_push_first](deque.assets/array_deque_step3_push_first.png){ class="animation-figure" } -=== "popLast()" +=== "pop_last()" ![array_deque_pop_last](deque.assets/array_deque_step4_pop_last.png){ class="animation-figure" } -=== "popFirst()" +=== "pop_first()" ![array_deque_pop_first](deque.assets/array_deque_step5_pop_first.png){ class="animation-figure" } -

Figure 5-9   Implementing Double-Ended Queue with Array for Enqueue and Dequeue Operations

+

Figure 5-9   Enqueue and dequeue operations in array implementation of deque

-The implementation only needs to add methods for "front enqueue" and "rear dequeue": +Based on the queue implementation, we only need to add methods for "enqueue at front" and "dequeue from rear": === "Python" ```python title="array_deque.py" class ArrayDeque: - """Double-ended queue class based on circular array""" + """Double-ended queue based on circular array implementation""" def __init__(self, capacity: int): """Constructor""" @@ -901,70 +2198,70 @@ The implementation only needs to add methods for "front enqueue" and "rear deque return self._size def is_empty(self) -> bool: - """Determine if the double-ended queue is empty""" + """Check if the double-ended queue is empty""" return self._size == 0 def index(self, i: int) -> int: """Calculate circular array index""" - # Implement circular array by modulo operation - # When i exceeds the tail of the array, return to the head - # When i exceeds the head of the array, return to the tail + # Use modulo operation to wrap the array head and tail together + # When i passes the tail of the array, return to the head + # When i passes the head of the array, return to the tail return (i + self.capacity()) % self.capacity() def push_first(self, num: int): - """Front enqueue""" + """Front of the queue enqueue""" if self._size == self.capacity(): print("Double-ended queue is full") return - # Move the front pointer one position to the left - # Implement front crossing the head of the array to return to the tail by modulo operation + # Front pointer moves one position to the left + # Use modulo operation to wrap front around to the tail after passing the head of the array self._front = self.index(self._front - 1) - # Add num to the front + # Add num to the front of the queue self._nums[self._front] = num self._size += 1 def push_last(self, num: int): - """Rear enqueue""" + """Rear of the queue enqueue""" if self._size == self.capacity(): print("Double-ended queue is full") return - # Calculate rear pointer, pointing to rear index + 1 + # Calculate rear pointer, points to rear index + 1 rear = self.index(self._front + self._size) - # Add num to the rear + # Add num to the rear of the queue self._nums[rear] = num self._size += 1 def pop_first(self) -> int: - """Front dequeue""" + """Front of the queue dequeue""" num = self.peek_first() - # Move front pointer one position backward + # Front pointer moves one position backward self._front = self.index(self._front + 1) self._size -= 1 return num def pop_last(self) -> int: - """Rear dequeue""" + """Rear of the queue dequeue""" num = self.peek_last() self._size -= 1 return num def peek_first(self) -> int: - """Access front element""" + """Access front of the queue element""" if self.is_empty(): raise IndexError("Double-ended queue is empty") return self._nums[self._front] def peek_last(self) -> int: - """Access rear element""" + """Access rear of the queue element""" if self.is_empty(): raise IndexError("Double-ended queue is empty") - # Calculate rear element index + # Calculate tail element index last = self.index(self._front + self._size - 1) return self._nums[last] def to_array(self) -> list[int]: """Return array for printing""" - # Only convert elements within valid length range + # Only convert list elements within the valid length range res = [] for i in range(self._size): res.append(self._nums[self.index(self._front + i)]) @@ -974,12 +2271,12 @@ The implementation only needs to add methods for "front enqueue" and "rear deque === "C++" ```cpp title="array_deque.cpp" - /* Double-ended queue class based on circular array */ + /* Double-ended queue based on circular array implementation */ class ArrayDeque { private: - vector nums; // Array used to store elements of the double-ended queue - int front; // Front pointer, pointing to the front element - int queSize; // Length of the double-ended queue + vector nums; // Array for storing double-ended queue elements + int front; // Front pointer, points to the front of the queue element + int queSize; // Double-ended queue length public: /* Constructor */ @@ -998,81 +2295,81 @@ The implementation only needs to add methods for "front enqueue" and "rear deque return queSize; } - /* Determine if the double-ended queue is empty */ + /* Check if the double-ended queue is empty */ bool isEmpty() { return queSize == 0; } /* Calculate circular array index */ int index(int i) { - // Implement circular array by modulo operation - // When i exceeds the tail of the array, return to the head - // When i exceeds the head of the array, return to the tail + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail return (i + capacity()) % capacity(); } - /* Front enqueue */ + /* Front of the queue enqueue */ void pushFirst(int num) { if (queSize == capacity()) { cout << "Double-ended queue is full" << endl; return; } - // Move the front pointer one position to the left - // Implement front crossing the head of the array to return to the tail by modulo operation + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue front = index(front - 1); - // Add num to the front + // Add num to front of queue nums[front] = num; queSize++; } - /* Rear enqueue */ + /* Rear of the queue enqueue */ void pushLast(int num) { if (queSize == capacity()) { cout << "Double-ended queue is full" << endl; return; } - // Calculate rear pointer, pointing to rear index + 1 + // Use modulo operation to wrap rear around to the head after passing the tail of the array int rear = index(front + queSize); - // Add num to the rear + // Front pointer moves one position backward nums[rear] = num; queSize++; } - /* Front dequeue */ + /* Rear of the queue dequeue */ int popFirst() { int num = peekFirst(); - // Move front pointer one position backward + // Move front pointer backward by one position front = index(front + 1); queSize--; return num; } - /* Rear dequeue */ + /* Access rear of the queue element */ int popLast() { int num = peekLast(); queSize--; return num; } - /* Access front element */ + /* Return list for printing */ int peekFirst() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); + throw out_of_range("Deque is empty"); return nums[front]; } - /* Access rear element */ + /* Driver Code */ int peekLast() { if (isEmpty()) - throw out_of_range("Double-ended queue is empty"); - // Calculate rear element index + throw out_of_range("Deque is empty"); + // Initialize double-ended queue int last = index(front + queSize - 1); return nums[last]; } /* Return array for printing */ vector toVector() { - // Only convert elements within valid length range + // Elements enqueue vector res(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[index(j)]; @@ -1085,11 +2382,11 @@ The implementation only needs to add methods for "front enqueue" and "rear deque === "Java" ```java title="array_deque.java" - /* Double-ended queue class based on circular array */ + /* Double-ended queue based on circular array implementation */ class ArrayDeque { - private int[] nums; // Array used to store elements of the double-ended queue - private int front; // Front pointer, pointing to the front element - private int queSize; // Length of the double-ended queue + private int[] nums; // Array for storing double-ended queue elements + private int front; // Front pointer, points to the front of the queue element + private int queSize; // Double-ended queue length /* Constructor */ public ArrayDeque(int capacity) { @@ -1107,81 +2404,81 @@ The implementation only needs to add methods for "front enqueue" and "rear deque return queSize; } - /* Determine if the double-ended queue is empty */ + /* Check if the double-ended queue is empty */ public boolean isEmpty() { return queSize == 0; } /* Calculate circular array index */ private int index(int i) { - // Implement circular array by modulo operation - // When i exceeds the tail of the array, return to the head - // When i exceeds the head of the array, return to the tail + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail return (i + capacity()) % capacity(); } - /* Front enqueue */ + /* Front of the queue enqueue */ public void pushFirst(int num) { if (queSize == capacity()) { System.out.println("Double-ended queue is full"); return; } - // Move the front pointer one position to the left - // Implement front crossing the head of the array to return to the tail by modulo operation + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue front = index(front - 1); - // Add num to the front + // Add num to front of queue nums[front] = num; queSize++; } - /* Rear enqueue */ + /* Rear of the queue enqueue */ public void pushLast(int num) { if (queSize == capacity()) { System.out.println("Double-ended queue is full"); return; } - // Calculate rear pointer, pointing to rear index + 1 + // Use modulo operation to wrap rear around to the head after passing the tail of the array int rear = index(front + queSize); - // Add num to the rear + // Front pointer moves one position backward nums[rear] = num; queSize++; } - /* Front dequeue */ + /* Rear of the queue dequeue */ public int popFirst() { int num = peekFirst(); - // Move front pointer one position backward + // Move front pointer backward by one position front = index(front + 1); queSize--; return num; } - /* Rear dequeue */ + /* Access rear of the queue element */ public int popLast() { int num = peekLast(); queSize--; return num; } - /* Access front element */ + /* Return list for printing */ public int peekFirst() { if (isEmpty()) throw new IndexOutOfBoundsException(); return nums[front]; } - /* Access rear element */ + /* Driver Code */ public int peekLast() { if (isEmpty()) throw new IndexOutOfBoundsException(); - // Calculate rear element index + // Initialize double-ended queue int last = index(front + queSize - 1); return nums[last]; } /* Return array for printing */ public int[] toArray() { - // Only convert elements within valid length range + // Elements enqueue int[] res = new int[queSize]; for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[index(j)]; @@ -1194,71 +2491,1119 @@ The implementation only needs to add methods for "front enqueue" and "rear deque === "C#" ```csharp title="array_deque.cs" - [class]{ArrayDeque}-[func]{} + /* Double-ended queue based on circular array implementation */ + class ArrayDeque { + int[] nums; // Array for storing double-ended queue elements + int front; // Front pointer, points to the front of the queue element + int queSize; // Double-ended queue length + + /* Constructor */ + public ArrayDeque(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* Get the capacity of the double-ended queue */ + int Capacity() { + return nums.Length; + } + + /* Get the length of the double-ended queue */ + public int Size() { + return queSize; + } + + /* Check if the double-ended queue is empty */ + public bool IsEmpty() { + return queSize == 0; + } + + /* Calculate circular array index */ + int Index(int i) { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + Capacity()) % Capacity(); + } + + /* Front of the queue enqueue */ + public void PushFirst(int num) { + if (queSize == Capacity()) { + Console.WriteLine("Double-ended queue is full"); + return; + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + front = Index(front - 1); + // Add num to front of queue + nums[front] = num; + queSize++; + } + + /* Rear of the queue enqueue */ + public void PushLast(int num) { + if (queSize == Capacity()) { + Console.WriteLine("Double-ended queue is full"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + int rear = Index(front + queSize); + // Front pointer moves one position backward + nums[rear] = num; + queSize++; + } + + /* Rear of the queue dequeue */ + public int PopFirst() { + int num = PeekFirst(); + // Move front pointer backward by one position + front = Index(front + 1); + queSize--; + return num; + } + + /* Access rear of the queue element */ + public int PopLast() { + int num = PeekLast(); + queSize--; + return num; + } + + /* Return list for printing */ + public int PeekFirst() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + return nums[front]; + } + + /* Driver Code */ + public int PeekLast() { + if (IsEmpty()) { + throw new InvalidOperationException(); + } + // Initialize double-ended queue + int last = Index(front + queSize - 1); + return nums[last]; + } + + /* Return array for printing */ + public int[] ToArray() { + // Elements enqueue + 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" - [class]{arrayDeque}-[func]{} + /* Double-ended queue based on circular array implementation */ + type arrayDeque struct { + nums []int // Array for storing double-ended queue elements + front int // Front pointer, points to the front of the queue element + queSize int // Double-ended queue length + queCapacity int // Queue capacity (maximum number of elements) + } + + /* Access front of the queue element */ + func newArrayDeque(queCapacity int) *arrayDeque { + return &arrayDeque{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* Get the length of the double-ended queue */ + func (q *arrayDeque) size() int { + return q.queSize + } + + /* Check if the double-ended queue is empty */ + func (q *arrayDeque) isEmpty() bool { + return q.queSize == 0 + } + + /* Calculate circular array index */ + func (q *arrayDeque) index(i int) int { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + q.queCapacity) % q.queCapacity + } + + /* Front of the queue enqueue */ + func (q *arrayDeque) pushFirst(num int) { + if q.queSize == q.queCapacity { + fmt.Println("Double-ended queue is full") + return + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + q.front = q.index(q.front - 1) + // Add num to front of queue + q.nums[q.front] = num + q.queSize++ + } + + /* Rear of the queue enqueue */ + func (q *arrayDeque) pushLast(num int) { + if q.queSize == q.queCapacity { + fmt.Println("Double-ended queue is full") + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + rear := q.index(q.front + q.queSize) + // Front pointer moves one position backward + q.nums[rear] = num + q.queSize++ + } + + /* Rear of the queue dequeue */ + func (q *arrayDeque) popFirst() any { + num := q.peekFirst() + if num == nil { + return nil + } + // Move front pointer backward by one position + q.front = q.index(q.front + 1) + q.queSize-- + return num + } + + /* Access rear of the queue element */ + func (q *arrayDeque) popLast() any { + num := q.peekLast() + if num == nil { + return nil + } + q.queSize-- + return num + } + + /* Return list for printing */ + func (q *arrayDeque) peekFirst() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* Driver Code */ + func (q *arrayDeque) peekLast() any { + if q.isEmpty() { + return nil + } + // Initialize double-ended queue + last := q.index(q.front + q.queSize - 1) + return q.nums[last] + } + + /* Get Slice for printing */ + func (q *arrayDeque) toSlice() []int { + // Elements enqueue + 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}-[func]{} + /* Double-ended queue based on circular array implementation */ + class ArrayDeque { + private var nums: [Int] // Array for storing double-ended queue elements + private var front: Int // Front pointer, points to the front of the queue element + private var _size: Int // Double-ended queue length + + /* Constructor */ + init(capacity: Int) { + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* Get the capacity of the double-ended queue */ + func capacity() -> Int { + nums.count + } + + /* Get the length of the double-ended queue */ + func size() -> Int { + _size + } + + /* Check if the double-ended queue is empty */ + func isEmpty() -> Bool { + size() == 0 + } + + /* Calculate circular array index */ + private func index(i: Int) -> Int { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + (i + capacity()) % capacity() + } + + /* Front of the queue enqueue */ + func pushFirst(num: Int) { + if size() == capacity() { + print("Double-ended queue is full") + return + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + front = index(i: front - 1) + // Add num to front of queue + nums[front] = num + _size += 1 + } + + /* Rear of the queue enqueue */ + func pushLast(num: Int) { + if size() == capacity() { + print("Double-ended queue is full") + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + let rear = index(i: front + size()) + // Front pointer moves one position backward + nums[rear] = num + _size += 1 + } + + /* Rear of the queue dequeue */ + func popFirst() -> Int { + let num = peekFirst() + // Move front pointer backward by one position + front = index(i: front + 1) + _size -= 1 + return num + } + + /* Access rear of the queue element */ + func popLast() -> Int { + let num = peekLast() + _size -= 1 + return num + } + + /* Return list for printing */ + func peekFirst() -> Int { + if isEmpty() { + fatalError("Deque is empty") + } + return nums[front] + } + + /* Driver Code */ + func peekLast() -> Int { + if isEmpty() { + fatalError("Deque is empty") + } + // Initialize double-ended queue + let last = index(i: front + size() - 1) + return nums[last] + } + + /* Return array for printing */ + func toArray() -> [Int] { + // Elements enqueue + (front ..< front + size()).map { nums[index(i: $0)] } + } + } ``` === "JS" ```javascript title="array_deque.js" - [class]{ArrayDeque}-[func]{} + /* Double-ended queue based on circular array implementation */ + class ArrayDeque { + #nums; // Array for storing double-ended queue elements + #front; // Front pointer, points to the front of the queue element + #queSize; // Double-ended queue length + + /* Constructor */ + constructor(capacity) { + this.#nums = new Array(capacity); + this.#front = 0; + this.#queSize = 0; + } + + /* Get the capacity of the double-ended queue */ + capacity() { + return this.#nums.length; + } + + /* Get the length of the double-ended queue */ + size() { + return this.#queSize; + } + + /* Check if the double-ended queue is empty */ + isEmpty() { + return this.#queSize === 0; + } + + /* Calculate circular array index */ + index(i) { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + this.capacity()) % this.capacity(); + } + + /* Front of the queue enqueue */ + pushFirst(num) { + if (this.#queSize === this.capacity()) { + console.log('Double-ended queue is full'); + return; + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + this.#front = this.index(this.#front - 1); + // Add num to front of queue + this.#nums[this.#front] = num; + this.#queSize++; + } + + /* Rear of the queue enqueue */ + pushLast(num) { + if (this.#queSize === this.capacity()) { + console.log('Double-ended queue is full'); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + const rear = this.index(this.#front + this.#queSize); + // Front pointer moves one position backward + this.#nums[rear] = num; + this.#queSize++; + } + + /* Rear of the queue dequeue */ + popFirst() { + const num = this.peekFirst(); + // Move front pointer backward by one position + this.#front = this.index(this.#front + 1); + this.#queSize--; + return num; + } + + /* Access rear of the queue element */ + popLast() { + const num = this.peekLast(); + this.#queSize--; + return num; + } + + /* Return list for printing */ + peekFirst() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.#nums[this.#front]; + } + + /* Driver Code */ + peekLast() { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // Initialize double-ended queue + const last = this.index(this.#front + this.#queSize - 1); + return this.#nums[last]; + } + + /* Return array for printing */ + toArray() { + // Elements enqueue + 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}-[func]{} + /* Double-ended queue based on circular array implementation */ + class ArrayDeque { + private nums: number[]; // Array for storing double-ended queue elements + private front: number; // Front pointer, points to the front of the queue element + private queSize: number; // Double-ended queue length + + /* Constructor */ + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = 0; + this.queSize = 0; + } + + /* Get the capacity of the double-ended queue */ + capacity(): number { + return this.nums.length; + } + + /* Get the length of the double-ended queue */ + size(): number { + return this.queSize; + } + + /* Check if the double-ended queue is empty */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* Calculate circular array index */ + index(i: number): number { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + this.capacity()) % this.capacity(); + } + + /* Front of the queue enqueue */ + pushFirst(num: number): void { + if (this.queSize === this.capacity()) { + console.log('Double-ended queue is full'); + return; + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + this.front = this.index(this.front - 1); + // Add num to front of queue + this.nums[this.front] = num; + this.queSize++; + } + + /* Rear of the queue enqueue */ + pushLast(num: number): void { + if (this.queSize === this.capacity()) { + console.log('Double-ended queue is full'); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + const rear: number = this.index(this.front + this.queSize); + // Front pointer moves one position backward + this.nums[rear] = num; + this.queSize++; + } + + /* Rear of the queue dequeue */ + popFirst(): number { + const num: number = this.peekFirst(); + // Move front pointer backward by one position + this.front = this.index(this.front + 1); + this.queSize--; + return num; + } + + /* Access rear of the queue element */ + popLast(): number { + const num: number = this.peekLast(); + this.queSize--; + return num; + } + + /* Return list for printing */ + peekFirst(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + return this.nums[this.front]; + } + + /* Driver Code */ + peekLast(): number { + if (this.isEmpty()) throw new Error('The Deque Is Empty.'); + // Initialize double-ended queue + const last = this.index(this.front + this.queSize - 1); + return this.nums[last]; + } + + /* Return array for printing */ + toArray(): number[] { + // Elements enqueue + 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}-[func]{} + /* Double-ended queue based on circular array implementation */ + class ArrayDeque { + late List _nums; // Array for storing double-ended queue elements + late int _front; // Front pointer, points to the front of the queue element + late int _queSize; // Double-ended queue length + + /* Constructor */ + ArrayDeque(int capacity) { + this._nums = List.filled(capacity, 0); + this._front = this._queSize = 0; + } + + /* Get the capacity of the double-ended queue */ + int capacity() { + return _nums.length; + } + + /* Get the length of the double-ended queue */ + int size() { + return _queSize; + } + + /* Check if the double-ended queue is empty */ + bool isEmpty() { + return _queSize == 0; + } + + /* Calculate circular array index */ + int index(int i) { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + capacity()) % capacity(); + } + + /* Front of the queue enqueue */ + void pushFirst(int _num) { + if (_queSize == capacity()) { + throw Exception("Double-ended queue is full"); + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Use modulo operation to wrap _front from array head back to tail + _front = index(_front - 1); + // Add _num to queue front + _nums[_front] = _num; + _queSize++; + } + + /* Rear of the queue enqueue */ + void pushLast(int _num) { + if (_queSize == capacity()) { + throw Exception("Double-ended queue is full"); + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + int rear = index(_front + _queSize); + // Add _num to queue rear + _nums[rear] = _num; + _queSize++; + } + + /* Rear of the queue dequeue */ + int popFirst() { + int _num = peekFirst(); + // Move front pointer right by one + _front = index(_front + 1); + _queSize--; + return _num; + } + + /* Access rear of the queue element */ + int popLast() { + int _num = peekLast(); + _queSize--; + return _num; + } + + /* Return list for printing */ + int peekFirst() { + if (isEmpty()) { + throw Exception("Deque is empty"); + } + return _nums[_front]; + } + + /* Driver Code */ + int peekLast() { + if (isEmpty()) { + throw Exception("Deque is empty"); + } + // Initialize double-ended queue + int last = index(_front + _queSize - 1); + return _nums[last]; + } + + /* Return array for printing */ + List toArray() { + // Elements enqueue + 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" - [class]{ArrayDeque}-[func]{} + /* Double-ended queue based on circular array implementation */ + struct ArrayDeque { + nums: Vec, // Array for storing double-ended queue elements + front: usize, // Front pointer, points to the front of the queue element + que_size: usize, // Double-ended queue length + } + + impl ArrayDeque { + /* Constructor */ + pub fn new(capacity: usize) -> Self { + Self { + nums: vec![T::default(); capacity], + front: 0, + que_size: 0, + } + } + + /* Get the capacity of the double-ended queue */ + pub fn capacity(&self) -> usize { + self.nums.len() + } + + /* Get the length of the double-ended queue */ + pub fn size(&self) -> usize { + self.que_size + } + + /* Check if the double-ended queue is empty */ + pub fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* Calculate circular array index */ + fn index(&self, i: i32) -> usize { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + ((i + self.capacity() as i32) % self.capacity() as i32) as usize + } + + /* Front of the queue enqueue */ + pub fn push_first(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("Double-ended queue is full"); + return; + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + self.front = self.index(self.front as i32 - 1); + // Add num to front of queue + self.nums[self.front] = num; + self.que_size += 1; + } + + /* Rear of the queue enqueue */ + pub fn push_last(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("Double-ended queue is full"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + let rear = self.index(self.front as i32 + self.que_size as i32); + // Front pointer moves one position backward + self.nums[rear] = num; + self.que_size += 1; + } + + /* Rear of the queue dequeue */ + fn pop_first(&mut self) -> T { + let num = self.peek_first(); + // Move front pointer backward by one position + self.front = self.index(self.front as i32 + 1); + self.que_size -= 1; + num + } + + /* Access rear of the queue element */ + fn pop_last(&mut self) -> T { + let num = self.peek_last(); + self.que_size -= 1; + num + } + + /* Return list for printing */ + fn peek_first(&self) -> T { + if self.is_empty() { + panic!("Deque is empty") + }; + self.nums[self.front] + } + + /* Driver Code */ + fn peek_last(&self) -> T { + if self.is_empty() { + panic!("Deque is empty") + }; + // Initialize double-ended queue + let last = self.index(self.front as i32 + self.que_size as i32 - 1); + self.nums[last] + } + + /* Return array for printing */ + fn to_array(&self) -> Vec { + // Elements enqueue + let mut res = vec![T::default(); 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" - [class]{ArrayDeque}-[func]{} + /* Double-ended queue based on circular array implementation */ + typedef struct { + int *nums; // Array for storing queue elements + int front; // Front pointer, points to the front of the queue element + int queSize; // Rear pointer, points to rear + 1 + int queCapacity; // Queue capacity + } ArrayDeque; + + /* Constructor */ + ArrayDeque *newArrayDeque(int capacity) { + ArrayDeque *deque = (ArrayDeque *)malloc(sizeof(ArrayDeque)); + // Initialize array + deque->queCapacity = capacity; + deque->nums = (int *)malloc(sizeof(int) * deque->queCapacity); + deque->front = deque->queSize = 0; + return deque; + } + + /* Destructor */ + void delArrayDeque(ArrayDeque *deque) { + free(deque->nums); + free(deque); + } + + /* Get the capacity of the double-ended queue */ + int capacity(ArrayDeque *deque) { + return deque->queCapacity; + } + + /* Get the length of the double-ended queue */ + int size(ArrayDeque *deque) { + return deque->queSize; + } + + /* Check if the double-ended queue is empty */ + bool empty(ArrayDeque *deque) { + return deque->queSize == 0; + } + + /* Calculate circular array index */ + int dequeIndex(ArrayDeque *deque, int i) { + // Use modulo operation to wrap the array head and tail together + // When i exceeds array end, wrap to head + // When i passes the head of the array, return to the tail + return ((i + capacity(deque)) % capacity(deque)); + } + + /* Front of the queue enqueue */ + void pushFirst(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("Deque is full\r\n"); + return; + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Use modulo to wrap front from array head to rear + deque->front = dequeIndex(deque, deque->front - 1); + // Add num to queue front + deque->nums[deque->front] = num; + deque->queSize++; + } + + /* Rear of the queue enqueue */ + void pushLast(ArrayDeque *deque, int num) { + if (deque->queSize == capacity(deque)) { + printf("Deque is full\r\n"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + int rear = dequeIndex(deque, deque->front + deque->queSize); + // Front pointer moves one position backward + deque->nums[rear] = num; + deque->queSize++; + } + + /* Return list for printing */ + int peekFirst(ArrayDeque *deque) { + // Access error: Deque is empty + assert(empty(deque) == 0); + return deque->nums[deque->front]; + } + + /* Driver Code */ + int peekLast(ArrayDeque *deque) { + // Access error: Deque is empty + assert(empty(deque) == 0); + int last = dequeIndex(deque, deque->front + deque->queSize - 1); + return deque->nums[last]; + } + + /* Rear of the queue dequeue */ + int popFirst(ArrayDeque *deque) { + int num = peekFirst(deque); + // Move front pointer backward by one position + deque->front = dequeIndex(deque, deque->front + 1); + deque->queSize--; + return num; + } + + /* Access rear of the queue element */ + int popLast(ArrayDeque *deque) { + int num = peekLast(deque); + deque->queSize--; + return num; + } + + /* Return array for printing */ + int *toArray(ArrayDeque *deque, int *queSize) { + *queSize = deque->queSize; + int *res = (int *)calloc(deque->queSize, sizeof(int)); + int j = deque->front; + for (int i = 0; i < deque->queSize; i++) { + res[i] = deque->nums[j % deque->queCapacity]; + j++; + } + return res; + } ``` === "Kotlin" ```kotlin title="array_deque.kt" - [class]{ArrayDeque}-[func]{} + /* Constructor */ + class ArrayDeque(capacity: Int) { + private var nums: IntArray = IntArray(capacity) // Array for storing double-ended queue elements + private var front: Int = 0 // Front pointer, points to the front of the queue element + private var queSize: Int = 0 // Double-ended queue length + + /* Get the capacity of the double-ended queue */ + fun capacity(): Int { + return nums.size + } + + /* Get the length of the double-ended queue */ + fun size(): Int { + return queSize + } + + /* Check if the double-ended queue is empty */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* Calculate circular array index */ + private fun index(i: Int): Int { + // Use modulo operation to wrap the array head and tail together + // When i passes the tail of the array, return to the head + // When i passes the head of the array, return to the tail + return (i + capacity()) % capacity() + } + + /* Front of the queue enqueue */ + fun pushFirst(num: Int) { + if (queSize == capacity()) { + println("Double-ended queue is full") + return + } + // Use modulo operation to wrap front around to the tail after passing the head of the array + // Add num to the front of the queue + front = index(front - 1) + // Add num to front of queue + nums[front] = num + queSize++ + } + + /* Rear of the queue enqueue */ + fun pushLast(num: Int) { + if (queSize == capacity()) { + println("Double-ended queue is full") + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + val rear = index(front + queSize) + // Front pointer moves one position backward + nums[rear] = num + queSize++ + } + + /* Rear of the queue dequeue */ + fun popFirst(): Int { + val num = peekFirst() + // Move front pointer backward by one position + front = index(front + 1) + queSize-- + return num + } + + /* Access rear of the queue element */ + fun popLast(): Int { + val num = peekLast() + queSize-- + return num + } + + /* Return list for printing */ + fun peekFirst(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* Driver Code */ + fun peekLast(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + // Initialize double-ended queue + val last = index(front + queSize - 1) + return nums[last] + } + + /* Return array for printing */ + fun toArray(): IntArray { + // Elements enqueue + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[index(j)] + i++ + j++ + } + return res + } + } ``` === "Ruby" ```ruby title="array_deque.rb" - [class]{ArrayDeque}-[func]{} + ### Deque based on circular array ### + class ArrayDeque + ### Get deque length ### + attr_reader :size + + ### Constructor ### + def initialize(capacity) + @nums = Array.new(capacity, 0) + @front = 0 + @size = 0 + end + + ### Get deque capacity ### + def capacity + @nums.length + end + + ### Check if deque is empty ### + def is_empty? + size.zero? + end + + ### Enqueue at front ### + def push_first(num) + if size == capacity + puts 'Double-ended queue is full' + return + end + + # Use modulo operation to wrap front around to the tail after passing the head of the array + # Add num to the front of the queue + @front = index(@front - 1) + # Add num to front of queue + @nums[@front] = num + @size += 1 + end + + ### Enqueue at rear ### + def push_last(num) + if size == capacity + puts 'Double-ended queue is full' + return + end + + # Use modulo operation to wrap rear around to the head after passing the tail of the array + rear = index(@front + size) + # Front pointer moves one position backward + @nums[rear] = num + @size += 1 + end + + ### Dequeue from front ### + def pop_first + num = peek_first + # Move front pointer backward by one position + @front = index(@front + 1) + @size -= 1 + num + end + + ### Dequeue from rear ### + def pop_last + num = peek_last + @size -= 1 + num + end + + ### Access front element ### + def peek_first + raise IndexError, 'Deque is empty' if is_empty? + + @nums[@front] + end + + ### Access rear element ### + def peek_last + raise IndexError, 'Deque is empty' if is_empty? + + # Initialize double-ended queue + last = index(@front + size - 1) + @nums[last] + end + + ### Return array for printing ### + def to_array + # Elements enqueue + res = [] + for i in 0...size + res << @nums[index(@front + i)] + end + res + end + + private + + ### Calculate circular array index ### + def index(i) + # Use modulo operation to wrap the array head and tail together + # When i passes the tail of the array, return to the head + # When i passes the head of the array, return to the tail + (i + capacity) % capacity + end + end ``` -=== "Zig" +## 5.3.3   Deque Applications - ```zig title="array_deque.zig" - [class]{ArrayDeque}-[func]{} - ``` +A deque combines the logic of both stacks and queues. **Therefore, it can implement all application scenarios of both, while providing greater flexibility**. -## 5.3.3   Applications of double-ended queue - -The double-ended queue combines the logic of both stacks and queues, **thus, it can implement all their respective use cases while offering greater flexibility**. - -We know that software's "undo" feature is typically implemented using a stack: the system `pushes` each change operation onto the stack and then `pops` to implement undoing. However, considering the limitations of system resources, software often restricts the number of undo steps (for example, only allowing the last 50 steps). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (the front of the queue). **But a regular stack cannot perform this function, where a double-ended queue becomes necessary**. Note that the core logic of "undo" still follows the Last-In-First-Out principle of a stack, but a double-ended queue can more flexibly implement some additional logic. +We know that the "undo" function in software is typically implemented using a stack: the system pushes each change operation onto the stack and then implements undo through pop. However, considering system resource limitations, software usually limits the number of undo steps (for example, only allowing 50 steps to be saved). When the stack length exceeds 50, the software needs to perform a deletion operation at the bottom of the stack (front of the queue). **But a stack cannot implement this functionality, so a deque is needed to replace the stack**. Note that the core logic of "undo" still follows the LIFO principle of a stack; it's just that the deque can more flexibly implement some additional logic. diff --git a/en/docs/chapter_stack_and_queue/index.md b/en/docs/chapter_stack_and_queue/index.md index d2131bfe6..23a17ab83 100644 --- a/en/docs/chapter_stack_and_queue/index.md +++ b/en/docs/chapter_stack_and_queue/index.md @@ -3,19 +3,19 @@ comments: true icon: material/stack-overflow --- -# Chapter 5.   Stack and queue +# Chapter 5.   Stack and Queue -![Stack and queue](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } +![Stack and Queue](../assets/covers/chapter_stack_and_queue.jpg){ class="cover-image" } !!! abstract - A stack is like cats placed on top of each other, while a queue is like cats lined up one by one. - - They represent the logical relationships of Last-In-First-Out (LIFO) and First-In-First-Out (FIFO), respectively. + Stacks are like stacking cats, while queues are like cats lining up. + + They represent LIFO (Last In First Out) and FIFO (First In First Out) logic, respectively. ## Chapter contents - [5.1   Stack](stack.md) - [5.2   Queue](queue.md) -- [5.3   Double-ended queue](deque.md) +- [5.3   Double-Ended Queue](deque.md) - [5.4   Summary](summary.md) diff --git a/en/docs/chapter_stack_and_queue/queue.md b/en/docs/chapter_stack_and_queue/queue.md index 9d2ae7607..44428bf59 100755 --- a/en/docs/chapter_stack_and_queue/queue.md +++ b/en/docs/chapter_stack_and_queue/queue.md @@ -4,27 +4,27 @@ comments: true # 5.2   Queue -A queue is a linear data structure that follows the First-In-First-Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers join the queue at the rear, and the person at the front leaves the queue first. +A queue is a linear data structure that follows the First In First Out (FIFO) rule. As the name suggests, a queue simulates the phenomenon of lining up, where newcomers continuously join the end of the queue, while people at the front of the queue leave one by one. -As shown in Figure 5-4, we call the front of the queue the "head" and the back the "tail." The operation of adding elements to the rear of the queue is termed "enqueue," and the operation of removing elements from the front is termed "dequeue." +As shown in Figure 5-4, we call the front of the queue the "front" and the end the "rear." The operation of adding an element to the rear is called "enqueue," and the operation of removing the front element is called "dequeue." -![Queue's first-in-first-out rule](queue.assets/queue_operations.png){ class="animation-figure" } +![FIFO rule of queue](queue.assets/queue_operations.png){ class="animation-figure" } -

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

+

Figure 5-4   FIFO rule of queue

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

Table 5-2   Efficiency of queue operations

+

Table 5-2   Efficiency of Queue Operations

-| Method Name | Description | Time Complexity | -| ----------- | -------------------------------------- | --------------- | -| `push()` | Enqueue an element, add it to the tail | $O(1)$ | -| `pop()` | Dequeue the head element | $O(1)$ | -| `peek()` | Access the head element | $O(1)$ | +| Method | Description | Time Complexity | +| -------- | ------------------------------------------ | --------------- | +| `push()` | Enqueue element, add element to rear | $O(1)$ | +| `pop()` | Dequeue front element | $O(1)$ | +| `peek()` | Access front element | $O(1)$ |
@@ -35,9 +35,9 @@ We can directly use the ready-made queue classes in programming languages: ```python title="queue.py" from collections import deque - # Initialize the queue + # Initialize queue # In Python, we generally use the deque class as a queue - # Although queue.Queue() is a pure queue class, it's not very user-friendly, so it's not recommended + # Although queue.Queue() is a pure queue class, it is not very user-friendly, so it is not recommended que: deque[int] = deque() # Enqueue elements @@ -47,23 +47,23 @@ We can directly use the ready-made queue classes in programming languages: que.append(5) que.append(4) - # Access the first element + # Access front element front: int = que[0] - # Dequeue an element + # Dequeue element pop: int = que.popleft() - # Get the length of the queue + # Get queue length size: int = len(que) - # Check if the queue is empty + # Check if queue is empty is_empty: bool = len(que) == 0 ``` === "C++" ```cpp title="queue.cpp" - /* Initialize the queue */ + /* Initialize queue */ queue queue; /* Enqueue elements */ @@ -73,23 +73,23 @@ We can directly use the ready-made queue classes in programming languages: queue.push(5); queue.push(4); - /* Access the first element*/ + /* Access front element */ int front = queue.front(); - /* Dequeue an element */ + /* Dequeue element */ queue.pop(); - /* Get the length of the queue */ + /* Get queue length */ int size = queue.size(); - /* Check if the queue is empty */ + /* Check if queue is empty */ bool empty = queue.empty(); ``` === "Java" ```java title="queue.java" - /* Initialize the queue */ + /* Initialize queue */ Queue queue = new LinkedList<>(); /* Enqueue elements */ @@ -99,23 +99,23 @@ We can directly use the ready-made queue classes in programming languages: queue.offer(5); queue.offer(4); - /* Access the first element */ + /* Access front element */ int peek = queue.peek(); - /* Dequeue an element */ + /* Dequeue element */ int pop = queue.poll(); - /* Get the length of the queue */ + /* Get queue length */ int size = queue.size(); - /* Check if the queue is empty */ + /* Check if queue is empty */ boolean isEmpty = queue.isEmpty(); ``` === "C#" ```csharp title="queue.cs" - /* Initialize the queue */ + /* Initialize queue */ Queue queue = new(); /* Enqueue elements */ @@ -125,23 +125,23 @@ We can directly use the ready-made queue classes in programming languages: queue.Enqueue(5); queue.Enqueue(4); - /* Access the first element */ + /* Access front element */ int peek = queue.Peek(); - /* Dequeue an element */ + /* Dequeue element */ int pop = queue.Dequeue(); - /* Get the length of the queue */ + /* Get queue length */ int size = queue.Count; - /* Check if the queue is empty */ + /* Check if queue is empty */ bool isEmpty = queue.Count == 0; ``` === "Go" ```go title="queue_test.go" - /* Initialize the queue */ + /* Initialize queue */ // In Go, use list as a queue queue := list.New() @@ -152,25 +152,25 @@ We can directly use the ready-made queue classes in programming languages: queue.PushBack(5) queue.PushBack(4) - /* Access the first element */ + /* Access front element */ peek := queue.Front() - /* Dequeue an element */ + /* Dequeue element */ pop := queue.Front() queue.Remove(pop) - /* Get the length of the queue */ + /* Get queue length */ size := queue.Len() - /* Check if the queue is empty */ + /* Check if queue is empty */ isEmpty := queue.Len() == 0 ``` === "Swift" ```swift title="queue.swift" - /* Initialize the queue */ - // Swift does not have a built-in queue class, so Array can be used as a queue + /* Initialize queue */ + // Swift does not have a built-in queue class, can use Array as a queue var queue: [Int] = [] /* Enqueue elements */ @@ -180,25 +180,25 @@ We can directly use the ready-made queue classes in programming languages: queue.append(5) queue.append(4) - /* Access the first element */ + /* Access front element */ let peek = queue.first! - /* Dequeue an element */ - // Since it's an array, removeFirst has a complexity of O(n) + /* Dequeue element */ + // Since it's an array, removeFirst has O(n) complexity let pool = queue.removeFirst() - /* Get the length of the queue */ + /* Get queue length */ let size = queue.count - /* Check if the queue is empty */ + /* Check if queue is empty */ let isEmpty = queue.isEmpty ``` === "JS" ```javascript title="queue.js" - /* Initialize the queue */ - // JavaScript does not have a built-in queue, so Array can be used as a queue + /* Initialize queue */ + // JavaScript does not have a built-in queue, can use Array as a queue const queue = []; /* Enqueue elements */ @@ -208,25 +208,25 @@ We can directly use the ready-made queue classes in programming languages: queue.push(5); queue.push(4); - /* Access the first element */ + /* Access front element */ const peek = queue[0]; - /* Dequeue an element */ - // Since the underlying structure is an array, shift() method has a time complexity of O(n) + /* Dequeue element */ + // The underlying structure is an array, so shift() has O(n) time complexity const pop = queue.shift(); - /* Get the length of the queue */ + /* Get queue length */ const size = queue.length; - /* Check if the queue is empty */ + /* Check if queue is empty */ const empty = queue.length === 0; ``` === "TS" ```typescript title="queue.ts" - /* Initialize the queue */ - // TypeScript does not have a built-in queue, so Array can be used as a queue + /* Initialize queue */ + // TypeScript does not have a built-in queue, can use Array as a queue const queue: number[] = []; /* Enqueue elements */ @@ -236,25 +236,25 @@ We can directly use the ready-made queue classes in programming languages: queue.push(5); queue.push(4); - /* Access the first element */ + /* Access front element */ const peek = queue[0]; - /* Dequeue an element */ - // Since the underlying structure is an array, shift() method has a time complexity of O(n) + /* Dequeue element */ + // The underlying structure is an array, so shift() has O(n) time complexity const pop = queue.shift(); - /* Get the length of the queue */ + /* Get queue length */ const size = queue.length; - /* Check if the queue is empty */ + /* Check if queue is empty */ const empty = queue.length === 0; ``` === "Dart" ```dart title="queue.dart" - /* Initialize the queue */ - // In Dart, the Queue class is a double-ended queue but can be used as a queue + /* Initialize queue */ + // In Dart, the Queue class is a deque and can also be used as a queue Queue queue = Queue(); /* Enqueue elements */ @@ -264,24 +264,24 @@ We can directly use the ready-made queue classes in programming languages: queue.add(5); queue.add(4); - /* Access the first element */ + /* Access front element */ int peek = queue.first; - /* Dequeue an element */ + /* Dequeue element */ int pop = queue.removeFirst(); - /* Get the length of the queue */ + /* Get queue length */ int size = queue.length; - /* Check if the queue is empty */ + /* Check if queue is empty */ bool isEmpty = queue.isEmpty; ``` === "Rust" ```rust title="queue.rs" - /* Initialize the double-ended queue */ - // In Rust, use a double-ended queue as a regular queue + /* Initialize deque */ + // In Rust, use deque as a regular queue let mut deque: VecDeque = VecDeque::new(); /* Enqueue elements */ @@ -291,18 +291,18 @@ We can directly use the ready-made queue classes in programming languages: deque.push_back(5); deque.push_back(4); - /* Access the first element */ + /* Access front element */ if let Some(front) = deque.front() { } - /* Dequeue an element */ + /* Dequeue element */ if let Some(pop) = deque.pop_front() { } - /* Get the length of the queue */ + /* Get queue length */ let size = deque.len(); - /* Check if the queue is empty */ + /* Check if queue is empty */ let is_empty = deque.is_empty(); ``` @@ -315,13 +315,55 @@ We can directly use the ready-made queue classes in programming languages: === "Kotlin" ```kotlin title="queue.kt" + /* Initialize queue */ + val queue = LinkedList() + /* Enqueue elements */ + queue.offer(1) + queue.offer(3) + queue.offer(2) + queue.offer(5) + queue.offer(4) + + /* Access front element */ + val peek = queue.peek() + + /* Dequeue element */ + val pop = queue.poll() + + /* Get queue length */ + val size = queue.size + + /* Check if queue is empty */ + val isEmpty = queue.isEmpty() ``` -=== "Zig" +=== "Ruby" - ```zig title="queue.zig" + ```ruby title="queue.rb" + # Initialize queue + # Ruby's built-in queue (Thread::Queue) does not have peek and traversal methods, can use Array as a queue + queue = [] + # Enqueue elements + queue.push(1) + queue.push(3) + queue.push(2) + queue.push(5) + queue.push(4) + + # Access front element + peek = queue.first + + # Dequeue element + # Please note that since it's an array, Array#shift has O(n) time complexity + pop = queue.shift + + # Get queue length + size = queue.length + + # Check if queue is empty + is_empty = queue.empty? ``` ??? pythontutor "Code Visualization" @@ -329,16 +371,16 @@ We can directly use the ready-made queue classes in programming languages:
-## 5.2.2   Implementing a queue +## 5.2.2   Queue Implementation -To implement a queue, we need a data structure that allows adding elements at one end and removing them at the other. Both linked lists and arrays meet this requirement. +To implement a queue, we need a data structure that allows adding elements at one end and removing elements at the other end. Both linked lists and arrays meet this requirement. -### 1.   Implementation based on a linked list +### 1.   Linked List Implementation -As shown in Figure 5-5, we can consider the "head node" and "tail node" of a linked list as the "front" and "rear" of the queue, respectively. It is stipulated that nodes can only be added at the rear and removed at the front. +As shown in Figure 5-5, we can treat the "head node" and "tail node" of a linked list as the "front" and "rear" of the queue, respectively, with the rule that nodes can only be added at the rear and removed from the front. === "LinkedListQueue" - ![Implementing Queue with Linked List for Enqueue and Dequeue Operations](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } + ![Enqueue and dequeue operations in linked list implementation of queue](queue.assets/linkedlist_queue_step1.png){ class="animation-figure" } === "push()" ![linkedlist_queue_push](queue.assets/linkedlist_queue_step2_push.png){ class="animation-figure" } @@ -346,7 +388,7 @@ As shown in Figure 5-5, we can consider the "head node" and "tail node" of a lin === "pop()" ![linkedlist_queue_pop](queue.assets/linkedlist_queue_step3_pop.png){ class="animation-figure" } -

Figure 5-5   Implementing Queue with Linked List for Enqueue and Dequeue Operations

+

Figure 5-5   Enqueue and dequeue operations in linked list implementation of queue

Below is the code for implementing a queue using a linked list: @@ -354,7 +396,7 @@ Below is the code for implementing a queue using a linked list: ```python title="linkedlist_queue.py" class LinkedListQueue: - """Queue class based on linked list""" + """Queue based on linked list implementation""" def __init__(self): """Constructor""" @@ -367,18 +409,18 @@ Below is the code for implementing a queue using a linked list: return self._size def is_empty(self) -> bool: - """Determine if the queue is empty""" + """Check if the queue is empty""" return self._size == 0 def push(self, num: int): """Enqueue""" - # Add num behind the tail node + # Add num after the tail node node = ListNode(num) - # If the queue is empty, make the head and tail nodes both point to that node + # If the queue is empty, make both front and rear point to the node if self._front is None: self._front = node self._rear = node - # If the queue is not empty, add that node behind the tail node + # If the queue is not empty, add the node after the tail node else: self._rear.next = node self._rear = node @@ -387,19 +429,19 @@ Below is the code for implementing a queue using a linked list: def pop(self) -> int: """Dequeue""" num = self.peek() - # Remove head node + # Delete head node self._front = self._front.next self._size -= 1 return num def peek(self) -> int: - """Access front element""" + """Access front of the queue element""" if self.is_empty(): raise IndexError("Queue is empty") return self._front.val def to_list(self) -> list[int]: - """Convert to a list for printing""" + """Convert to list for printing""" queue = [] temp = self._front while temp: @@ -411,10 +453,10 @@ Below is the code for implementing a queue using a linked list: === "C++" ```cpp title="linkedlist_queue.cpp" - /* Queue class based on linked list */ + /* Queue based on linked list implementation */ class LinkedListQueue { private: - ListNode *front, *rear; // Front node front, back node rear + ListNode *front, *rear; // Head node front, tail node rear int queSize; public: @@ -425,7 +467,7 @@ Below is the code for implementing a queue using a linked list: } ~LinkedListQueue() { - // Traverse the linked list, remove nodes, free memory + // Traverse linked list to delete nodes and free memory freeMemoryLinkedList(front); } @@ -434,21 +476,21 @@ Below is the code for implementing a queue using a linked list: return queSize; } - /* Determine if the queue is empty */ + /* Check if the queue is empty */ bool isEmpty() { return queSize == 0; } /* Enqueue */ void push(int num) { - // Add num behind the tail node + // Add num after the tail node ListNode *node = new ListNode(num); - // If the queue is empty, make the head and tail nodes both point to that node + // If the queue is empty, make both front and rear point to the node if (front == nullptr) { front = node; rear = node; } - // If the queue is not empty, add that node behind the tail node + // If the queue is not empty, add the node after the tail node else { rear->next = node; rear = node; @@ -459,7 +501,7 @@ Below is the code for implementing a queue using a linked list: /* Dequeue */ int pop() { int num = peek(); - // Remove head node + // Delete head node ListNode *tmp = front; front = front->next; // Free memory @@ -468,14 +510,14 @@ Below is the code for implementing a queue using a linked list: return num; } - /* Access front element */ + /* Return list for printing */ int peek() { if (size() == 0) throw out_of_range("Queue is empty"); return front->val; } - /* Convert the linked list to Vector and return */ + /* Convert linked list to Vector and return */ vector toVector() { ListNode *node = front; vector res(size()); @@ -491,9 +533,9 @@ Below is the code for implementing a queue using a linked list: === "Java" ```java title="linkedlist_queue.java" - /* Queue class based on linked list */ + /* Queue based on linked list implementation */ class LinkedListQueue { - private ListNode front, rear; // Front node front, back node rear + private ListNode front, rear; // Head node front, tail node rear private int queSize = 0; public LinkedListQueue() { @@ -506,20 +548,20 @@ Below is the code for implementing a queue using a linked list: return queSize; } - /* Determine if the queue is empty */ + /* Check if the queue is empty */ public boolean isEmpty() { return size() == 0; } /* Enqueue */ public void push(int num) { - // Add num behind the tail node + // Add num after the tail node ListNode node = new ListNode(num); - // If the queue is empty, make the head and tail nodes both point to that node + // If the queue is empty, make both front and rear point to the node if (front == null) { front = node; rear = node; - // If the queue is not empty, add that node behind the tail node + // If the queue is not empty, add the node after the tail node } else { rear.next = node; rear = node; @@ -530,20 +572,20 @@ Below is the code for implementing a queue using a linked list: /* Dequeue */ public int pop() { int num = peek(); - // Remove head node + // Delete head node front = front.next; queSize--; return num; } - /* Access front element */ + /* Return list for printing */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); return front.val; } - /* Convert the linked list to Array and return */ + /* Convert linked list to Array and return */ public int[] toArray() { ListNode node = front; int[] res = new int[size()]; @@ -559,84 +601,731 @@ Below is the code for implementing a queue using a linked list: === "C#" ```csharp title="linkedlist_queue.cs" - [class]{LinkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue { + ListNode? front, rear; // Head node front, tail node rear + int queSize = 0; + + public LinkedListQueue() { + front = null; + rear = null; + } + + /* Get the length of the queue */ + public int Size() { + return queSize; + } + + /* Check if the queue is empty */ + public bool IsEmpty() { + return Size() == 0; + } + + /* Enqueue */ + public void Push(int num) { + // Add num after the tail node + ListNode node = new(num); + // If the queue is empty, make both front and rear point to the node + if (front == null) { + front = node; + rear = node; + // If the queue is not empty, add the node after the tail node + } else if (rear != null) { + rear.next = node; + rear = node; + } + queSize++; + } + + /* Dequeue */ + public int Pop() { + int num = Peek(); + // Delete head node + front = front?.next; + queSize--; + return num; + } + + /* Return list for printing */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return front!.val; + } + + /* Convert linked list to Array and return */ + public int[] ToArray() { + if (front == null) + return []; + + ListNode? node = front; + int[] res = new int[Size()]; + for (int i = 0; i < res.Length; i++) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } ``` === "Go" ```go title="linkedlist_queue.go" - [class]{linkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + type linkedListQueue struct { + // Use built-in package list to implement queue + data *list.List + } + + /* Access front of the queue element */ + func newLinkedListQueue() *linkedListQueue { + return &linkedListQueue{ + data: list.New(), + } + } + + /* Enqueue */ + func (s *linkedListQueue) push(value any) { + s.data.PushBack(value) + } + + /* Dequeue */ + func (s *linkedListQueue) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + s.data.Remove(e) + return e.Value + } + + /* Return list for printing */ + func (s *linkedListQueue) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Front() + return e.Value + } + + /* Get the length of the queue */ + func (s *linkedListQueue) size() int { + return s.data.Len() + } + + /* Check if the queue is empty */ + func (s *linkedListQueue) isEmpty() bool { + return s.data.Len() == 0 + } + + /* Get List for printing */ + func (s *linkedListQueue) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_queue.swift" - [class]{LinkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue { + private var front: ListNode? // Head node + private var rear: ListNode? // Tail node + private var _size: Int + + init() { + _size = 0 + } + + /* Get the length of the queue */ + func size() -> Int { + _size + } + + /* Check if the queue is empty */ + func isEmpty() -> Bool { + size() == 0 + } + + /* Enqueue */ + func push(num: Int) { + // Add num after the tail node + let node = ListNode(x: num) + // If the queue is empty, make both front and rear point to the node + if front == nil { + front = node + rear = node + } + // If the queue is not empty, add the node after the tail node + else { + rear?.next = node + rear = node + } + _size += 1 + } + + /* Dequeue */ + @discardableResult + func pop() -> Int { + let num = peek() + // Delete head node + front = front?.next + _size -= 1 + return num + } + + /* Return list for printing */ + func peek() -> Int { + if isEmpty() { + fatalError("Queue is empty") + } + return front!.val + } + + /* Convert linked list to Array and return */ + 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}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue { + #front; // Front node #front + #rear; // Rear node #rear + #queSize = 0; + + constructor() { + this.#front = null; + this.#rear = null; + } + + /* Get the length of the queue */ + get size() { + return this.#queSize; + } + + /* Check if the queue is empty */ + isEmpty() { + return this.size === 0; + } + + /* Enqueue */ + push(num) { + // Add num after the tail node + const node = new ListNode(num); + // If the queue is empty, make both front and rear point to the node + if (!this.#front) { + this.#front = node; + this.#rear = node; + // If the queue is not empty, add the node after the tail node + } else { + this.#rear.next = node; + this.#rear = node; + } + this.#queSize++; + } + + /* Dequeue */ + pop() { + const num = this.peek(); + // Delete head node + this.#front = this.#front.next; + this.#queSize--; + return num; + } + + /* Return list for printing */ + peek() { + if (this.size === 0) throw new Error('Queue is empty'); + return this.#front.val; + } + + /* Convert linked list to Array and return */ + 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}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue { + private front: ListNode | null; // Head node front + private rear: ListNode | null; // Tail node rear + private queSize: number = 0; + + constructor() { + this.front = null; + this.rear = null; + } + + /* Get the length of the queue */ + get size(): number { + return this.queSize; + } + + /* Check if the queue is empty */ + isEmpty(): boolean { + return this.size === 0; + } + + /* Enqueue */ + push(num: number): void { + // Add num after the tail node + const node = new ListNode(num); + // If the queue is empty, make both front and rear point to the node + if (!this.front) { + this.front = node; + this.rear = node; + // If the queue is not empty, add the node after the tail node + } else { + this.rear!.next = node; + this.rear = node; + } + this.queSize++; + } + + /* Dequeue */ + pop(): number { + const num = this.peek(); + if (!this.front) throw new Error('Queue is empty'); + // Delete head node + this.front = this.front.next; + this.queSize--; + return num; + } + + /* Return list for printing */ + peek(): number { + if (this.size === 0) throw new Error('Queue is empty'); + return this.front!.val; + } + + /* Convert linked list to Array and return */ + 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}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue { + ListNode? _front; // Head node _front + ListNode? _rear; // Tail node _rear + int _queSize = 0; // Queue length + + LinkedListQueue() { + _front = null; + _rear = null; + } + + /* Get the length of the queue */ + int size() { + return _queSize; + } + + /* Check if the queue is empty */ + bool isEmpty() { + return _queSize == 0; + } + + /* Enqueue */ + void push(int _num) { + // Add _num after tail node + final node = ListNode(_num); + // If the queue is empty, make both front and rear point to the node + if (_front == null) { + _front = node; + _rear = node; + } else { + // If the queue is not empty, add the node after the tail node + _rear!.next = node; + _rear = node; + } + _queSize++; + } + + /* Dequeue */ + int pop() { + final int _num = peek(); + // Delete head node + _front = _front!.next; + _queSize--; + return _num; + } + + /* Return list for printing */ + int peek() { + if (_queSize == 0) { + throw Exception('Queue is empty'); + } + return _front!.val; + } + + /* Convert linked list to Array and return */ + 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" - [class]{LinkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + #[allow(dead_code)] + pub struct LinkedListQueue { + front: Option>>>, // Head node front + rear: Option>>>, // Tail node rear + que_size: usize, // Queue length + } + + impl LinkedListQueue { + pub fn new() -> Self { + Self { + front: None, + rear: None, + que_size: 0, + } + } + + /* Get the length of the queue */ + pub fn size(&self) -> usize { + return self.que_size; + } + + /* Check if the queue is empty */ + pub fn is_empty(&self) -> bool { + return self.que_size == 0; + } + + /* Enqueue */ + pub fn push(&mut self, num: T) { + // Add num after the tail node + let new_rear = ListNode::new(num); + match self.rear.take() { + // If the queue is not empty, add the node after the tail node + Some(old_rear) => { + old_rear.borrow_mut().next = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + // If the queue is empty, make both front and rear point to the node + None => { + self.front = Some(new_rear.clone()); + self.rear = Some(new_rear); + } + } + self.que_size += 1; + } + + /* Dequeue */ + 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; + old_front.borrow().val + }) + } + + /* Return list for printing */ + pub fn peek(&self) -> Option<&Rc>>> { + self.front.as_ref() + } + + /* Convert linked list to Array and return */ + pub fn to_array(&self, head: Option<&Rc>>>) -> Vec { + let mut res: Vec = Vec::new(); + + fn recur(cur: Option<&Rc>>>, res: &mut Vec) { + if let Some(cur) = cur { + res.push(cur.borrow().val); + recur(cur.borrow().next.as_ref(), res); + } + } + + recur(head, &mut res); + + res + } + } ``` === "C" ```c title="linkedlist_queue.c" - [class]{LinkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + typedef struct { + ListNode *front, *rear; + int queSize; + } LinkedListQueue; + + /* Constructor */ + LinkedListQueue *newLinkedListQueue() { + LinkedListQueue *queue = (LinkedListQueue *)malloc(sizeof(LinkedListQueue)); + queue->front = NULL; + queue->rear = NULL; + queue->queSize = 0; + return queue; + } + + /* Destructor */ + void delLinkedListQueue(LinkedListQueue *queue) { + // Free all nodes + while (queue->front != NULL) { + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + } + // Free queue structure + free(queue); + } + + /* Get the length of the queue */ + int size(LinkedListQueue *queue) { + return queue->queSize; + } + + /* Check if the queue is empty */ + bool empty(LinkedListQueue *queue) { + return (size(queue) == 0); + } + + /* Enqueue */ + void push(LinkedListQueue *queue, int num) { + // Add node at tail + ListNode *node = newListNode(num); + // If the queue is empty, make both front and rear point to the node + if (queue->front == NULL) { + queue->front = node; + queue->rear = node; + } + // If the queue is not empty, add the node after the tail node + else { + queue->rear->next = node; + queue->rear = node; + } + queue->queSize++; + } + + /* Return list for printing */ + int peek(LinkedListQueue *queue) { + assert(size(queue) && queue->front); + return queue->front->val; + } + + /* Dequeue */ + int pop(LinkedListQueue *queue) { + int num = peek(queue); + ListNode *tmp = queue->front; + queue->front = queue->front->next; + free(tmp); + queue->queSize--; + return num; + } + + /* Print queue */ + void printLinkedListQueue(LinkedListQueue *queue) { + int *arr = malloc(sizeof(int) * queue->queSize); + // Copy data from list to array + int i; + ListNode *node; + for (i = 0, node = queue->front; i < queue->queSize; i++) { + arr[i] = node->val; + node = node->next; + } + printArray(arr, queue->queSize); + free(arr); + } ``` === "Kotlin" ```kotlin title="linkedlist_queue.kt" - [class]{LinkedListQueue}-[func]{} + /* Queue based on linked list implementation */ + class LinkedListQueue( + // Head node front, tail node rear + private var front: ListNode? = null, + private var rear: ListNode? = null, + private var queSize: Int = 0 + ) { + + /* Get the length of the queue */ + fun size(): Int { + return queSize + } + + /* Check if the queue is empty */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* Enqueue */ + fun push(num: Int) { + // Add num after the tail node + val node = ListNode(num) + // If the queue is empty, make both front and rear point to the node + if (front == null) { + front = node + rear = node + // If the queue is not empty, add the node after the tail node + } else { + rear?.next = node + rear = node + } + queSize++ + } + + /* Dequeue */ + fun pop(): Int { + val num = peek() + // Delete head node + front = front?.next + queSize-- + return num + } + + /* Return list for printing */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return front!!._val + } + + /* Convert linked list to Array and return */ + fun toArray(): IntArray { + var node = front + val res = IntArray(size()) + for (i in res.indices) { + res[i] = node!!._val + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_queue.rb" - [class]{LinkedListQueue}-[func]{} + ### Queue based on linked list ### + class LinkedListQueue + ### Get queue length ### + attr_reader :size + + ### Constructor ### + def initialize + @front = nil # Head node front + @rear = nil # Tail node rear + @size = 0 + end + + ### Check if queue is empty ### + def is_empty? + @front.nil? + end + + ### Enqueue ### + def push(num) + # Add num after the tail node + node = ListNode.new(num) + + # If queue is empty, set both front and rear to this node + if @front.nil? + @front = node + @rear = node + # If queue is not empty, add this node after rear + else + @rear.next = node + @rear = node + end + + @size += 1 + end + + ### Dequeue ### + def pop + num = peek + # Delete head node + @front = @front.next + @size -= 1 + num + end + + ### Access front element ### + def peek + raise IndexError, 'Queue is empty' if is_empty? + + @front.val + end + + ### Convert linked list to Array and return ### + def to_array + queue = [] + temp = @front + while temp + queue << temp.val + temp = temp.next + end + queue + end + end ``` -=== "Zig" +### 2.   Array Implementation - ```zig title="linkedlist_queue.zig" - [class]{LinkedListQueue}-[func]{} - ``` +Deleting the first element in an array has a time complexity of $O(n)$, which would make the dequeue operation inefficient. However, we can use the following clever method to avoid this problem. -### 2.   Implementation based on an array +We can use a variable `front` to point to the index of the front element and maintain a variable `size` to record the queue length. We define `rear = front + size`, which calculates the position right after the rear element. -Deleting the first element in an array has a time complexity of $O(n)$, which would make the dequeue operation inefficient. However, this problem can be cleverly avoided as follows. - -We use a variable `front` to indicate the index of the front element and maintain a variable `size` to record the queue's length. Define `rear = front + size`, which points to the position immediately following the tail element. - -With this design, **the effective interval of elements in the array is `[front, rear - 1]`**. The implementation methods for various operations are shown in Figure 5-6. +Based on this design, **the valid interval containing elements in the array is `[front, rear - 1]`**. The implementation methods for various operations are shown in Figure 5-6: - Enqueue operation: Assign the input element to the `rear` index and increase `size` by 1. - Dequeue operation: Simply increase `front` by 1 and decrease `size` by 1. -Both enqueue and dequeue operations only require a single operation, each with a time complexity of $O(1)$. +As you can see, both enqueue and dequeue operations require only one operation, with a time complexity of $O(1)$. === "ArrayQueue" - ![Implementing Queue with Array for Enqueue and Dequeue Operations](queue.assets/array_queue_step1.png){ class="animation-figure" } + ![Enqueue and dequeue operations in array implementation of queue](queue.assets/array_queue_step1.png){ class="animation-figure" } === "push()" ![array_queue_push](queue.assets/array_queue_step2_push.png){ class="animation-figure" } @@ -644,22 +1333,22 @@ Both enqueue and dequeue operations only require a single operation, each with a === "pop()" ![array_queue_pop](queue.assets/array_queue_step3_pop.png){ class="animation-figure" } -

Figure 5-6   Implementing Queue with Array for Enqueue and Dequeue Operations

+

Figure 5-6   Enqueue and dequeue operations in array implementation of queue

-You might notice a problem: as enqueue and dequeue operations are continuously performed, both `front` and `rear` move to the right and **will eventually reach the end of the array and can't move further**. To resolve this, we can treat the array as a "circular array" where connecting the end of the array back to its beginning. +You may notice a problem: as we continuously enqueue and dequeue, both `front` and `rear` move to the right. **When they reach the end of the array, they cannot continue moving**. To solve this problem, we can treat the array as a "circular array" with head and tail connected. -In a circular array, `front` or `rear` needs to loop back to the start of the array upon reaching the end. This cyclical pattern can be achieved with a "modulo operation" as shown in the code below: +For a circular array, we need to let `front` or `rear` wrap around to the beginning of the array when they cross the end. This periodic pattern can be implemented using the "modulo operation," as shown in the code below: === "Python" ```python title="array_queue.py" class ArrayQueue: - """Queue class based on circular array""" + """Queue based on circular array implementation""" def __init__(self, size: int): """Constructor""" self._nums: list[int] = [0] * size # Array for storing queue elements - self._front: int = 0 # Front pointer, pointing to the front element + self._front: int = 0 # Front pointer, points to the front of the queue element self._size: int = 0 # Queue length def capacity(self) -> int: @@ -671,36 +1360,36 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar return self._size def is_empty(self) -> bool: - """Determine if the queue is empty""" + """Check if the queue is empty""" return self._size == 0 def push(self, num: int): """Enqueue""" if self._size == self.capacity(): raise IndexError("Queue is full") - # Calculate rear pointer, pointing to rear index + 1 - # Use modulo operation to wrap the rear pointer from the end of the array back to the start + # Calculate rear pointer, points to rear index + 1 + # Use modulo operation to wrap rear around to the head after passing the tail of the array rear: int = (self._front + self._size) % self.capacity() - # Add num to the rear + # Add num to the rear of the queue self._nums[rear] = num self._size += 1 def pop(self) -> int: """Dequeue""" num: int = self.peek() - # Move front pointer one position backward, returning to the head of the array if it exceeds the tail + # Front pointer moves one position backward, if it passes the tail, return to the head of the array self._front = (self._front + 1) % self.capacity() self._size -= 1 return num def peek(self) -> int: - """Access front element""" + """Access front of the queue element""" if self.is_empty(): raise IndexError("Queue is empty") return self._nums[self._front] def to_list(self) -> list[int]: - """Return array for printing""" + """Return list for printing""" res = [0] * self.size() j: int = self._front for i in range(self.size()): @@ -712,17 +1401,17 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar === "C++" ```cpp title="array_queue.cpp" - /* Queue class based on circular array */ + /* Queue based on circular array implementation */ class ArrayQueue { private: int *nums; // Array for storing queue elements - int front; // Front pointer, pointing to the front element + int front; // Front pointer, points to the front of the queue element int queSize; // Queue length int queCapacity; // Queue capacity public: ArrayQueue(int capacity) { - // Initialize an array + // Initialize array nums = new int[capacity]; queCapacity = capacity; front = queSize = 0; @@ -742,7 +1431,7 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar return queSize; } - /* Determine if the queue is empty */ + /* Check if the queue is empty */ bool isEmpty() { return size() == 0; } @@ -753,10 +1442,10 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar cout << "Queue is full" << endl; return; } - // Calculate rear pointer, pointing to rear index + 1 - // Use modulo operation to wrap the rear pointer from the end of the array back to the start + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue int rear = (front + queSize) % queCapacity; - // Add num to the rear + // Front pointer moves one position backward nums[rear] = num; queSize++; } @@ -764,13 +1453,13 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar /* Dequeue */ int pop() { int num = peek(); - // Move front pointer one position backward, returning to the head of the array if it exceeds the tail + // Move front pointer backward by one position, if it passes the tail, return to array head front = (front + 1) % queCapacity; queSize--; return num; } - /* Access front element */ + /* Return list for printing */ int peek() { if (isEmpty()) throw out_of_range("Queue is empty"); @@ -779,7 +1468,7 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar /* Convert array to Vector and return */ vector toVector() { - // Only convert elements within valid length range + // Elements enqueue vector arr(queSize); for (int i = 0, j = front; i < queSize; i++, j++) { arr[i] = nums[j % queCapacity]; @@ -792,10 +1481,10 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar === "Java" ```java title="array_queue.java" - /* Queue class based on circular array */ + /* Queue based on circular array implementation */ class ArrayQueue { private int[] nums; // Array for storing queue elements - private int front; // Front pointer, pointing to the front element + private int front; // Front pointer, points to the front of the queue element private int queSize; // Queue length public ArrayQueue(int capacity) { @@ -813,7 +1502,7 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar return queSize; } - /* Determine if the queue is empty */ + /* Check if the queue is empty */ public boolean isEmpty() { return queSize == 0; } @@ -824,10 +1513,10 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar System.out.println("Queue is full"); return; } - // Calculate rear pointer, pointing to rear index + 1 - // Use modulo operation to wrap the rear pointer from the end of the array back to the start + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue int rear = (front + queSize) % capacity(); - // Add num to the rear + // Front pointer moves one position backward nums[rear] = num; queSize++; } @@ -835,13 +1524,13 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar /* Dequeue */ public int pop() { int num = peek(); - // Move front pointer one position backward, returning to the head of the array if it exceeds the tail + // Move front pointer backward by one position, if it passes the tail, return to array head front = (front + 1) % capacity(); queSize--; return num; } - /* Access front element */ + /* Return list for printing */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); @@ -850,7 +1539,7 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar /* Return array */ public int[] toArray() { - // Only convert elements within valid length range + // Elements enqueue int[] res = new int[queSize]; for (int i = 0, j = front; i < queSize; i++, j++) { res[i] = nums[j % capacity()]; @@ -863,74 +1552,740 @@ In a circular array, `front` or `rear` needs to loop back to the start of the ar === "C#" ```csharp title="array_queue.cs" - [class]{ArrayQueue}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue { + int[] nums; // Array for storing queue elements + int front; // Front pointer, points to the front of the queue element + int queSize; // Queue length + + public ArrayQueue(int capacity) { + nums = new int[capacity]; + front = queSize = 0; + } + + /* Get the capacity of the queue */ + int Capacity() { + return nums.Length; + } + + /* Get the length of the queue */ + public int Size() { + return queSize; + } + + /* Check if the queue is empty */ + public bool IsEmpty() { + return queSize == 0; + } + + /* Enqueue */ + public void Push(int num) { + if (queSize == Capacity()) { + Console.WriteLine("Queue is full"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + int rear = (front + queSize) % Capacity(); + // Front pointer moves one position backward + nums[rear] = num; + queSize++; + } + + /* Dequeue */ + public int Pop() { + int num = Peek(); + // Move front pointer backward by one position, if it passes the tail, return to array head + front = (front + 1) % Capacity(); + queSize--; + return num; + } + + /* Return list for printing */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return nums[front]; + } + + /* Return array */ + public int[] ToArray() { + // Elements enqueue + 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" - [class]{arrayQueue}-[func]{} + /* Queue based on circular array implementation */ + type arrayQueue struct { + nums []int // Array for storing queue elements + front int // Front pointer, points to the front of the queue element + queSize int // Queue length + queCapacity int // Queue capacity (maximum number of elements) + } + + /* Access front of the queue element */ + func newArrayQueue(queCapacity int) *arrayQueue { + return &arrayQueue{ + nums: make([]int, queCapacity), + queCapacity: queCapacity, + front: 0, + queSize: 0, + } + } + + /* Get the length of the queue */ + func (q *arrayQueue) size() int { + return q.queSize + } + + /* Check if the queue is empty */ + func (q *arrayQueue) isEmpty() bool { + return q.queSize == 0 + } + + /* Enqueue */ + func (q *arrayQueue) push(num int) { + // When rear == queCapacity, queue is full + if q.queSize == q.queCapacity { + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + rear := (q.front + q.queSize) % q.queCapacity + // Front pointer moves one position backward + q.nums[rear] = num + q.queSize++ + } + + /* Dequeue */ + func (q *arrayQueue) pop() any { + num := q.peek() + if num == nil { + return nil + } + + // Move front pointer backward by one position, if it passes the tail, return to array head + q.front = (q.front + 1) % q.queCapacity + q.queSize-- + return num + } + + /* Return list for printing */ + func (q *arrayQueue) peek() any { + if q.isEmpty() { + return nil + } + return q.nums[q.front] + } + + /* Get Slice for printing */ + 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}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue { + private var nums: [Int] // Array for storing queue elements + private var front: Int // Front pointer, points to the front of the queue element + private var _size: Int // Queue length + + init(capacity: Int) { + // Initialize array + nums = Array(repeating: 0, count: capacity) + front = 0 + _size = 0 + } + + /* Get the capacity of the queue */ + func capacity() -> Int { + nums.count + } + + /* Get the length of the queue */ + func size() -> Int { + _size + } + + /* Check if the queue is empty */ + func isEmpty() -> Bool { + size() == 0 + } + + /* Enqueue */ + func push(num: Int) { + if size() == capacity() { + print("Queue is full") + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + let rear = (front + size()) % capacity() + // Front pointer moves one position backward + nums[rear] = num + _size += 1 + } + + /* Dequeue */ + @discardableResult + func pop() -> Int { + let num = peek() + // Move front pointer backward by one position, if it passes the tail, return to array head + front = (front + 1) % capacity() + _size -= 1 + return num + } + + /* Return list for printing */ + func peek() -> Int { + if isEmpty() { + fatalError("Queue is empty") + } + return nums[front] + } + + /* Return array */ + func toArray() -> [Int] { + // Elements enqueue + (front ..< front + size()).map { nums[$0 % capacity()] } + } + } ``` === "JS" ```javascript title="array_queue.js" - [class]{ArrayQueue}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue { + #nums; // Array for storing queue elements + #front = 0; // Front pointer, points to the front of the queue element + #queSize = 0; // Queue length + + constructor(capacity) { + this.#nums = new Array(capacity); + } + + /* Get the capacity of the queue */ + get capacity() { + return this.#nums.length; + } + + /* Get the length of the queue */ + get size() { + return this.#queSize; + } + + /* Check if the queue is empty */ + isEmpty() { + return this.#queSize === 0; + } + + /* Enqueue */ + push(num) { + if (this.size === this.capacity) { + console.log('Queue is full'); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + const rear = (this.#front + this.size) % this.capacity; + // Front pointer moves one position backward + this.#nums[rear] = num; + this.#queSize++; + } + + /* Dequeue */ + pop() { + const num = this.peek(); + // Move front pointer backward by one position, if it passes the tail, return to array head + this.#front = (this.#front + 1) % this.capacity; + this.#queSize--; + return num; + } + + /* Return list for printing */ + peek() { + if (this.isEmpty()) throw new Error('Queue is empty'); + return this.#nums[this.#front]; + } + + /* Return Array */ + toArray() { + // Elements enqueue + 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}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue { + private nums: number[]; // Array for storing queue elements + private front: number; // Front pointer, points to the front of the queue element + private queSize: number; // Queue length + + constructor(capacity: number) { + this.nums = new Array(capacity); + this.front = this.queSize = 0; + } + + /* Get the capacity of the queue */ + get capacity(): number { + return this.nums.length; + } + + /* Get the length of the queue */ + get size(): number { + return this.queSize; + } + + /* Check if the queue is empty */ + isEmpty(): boolean { + return this.queSize === 0; + } + + /* Enqueue */ + push(num: number): void { + if (this.size === this.capacity) { + console.log('Queue is full'); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + const rear = (this.front + this.queSize) % this.capacity; + // Front pointer moves one position backward + this.nums[rear] = num; + this.queSize++; + } + + /* Dequeue */ + pop(): number { + const num = this.peek(); + // Move front pointer backward by one position, if it passes the tail, return to array head + this.front = (this.front + 1) % this.capacity; + this.queSize--; + return num; + } + + /* Return list for printing */ + peek(): number { + if (this.isEmpty()) throw new Error('Queue is empty'); + return this.nums[this.front]; + } + + /* Return Array */ + toArray(): number[] { + // Elements enqueue + 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}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue { + late List _nums; // Array for storing queue elements + late int _front; // Front pointer, points to the front of the queue element + late int _queSize; // Queue length + + ArrayQueue(int capacity) { + _nums = List.filled(capacity, 0); + _front = _queSize = 0; + } + + /* Get the capacity of the queue */ + int capaCity() { + return _nums.length; + } + + /* Get the length of the queue */ + int size() { + return _queSize; + } + + /* Check if the queue is empty */ + bool isEmpty() { + return _queSize == 0; + } + + /* Enqueue */ + void push(int _num) { + if (_queSize == capaCity()) { + throw Exception("Queue is full"); + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + int rear = (_front + _queSize) % capaCity(); + // Add _num to queue rear + _nums[rear] = _num; + _queSize++; + } + + /* Dequeue */ + int pop() { + int _num = peek(); + // Move front pointer backward by one position, if it passes the tail, return to array head + _front = (_front + 1) % capaCity(); + _queSize--; + return _num; + } + + /* Return list for printing */ + int peek() { + if (isEmpty()) { + throw Exception("Queue is empty"); + } + return _nums[_front]; + } + + /* Return Array */ + List toArray() { + // Elements enqueue + 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" - [class]{ArrayQueue}-[func]{} + /* Queue based on circular array implementation */ + struct ArrayQueue { + nums: Vec, // Array for storing queue elements + front: i32, // Front pointer, points to the front of the queue element + que_size: i32, // Queue length + que_capacity: i32, // Queue capacity + } + + impl ArrayQueue { + /* Constructor */ + fn new(capacity: i32) -> ArrayQueue { + ArrayQueue { + nums: vec![T::default(); capacity as usize], + front: 0, + que_size: 0, + que_capacity: capacity, + } + } + + /* Get the capacity of the queue */ + fn capacity(&self) -> i32 { + self.que_capacity + } + + /* Get the length of the queue */ + fn size(&self) -> i32 { + self.que_size + } + + /* Check if the queue is empty */ + fn is_empty(&self) -> bool { + self.que_size == 0 + } + + /* Enqueue */ + fn push(&mut self, num: T) { + if self.que_size == self.capacity() { + println!("Queue is full"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + let rear = (self.front + self.que_size) % self.que_capacity; + // Front pointer moves one position backward + self.nums[rear as usize] = num; + self.que_size += 1; + } + + /* Dequeue */ + fn pop(&mut self) -> T { + let num = self.peek(); + // Move front pointer backward by one position, if it passes the tail, return to array head + self.front = (self.front + 1) % self.que_capacity; + self.que_size -= 1; + num + } + + /* Return list for printing */ + fn peek(&self) -> T { + if self.is_empty() { + panic!("index out of bounds"); + } + self.nums[self.front as usize] + } + + /* Return array */ + fn to_vector(&self) -> Vec { + let cap = self.que_capacity; + let mut j = self.front; + let mut arr = vec![T::default(); cap 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" - [class]{ArrayQueue}-[func]{} + /* Queue based on circular array implementation */ + typedef struct { + int *nums; // Array for storing queue elements + int front; // Front pointer, points to the front of the queue element + int queSize; // Rear pointer, points to rear + 1 + int queCapacity; // Queue capacity + } ArrayQueue; + + /* Constructor */ + ArrayQueue *newArrayQueue(int capacity) { + ArrayQueue *queue = (ArrayQueue *)malloc(sizeof(ArrayQueue)); + // Initialize array + queue->queCapacity = capacity; + queue->nums = (int *)malloc(sizeof(int) * queue->queCapacity); + queue->front = queue->queSize = 0; + return queue; + } + + /* Destructor */ + void delArrayQueue(ArrayQueue *queue) { + free(queue->nums); + free(queue); + } + + /* Get the capacity of the queue */ + int capacity(ArrayQueue *queue) { + return queue->queCapacity; + } + + /* Get the length of the queue */ + int size(ArrayQueue *queue) { + return queue->queSize; + } + + /* Check if the queue is empty */ + bool empty(ArrayQueue *queue) { + return queue->queSize == 0; + } + + /* Return list for printing */ + int peek(ArrayQueue *queue) { + assert(size(queue) != 0); + return queue->nums[queue->front]; + } + + /* Enqueue */ + void push(ArrayQueue *queue, int num) { + if (size(queue) == capacity(queue)) { + printf("Queue is full\r\n"); + return; + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + int rear = (queue->front + queue->queSize) % queue->queCapacity; + // Front pointer moves one position backward + queue->nums[rear] = num; + queue->queSize++; + } + + /* Dequeue */ + int pop(ArrayQueue *queue) { + int num = peek(queue); + // Move front pointer backward by one position, if it passes the tail, return to array head + queue->front = (queue->front + 1) % queue->queCapacity; + queue->queSize--; + return num; + } + + /* Return array for printing */ + int *toArray(ArrayQueue *queue, int *queSize) { + *queSize = queue->queSize; + int *res = (int *)calloc(queue->queSize, sizeof(int)); + int j = queue->front; + for (int i = 0; i < queue->queSize; i++) { + res[i] = queue->nums[j % queue->queCapacity]; + j++; + } + return res; + } ``` === "Kotlin" ```kotlin title="array_queue.kt" - [class]{ArrayQueue}-[func]{} + /* Queue based on circular array implementation */ + class ArrayQueue(capacity: Int) { + private val nums: IntArray = IntArray(capacity) // Array for storing queue elements + private var front: Int = 0 // Front pointer, points to the front of the queue element + private var queSize: Int = 0 // Queue length + + /* Get the capacity of the queue */ + fun capacity(): Int { + return nums.size + } + + /* Get the length of the queue */ + fun size(): Int { + return queSize + } + + /* Check if the queue is empty */ + fun isEmpty(): Boolean { + return queSize == 0 + } + + /* Enqueue */ + fun push(num: Int) { + if (queSize == capacity()) { + println("Queue is full") + return + } + // Use modulo operation to wrap rear around to the head after passing the tail of the array + // Add num to the rear of the queue + val rear = (front + queSize) % capacity() + // Front pointer moves one position backward + nums[rear] = num + queSize++ + } + + /* Dequeue */ + fun pop(): Int { + val num = peek() + // Move front pointer backward by one position, if it passes the tail, return to array head + front = (front + 1) % capacity() + queSize-- + return num + } + + /* Return list for printing */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return nums[front] + } + + /* Return array */ + fun toArray(): IntArray { + // Elements enqueue + val res = IntArray(queSize) + var i = 0 + var j = front + while (i < queSize) { + res[i] = nums[j % capacity()] + i++ + j++ + } + return res + } + } ``` === "Ruby" ```ruby title="array_queue.rb" - [class]{ArrayQueue}-[func]{} + ### Queue based on circular array ### + class ArrayQueue + ### Get queue length ### + attr_reader :size + + ### Constructor ### + def initialize(size) + @nums = Array.new(size, 0) # Array for storing queue elements + @front = 0 # Front pointer, points to the front of the queue element + @size = 0 # Queue length + end + + ### Get queue capacity ### + def capacity + @nums.length + end + + ### Check if queue is empty ### + def is_empty? + size.zero? + end + + ### Enqueue ### + def push(num) + raise IndexError, 'Queue is full' if size == capacity + + # Use modulo operation to wrap rear around to the head after passing the tail of the array + # Add num to the rear of the queue + rear = (@front + size) % capacity + # Front pointer moves one position backward + @nums[rear] = num + @size += 1 + end + + ### Dequeue ### + def pop + num = peek + # Move front pointer backward by one position, if it passes the tail, return to array head + @front = (@front + 1) % capacity + @size -= 1 + num + end + + ### Access front element ### + def peek + raise IndexError, 'Queue is empty' if is_empty? + + @nums[@front] + end + + ### Return list for printing ### + def to_array + res = Array.new(size, 0) + j = @front + + for i in 0...size + res[i] = @nums[j % capacity] + j += 1 + end + + res + end + end ``` -=== "Zig" +The queue implemented above still has limitations: its length is immutable. However, this problem is not difficult to solve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves. - ```zig title="array_queue.zig" - [class]{ArrayQueue}-[func]{} - ``` +The comparison conclusions for the two implementations are consistent with those for stacks and will not be repeated here. -The above implementation of the queue still has its limitations: its length is fixed. However, this issue is not difficult to resolve. We can replace the array with a dynamic array that can expand itself if needed. Interested readers can try to implement this themselves. +## 5.2.3   Typical Applications of Queue -The comparison of the two implementations is consistent with that of the stack and is not repeated here. - -## 5.2.3   Typical applications of queue - -- **Amazon orders**: After shoppers place orders, these orders join a queue, and the system processes them in order. During events like Singles' Day, a massive number of orders are generated in a short time, making high concurrency a key challenge for engineers. -- **Various to-do lists**: Any scenario requiring a "first-come, first-served" functionality, such as a printer's task queue or a restaurant's food delivery queue, can effectively maintain the order of processing with a queue. +- **Taobao orders**. After shoppers place orders, the orders are added to a queue, and the system subsequently processes the orders in the queue according to their sequence. During Double Eleven, massive orders are generated in a short time, and high concurrency becomes a key challenge that engineers need to tackle. +- **Various to-do tasks**. Any scenario that needs to implement "first come, first served" functionality, such as a printer's task queue or a restaurant's order queue, can effectively maintain the processing order using queues. diff --git a/en/docs/chapter_stack_and_queue/stack.md b/en/docs/chapter_stack_and_queue/stack.md index 166d155b4..f8cf1756b 100755 --- a/en/docs/chapter_stack_and_queue/stack.md +++ b/en/docs/chapter_stack_and_queue/stack.md @@ -4,299 +4,299 @@ comments: true # 5.1   Stack -A stack is a linear data structure that follows the principle of Last-In-First-Out (LIFO). +A stack is a linear data structure that follows the Last In First Out (LIFO) logic. -We can compare a stack to a pile of plates on a table. To access the bottom plate, one must first remove the plates on top. By replacing the plates with various types of elements (such as integers, characters, objects, etc.), we obtain the data structure known as a stack. +We can compare a stack to a pile of plates on a table. If we specify that only one plate can be moved at a time, then to get the bottom plate, we must first remove the plates above it one by one. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we get the stack data structure. -As shown in Figure 5-1, we refer to the top of the pile of elements as the "top of the stack" and the bottom as the "bottom of the stack." The operation of adding elements to the top of the stack is called "push," and the operation of removing the top element is called "pop." +As shown in Figure 5-1, we call the top of the stacked elements the "top" and the bottom the "base." The operation of adding an element to the top is called "push," and the operation of removing the top element is called "pop." -![Stack's last-in-first-out rule](stack.assets/stack_operations.png){ class="animation-figure" } +![LIFO rule of stack](stack.assets/stack_operations.png){ class="animation-figure" } -

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

+

Figure 5-1   LIFO rule of stack

-## 5.1.1   Common operations on stack +## 5.1.1   Common Stack Operations -The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use `push()`, `pop()`, and `peek()` as examples. +The common operations on a stack are shown in Table 5-1. The specific method names depend on the programming language used. Here, we use the common naming convention of `push()`, `pop()`, and `peek()`. -

Table 5-1   Efficiency of stack operations

+

Table 5-1   Efficiency of Stack Operations

-| Method | Description | Time Complexity | -| -------- | ----------------------------------------------- | --------------- | -| `push()` | Push an element onto the stack (add to the top) | $O(1)$ | -| `pop()` | Pop the top element from the stack | $O(1)$ | -| `peek()` | Access the top element of the stack | $O(1)$ | +| Method | Description | Time Complexity | +| -------- | ---------------------------------------------- | --------------- | +| `push()` | Push element onto stack (add to top) | $O(1)$ | +| `pop()` | Pop top element from stack | $O(1)$ | +| `peek()` | Access top element | $O(1)$ |
-Typically, we can directly use the stack class built into the programming language. However, some languages may not specifically provide a stack class. In these cases, we can use the language's "array" or "linked list" as a stack and ignore operations that are not related to stack logic in the program. +Typically, we can directly use the built-in stack class provided by the programming language. However, some languages may not provide a dedicated stack class. In these cases, we can use the language's "array" or "linked list" as a stack and ignore operations unrelated to the stack in the program logic. === "Python" ```python title="stack.py" - # Initialize the stack - # Python does not have a built-in stack class, so a list can be used as a stack + # Initialize stack + # Python does not have a built-in stack class, can use list as a stack stack: list[int] = [] - # Push elements onto the stack + # Push elements stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4) - # Access the top element of the stack + # Access top element peek: int = stack[-1] - # Pop an element from the stack + # Pop element pop: int = stack.pop() - # Get the length of the stack + # Get stack length size: int = len(stack) - # Check if the stack is empty + # Check if empty is_empty: bool = len(stack) == 0 ``` === "C++" ```cpp title="stack.cpp" - /* Initialize the stack */ + /* Initialize stack */ stack stack; - /* Push elements onto the stack */ + /* Push elements */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* Access the top element of the stack */ + /* Access top element */ int top = stack.top(); - /* Pop an element from the stack */ + /* Pop element */ stack.pop(); // No return value - /* Get the length of the stack */ + /* Get stack length */ int size = stack.size(); - /* Check if the stack is empty */ + /* Check if empty */ bool empty = stack.empty(); ``` === "Java" ```java title="stack.java" - /* Initialize the stack */ + /* Initialize stack */ Stack stack = new Stack<>(); - /* Push elements onto the stack */ + /* Push elements */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* Access the top element of the stack */ + /* Access top element */ int peek = stack.peek(); - /* Pop an element from the stack */ + /* Pop element */ int pop = stack.pop(); - /* Get the length of the stack */ + /* Get stack length */ int size = stack.size(); - /* Check if the stack is empty */ + /* Check if empty */ boolean isEmpty = stack.isEmpty(); ``` === "C#" ```csharp title="stack.cs" - /* Initialize the stack */ + /* Initialize stack */ Stack stack = new(); - /* Push elements onto the stack */ + /* Push elements */ stack.Push(1); stack.Push(3); stack.Push(2); stack.Push(5); stack.Push(4); - /* Access the top element of the stack */ + /* Access top element */ int peek = stack.Peek(); - /* Pop an element from the stack */ + /* Pop element */ int pop = stack.Pop(); - /* Get the length of the stack */ + /* Get stack length */ int size = stack.Count; - /* Check if the stack is empty */ + /* Check if empty */ bool isEmpty = stack.Count == 0; ``` === "Go" ```go title="stack_test.go" - /* Initialize the stack */ - // In Go, it is recommended to use a Slice as a stack + /* Initialize stack */ + // In Go, it is recommended to use Slice as a stack var stack []int - /* Push elements onto the stack */ + /* Push elements */ stack = append(stack, 1) stack = append(stack, 3) stack = append(stack, 2) stack = append(stack, 5) stack = append(stack, 4) - /* Access the top element of the stack */ + /* Access top element */ peek := stack[len(stack)-1] - /* Pop an element from the stack */ + /* Pop element */ pop := stack[len(stack)-1] stack = stack[:len(stack)-1] - /* Get the length of the stack */ + /* Get stack length */ size := len(stack) - /* Check if the stack is empty */ + /* Check if empty */ isEmpty := len(stack) == 0 ``` === "Swift" ```swift title="stack.swift" - /* Initialize the stack */ - // Swift does not have a built-in stack class, so Array can be used as a stack + /* Initialize stack */ + // Swift does not have a built-in stack class, can use Array as a stack var stack: [Int] = [] - /* Push elements onto the stack */ + /* Push elements */ stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4) - /* Access the top element of the stack */ + /* Access top element */ let peek = stack.last! - /* Pop an element from the stack */ + /* Pop element */ let pop = stack.removeLast() - /* Get the length of the stack */ + /* Get stack length */ let size = stack.count - /* Check if the stack is empty */ + /* Check if empty */ let isEmpty = stack.isEmpty ``` === "JS" ```javascript title="stack.js" - /* Initialize the stack */ - // JavaScript does not have a built-in stack class, so Array can be used as a stack + /* Initialize stack */ + // JavaScript does not have a built-in stack class, can use Array as a stack const stack = []; - /* Push elements onto the stack */ + /* Push elements */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* Access the top element of the stack */ + /* Access top element */ const peek = stack[stack.length-1]; - /* Pop an element from the stack */ + /* Pop element */ const pop = stack.pop(); - /* Get the length of the stack */ + /* Get stack length */ const size = stack.length; - /* Check if the stack is empty */ + /* Check if empty */ const is_empty = stack.length === 0; ``` === "TS" ```typescript title="stack.ts" - /* Initialize the stack */ - // TypeScript does not have a built-in stack class, so Array can be used as a stack + /* Initialize stack */ + // TypeScript does not have a built-in stack class, can use Array as a stack const stack: number[] = []; - /* Push elements onto the stack */ + /* Push elements */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* Access the top element of the stack */ + /* Access top element */ const peek = stack[stack.length - 1]; - /* Pop an element from the stack */ + /* Pop element */ const pop = stack.pop(); - /* Get the length of the stack */ + /* Get stack length */ const size = stack.length; - /* Check if the stack is empty */ + /* Check if empty */ const is_empty = stack.length === 0; ``` === "Dart" ```dart title="stack.dart" - /* Initialize the stack */ - // Dart does not have a built-in stack class, so List can be used as a stack + /* Initialize stack */ + // Dart does not have a built-in stack class, can use List as a stack List stack = []; - /* Push elements onto the stack */ + /* Push elements */ stack.add(1); stack.add(3); stack.add(2); stack.add(5); stack.add(4); - /* Access the top element of the stack */ + /* Access top element */ int peek = stack.last; - /* Pop an element from the stack */ + /* Pop element */ int pop = stack.removeLast(); - /* Get the length of the stack */ + /* Get stack length */ int size = stack.length; - /* Check if the stack is empty */ + /* Check if empty */ bool isEmpty = stack.isEmpty; ``` === "Rust" ```rust title="stack.rs" - /* Initialize the stack */ + /* Initialize stack */ // Use Vec as a stack let mut stack: Vec = Vec::new(); - /* Push elements onto the stack */ + /* Push elements */ stack.push(1); stack.push(3); stack.push(2); stack.push(5); stack.push(4); - /* Access the top element of the stack */ + /* Access top element */ let top = stack.last().unwrap(); - /* Pop an element from the stack */ + /* Pop element */ let pop = stack.pop().unwrap(); - /* Get the length of the stack */ + /* Get stack length */ let size = stack.len(); - /* Check if the stack is empty */ + /* Check if empty */ let is_empty = stack.is_empty(); ``` @@ -309,13 +309,54 @@ Typically, we can directly use the stack class built into the programming langua === "Kotlin" ```kotlin title="stack.kt" + /* Initialize stack */ + val stack = Stack() + /* Push elements */ + stack.push(1) + stack.push(3) + stack.push(2) + stack.push(5) + stack.push(4) + + /* Access top element */ + val peek = stack.peek() + + /* Pop element */ + val pop = stack.pop() + + /* Get stack length */ + val size = stack.size + + /* Check if empty */ + val isEmpty = stack.isEmpty() ``` -=== "Zig" +=== "Ruby" - ```zig title="stack.zig" + ```ruby title="stack.rb" + # Initialize stack + # Ruby does not have a built-in stack class, can use Array as a stack + stack = [] + # Push elements + stack << 1 + stack << 3 + stack << 2 + stack << 5 + stack << 4 + + # Access top element + peek = stack.last + + # Pop element + pop = stack.pop + + # Get stack length + size = stack.length + + # Check if empty + is_empty = stack.empty? ``` ??? pythontutor "Code Visualization" @@ -323,20 +364,20 @@ Typically, we can directly use the stack class built into the programming langua
-## 5.1.2   Implementing a stack +## 5.1.2   Stack Implementation To gain a deeper understanding of how a stack operates, let's try implementing a stack class ourselves. -A stack follows the principle of Last-In-First-Out, which means we can only add or remove elements at the top of the stack. However, both arrays and linked lists allow adding and removing elements at any position, **therefore a stack can be seen as a restricted array or linked list**. In other words, we can "shield" certain irrelevant operations of an array or linked list, aligning their external behavior with the characteristics of a stack. +A stack follows the LIFO principle, so we can only add or remove elements at the top. However, both arrays and linked lists allow adding and removing elements at any position. **Therefore, a stack can be viewed as a restricted array or linked list**. In other words, we can "shield" some irrelevant operations of arrays or linked lists so that their external logic conforms to the characteristics of a stack. -### 1.   Implementation based on a linked list +### 1.   Linked List Implementation -When implementing a stack using a linked list, we can consider the head node of the list as the top of the stack and the tail node as the bottom of the stack. +When implementing a stack using a linked list, we can treat the head node of the linked list as the top of the stack and the tail node as the base. -As shown in Figure 5-2, for the push operation, we simply insert elements at the head of the linked list. This method of node insertion is known as "head insertion." For the pop operation, we just need to remove the head node from the list. +As shown in Figure 5-2, for the push operation, we simply insert an element at the head of the linked list. This node insertion method is called the "head insertion method." For the pop operation, we just need to remove the head node from the linked list. === "LinkedListStack" - ![Implementing Stack with Linked List for Push and Pop Operations](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } + ![Push and pop operations in linked list implementation of stack](stack.assets/linkedlist_stack_step1.png){ class="animation-figure" } === "push()" ![linkedlist_stack_push](stack.assets/linkedlist_stack_step2_push.png){ class="animation-figure" } @@ -344,15 +385,15 @@ As shown in Figure 5-2, for the push operation, we simply insert elements at the === "pop()" ![linkedlist_stack_pop](stack.assets/linkedlist_stack_step3_pop.png){ class="animation-figure" } -

Figure 5-2   Implementing Stack with Linked List for Push and Pop Operations

+

Figure 5-2   Push and pop operations in linked list implementation of stack

-Below is an example code for implementing a stack based on a linked list: +Below is sample code for implementing a stack based on a linked list: === "Python" ```python title="linkedlist_stack.py" class LinkedListStack: - """Stack class based on linked list""" + """Stack based on linked list implementation""" def __init__(self): """Constructor""" @@ -364,7 +405,7 @@ Below is an example code for implementing a stack based on a linked list: return self._size def is_empty(self) -> bool: - """Determine if the stack is empty""" + """Check if the stack is empty""" return self._size == 0 def push(self, val: int): @@ -382,13 +423,13 @@ Below is an example code for implementing a stack based on a linked list: return num def peek(self) -> int: - """Access stack top element""" + """Access top of the stack element""" if self.is_empty(): raise IndexError("Stack is empty") return self._peek.val def to_list(self) -> list[int]: - """Convert to a list for printing""" + """Convert to list for printing""" arr = [] node = self._peek while node: @@ -401,11 +442,11 @@ Below is an example code for implementing a stack based on a linked list: === "C++" ```cpp title="linkedlist_stack.cpp" - /* Stack class based on linked list */ + /* Stack based on linked list implementation */ class LinkedListStack { private: - ListNode *stackTop; // Use the head node as the top of the stack - int stkSize; // Length of the stack + ListNode *stackTop; // Use head node as stack top + int stkSize; // Stack length public: LinkedListStack() { @@ -414,7 +455,7 @@ Below is an example code for implementing a stack based on a linked list: } ~LinkedListStack() { - // Traverse the linked list, remove nodes, free memory + // Traverse linked list to delete nodes and free memory freeMemoryLinkedList(stackTop); } @@ -423,7 +464,7 @@ Below is an example code for implementing a stack based on a linked list: return stkSize; } - /* Determine if the stack is empty */ + /* Check if the stack is empty */ bool isEmpty() { return size() == 0; } @@ -447,14 +488,14 @@ Below is an example code for implementing a stack based on a linked list: return num; } - /* Access stack top element */ + /* Return list for printing */ int top() { if (isEmpty()) throw out_of_range("Stack is empty"); return stackTop->val; } - /* Convert the List to Array and return */ + /* Convert List to Array and return */ vector toVector() { ListNode *node = stackTop; vector res(size()); @@ -470,10 +511,10 @@ Below is an example code for implementing a stack based on a linked list: === "Java" ```java title="linkedlist_stack.java" - /* Stack class based on linked list */ + /* Stack based on linked list implementation */ class LinkedListStack { - private ListNode stackPeek; // Use the head node as the top of the stack - private int stkSize = 0; // Length of the stack + private ListNode stackPeek; // Use head node as stack top + private int stkSize = 0; // Stack length public LinkedListStack() { stackPeek = null; @@ -484,7 +525,7 @@ Below is an example code for implementing a stack based on a linked list: return stkSize; } - /* Determine if the stack is empty */ + /* Check if the stack is empty */ public boolean isEmpty() { return size() == 0; } @@ -505,14 +546,14 @@ Below is an example code for implementing a stack based on a linked list: return num; } - /* Access stack top element */ + /* Return list for printing */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); return stackPeek.val; } - /* Convert the List to Array and return */ + /* Convert List to Array and return */ public int[] toArray() { ListNode node = stackPeek; int[] res = new int[size()]; @@ -528,75 +569,603 @@ Below is an example code for implementing a stack based on a linked list: === "C#" ```csharp title="linkedlist_stack.cs" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + class LinkedListStack { + ListNode? stackPeek; // Use head node as stack top + int stkSize = 0; // Stack length + + public LinkedListStack() { + stackPeek = null; + } + + /* Get the length of the stack */ + public int Size() { + return stkSize; + } + + /* Check if the stack is empty */ + public bool IsEmpty() { + return Size() == 0; + } + + /* Push */ + public void Push(int num) { + ListNode node = new(num) { + next = stackPeek + }; + stackPeek = node; + stkSize++; + } + + /* Pop */ + public int Pop() { + int num = Peek(); + stackPeek = stackPeek!.next; + stkSize--; + return num; + } + + /* Return list for printing */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stackPeek!.val; + } + + /* Convert List to Array and return */ + public int[] ToArray() { + if (stackPeek == null) + return []; + + ListNode? node = stackPeek; + int[] res = new int[Size()]; + for (int i = res.Length - 1; i >= 0; i--) { + res[i] = node!.val; + node = node.next; + } + return res; + } + } ``` === "Go" ```go title="linkedlist_stack.go" - [class]{linkedListStack}-[func]{} + /* Stack based on linked list implementation */ + type linkedListStack struct { + // Use built-in package list to implement stack + data *list.List + } + + /* Access top of the stack element */ + func newLinkedListStack() *linkedListStack { + return &linkedListStack{ + data: list.New(), + } + } + + /* Push */ + func (s *linkedListStack) push(value int) { + s.data.PushBack(value) + } + + /* Pop */ + func (s *linkedListStack) pop() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + s.data.Remove(e) + return e.Value + } + + /* Return list for printing */ + func (s *linkedListStack) peek() any { + if s.isEmpty() { + return nil + } + e := s.data.Back() + return e.Value + } + + /* Get the length of the stack */ + func (s *linkedListStack) size() int { + return s.data.Len() + } + + /* Check if the stack is empty */ + func (s *linkedListStack) isEmpty() bool { + return s.data.Len() == 0 + } + + /* Get List for printing */ + func (s *linkedListStack) toList() *list.List { + return s.data + } ``` === "Swift" ```swift title="linkedlist_stack.swift" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + class LinkedListStack { + private var _peek: ListNode? // Use head node as stack top + private var _size: Int // Stack length + + init() { + _size = 0 + } + + /* Get the length of the stack */ + func size() -> Int { + _size + } + + /* Check if the stack is empty */ + func isEmpty() -> Bool { + size() == 0 + } + + /* Push */ + func push(num: Int) { + let node = ListNode(x: num) + node.next = _peek + _peek = node + _size += 1 + } + + /* Pop */ + @discardableResult + func pop() -> Int { + let num = peek() + _peek = _peek?.next + _size -= 1 + return num + } + + /* Return list for printing */ + func peek() -> Int { + if isEmpty() { + fatalError("Stack is empty") + } + return _peek!.val + } + + /* Convert List to Array and return */ + func toArray() -> [Int] { + var node = _peek + var res = Array(repeating: 0, count: size()) + for i in res.indices.reversed() { + res[i] = node!.val + node = node?.next + } + return res + } + } ``` === "JS" ```javascript title="linkedlist_stack.js" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + class LinkedListStack { + #stackPeek; // Use head node as stack top + #stkSize = 0; // Stack length + + constructor() { + this.#stackPeek = null; + } + + /* Get the length of the stack */ + get size() { + return this.#stkSize; + } + + /* Check if the stack is empty */ + isEmpty() { + return this.size === 0; + } + + /* Push */ + push(num) { + const node = new ListNode(num); + node.next = this.#stackPeek; + this.#stackPeek = node; + this.#stkSize++; + } + + /* Pop */ + pop() { + const num = this.peek(); + this.#stackPeek = this.#stackPeek.next; + this.#stkSize--; + return num; + } + + /* Return list for printing */ + peek() { + if (!this.#stackPeek) throw new Error('Stack is empty'); + return this.#stackPeek.val; + } + + /* Convert linked list to Array and return */ + 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}-[func]{} + /* Stack based on linked list implementation */ + class LinkedListStack { + private stackPeek: ListNode | null; // Use head node as stack top + private stkSize: number = 0; // Stack length + + constructor() { + this.stackPeek = null; + } + + /* Get the length of the stack */ + get size(): number { + return this.stkSize; + } + + /* Check if the stack is empty */ + isEmpty(): boolean { + return this.size === 0; + } + + /* Push */ + push(num: number): void { + const node = new ListNode(num); + node.next = this.stackPeek; + this.stackPeek = node; + this.stkSize++; + } + + /* Pop */ + pop(): number { + const num = this.peek(); + if (!this.stackPeek) throw new Error('Stack is empty'); + this.stackPeek = this.stackPeek.next; + this.stkSize--; + return num; + } + + /* Return list for printing */ + peek(): number { + if (!this.stackPeek) throw new Error('Stack is empty'); + return this.stackPeek.val; + } + + /* Convert linked list to Array and return */ + 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}-[func]{} + /* Stack implemented based on linked list class */ + class LinkedListStack { + ListNode? _stackPeek; // Use head node as stack top + int _stkSize = 0; // Stack length + + LinkedListStack() { + _stackPeek = null; + } + + /* Get the length of the stack */ + int size() { + return _stkSize; + } + + /* Check if the stack is empty */ + bool isEmpty() { + return _stkSize == 0; + } + + /* Push */ + void push(int _num) { + final ListNode node = ListNode(_num); + node.next = _stackPeek; + _stackPeek = node; + _stkSize++; + } + + /* Pop */ + int pop() { + final int _num = peek(); + _stackPeek = _stackPeek!.next; + _stkSize--; + return _num; + } + + /* Return list for printing */ + int peek() { + if (_stackPeek == null) { + throw Exception("Stack is empty"); + } + return _stackPeek!.val; + } + + /* Convert linked list to List and return */ + 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" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + #[allow(dead_code)] + pub struct LinkedListStack { + stack_peek: Option>>>, // Use head node as stack top + stk_size: usize, // Stack length + } + + impl LinkedListStack { + pub fn new() -> Self { + Self { + stack_peek: None, + stk_size: 0, + } + } + + /* Get the length of the stack */ + pub fn size(&self) -> usize { + return self.stk_size; + } + + /* Check if the stack is empty */ + pub fn is_empty(&self) -> bool { + return self.size() == 0; + } + + /* Push */ + 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; + } + + /* Pop */ + pub fn pop(&mut self) -> Option { + self.stack_peek.take().map(|old_head| { + self.stack_peek = old_head.borrow_mut().next.take(); + self.stk_size -= 1; + + old_head.borrow().val + }) + } + + /* Return list for printing */ + pub fn peek(&self) -> Option<&Rc>>> { + self.stack_peek.as_ref() + } + + /* Convert List to Array and return */ + pub fn to_array(&self) -> Vec { + fn _to_array(head: Option<&Rc>>>) -> Vec { + if let Some(node) = head { + let mut nums = _to_array(node.borrow().next.as_ref()); + nums.push(node.borrow().val); + return nums; + } + return Vec::new(); + } + + _to_array(self.peek()) + } + } ``` === "C" ```c title="linkedlist_stack.c" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + typedef struct { + ListNode *top; // Use head node as stack top + int size; // Stack length + } LinkedListStack; + + /* Constructor */ + LinkedListStack *newLinkedListStack() { + LinkedListStack *s = malloc(sizeof(LinkedListStack)); + s->top = NULL; + s->size = 0; + return s; + } + + /* Destructor */ + void delLinkedListStack(LinkedListStack *s) { + while (s->top) { + ListNode *n = s->top->next; + free(s->top); + s->top = n; + } + free(s); + } + + /* Get the length of the stack */ + int size(LinkedListStack *s) { + return s->size; + } + + /* Check if the stack is empty */ + bool isEmpty(LinkedListStack *s) { + return size(s) == 0; + } + + /* Push */ + void push(LinkedListStack *s, int num) { + ListNode *node = (ListNode *)malloc(sizeof(ListNode)); + node->next = s->top; // Update new node's pointer field + node->val = num; // Update new node's data field + s->top = node; // Update stack top + s->size++; // Update stack size + } + + /* Return list for printing */ + int peek(LinkedListStack *s) { + if (s->size == 0) { + printf("Stack is empty\n"); + return INT_MAX; + } + return s->top->val; + } + + /* Pop */ + int pop(LinkedListStack *s) { + int val = peek(s); + ListNode *tmp = s->top; + s->top = s->top->next; + // Free memory + free(tmp); + s->size--; + return val; + } ``` === "Kotlin" ```kotlin title="linkedlist_stack.kt" - [class]{LinkedListStack}-[func]{} + /* Stack based on linked list implementation */ + class LinkedListStack( + private var stackPeek: ListNode? = null, // Use head node as stack top + private var stkSize: Int = 0 // Stack length + ) { + + /* Get the length of the stack */ + fun size(): Int { + return stkSize + } + + /* Check if the stack is empty */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* Push */ + fun push(num: Int) { + val node = ListNode(num) + node.next = stackPeek + stackPeek = node + stkSize++ + } + + /* Pop */ + fun pop(): Int? { + val num = peek() + stackPeek = stackPeek?.next + stkSize-- + return num + } + + /* Return list for printing */ + fun peek(): Int? { + if (isEmpty()) throw IndexOutOfBoundsException() + return stackPeek?._val + } + + /* Convert List to Array and return */ + fun toArray(): IntArray { + var node = stackPeek + val res = IntArray(size()) + for (i in res.size - 1 downTo 0) { + res[i] = node?._val!! + node = node.next + } + return res + } + } ``` === "Ruby" ```ruby title="linkedlist_stack.rb" - [class]{LinkedListStack}-[func]{} + ### Stack based on linked list ### + class LinkedListStack + attr_reader :size + + ### Constructor ### + def initialize + @size = 0 + end + + ### Check if stack is empty ### + def is_empty? + @peek.nil? + end + + ### Push ### + def push(val) + node = ListNode.new(val) + node.next = @peek + @peek = node + @size += 1 + end + + ### Pop ### + def pop + num = peek + @peek = @peek.next + @size -= 1 + num + end + + ### Access top element ### + def peek + raise IndexError, 'Stack is empty' if is_empty? + + @peek.val + end + + ### Convert linked list to Array and return ### + def to_array + arr = [] + node = @peek + while node + arr << node.val + node = node.next + end + arr.reverse + end + end ``` -=== "Zig" +### 2.   Array Implementation - ```zig title="linkedlist_stack.zig" - [class]{LinkedListStack}-[func]{} - ``` - -### 2.   Implementation based on an array - -When implementing a stack using an array, we can consider the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, respectively, both with a time complexity of $O(1)$. +When implementing a stack using an array, we can treat the end of the array as the top of the stack. As shown in Figure 5-3, push and pop operations correspond to adding and removing elements at the end of the array, both with a time complexity of $O(1)$. === "ArrayStack" - ![Implementing Stack with Array for Push and Pop Operations](stack.assets/array_stack_step1.png){ class="animation-figure" } + ![Push and pop operations in array implementation of stack](stack.assets/array_stack_step1.png){ class="animation-figure" } === "push()" ![array_stack_push](stack.assets/array_stack_step2_push.png){ class="animation-figure" } @@ -604,15 +1173,15 @@ When implementing a stack using an array, we can consider the end of the array a === "pop()" ![array_stack_pop](stack.assets/array_stack_step3_pop.png){ class="animation-figure" } -

Figure 5-3   Implementing Stack with Array for Push and Pop Operations

+

Figure 5-3   Push and pop operations in array implementation of stack

-Since the elements to be pushed onto the stack may continuously increase, we can use a dynamic array, thus avoiding the need to handle array expansion ourselves. Here is an example code: +Since elements pushed onto the stack may increase continuously, we can use a dynamic array, which eliminates the need to handle array expansion ourselves. Here is the sample code: === "Python" ```python title="array_stack.py" class ArrayStack: - """Stack class based on array""" + """Stack based on array implementation""" def __init__(self): """Constructor""" @@ -623,7 +1192,7 @@ Since the elements to be pushed onto the stack may continuously increase, we can return len(self._stack) def is_empty(self) -> bool: - """Determine if the stack is empty""" + """Check if the stack is empty""" return self.size() == 0 def push(self, item: int): @@ -637,20 +1206,20 @@ Since the elements to be pushed onto the stack may continuously increase, we can return self._stack.pop() def peek(self) -> int: - """Access stack top element""" + """Access top of the stack element""" if self.is_empty(): raise IndexError("Stack is empty") return self._stack[-1] def to_list(self) -> list[int]: - """Return array for printing""" + """Return list for printing""" return self._stack ``` === "C++" ```cpp title="array_stack.cpp" - /* Stack class based on array */ + /* Stack based on array implementation */ class ArrayStack { private: vector stack; @@ -661,7 +1230,7 @@ Since the elements to be pushed onto the stack may continuously increase, we can return stack.size(); } - /* Determine if the stack is empty */ + /* Check if the stack is empty */ bool isEmpty() { return stack.size() == 0; } @@ -678,7 +1247,7 @@ Since the elements to be pushed onto the stack may continuously increase, we can return num; } - /* Access stack top element */ + /* Return list for printing */ int top() { if (isEmpty()) throw out_of_range("Stack is empty"); @@ -695,12 +1264,12 @@ Since the elements to be pushed onto the stack may continuously increase, we can === "Java" ```java title="array_stack.java" - /* Stack class based on array */ + /* Stack based on array implementation */ class ArrayStack { private ArrayList stack; public ArrayStack() { - // Initialize the list (dynamic array) + // Initialize list (dynamic array) stack = new ArrayList<>(); } @@ -709,7 +1278,7 @@ Since the elements to be pushed onto the stack may continuously increase, we can return stack.size(); } - /* Determine if the stack is empty */ + /* Check if the stack is empty */ public boolean isEmpty() { return size() == 0; } @@ -726,14 +1295,14 @@ Since the elements to be pushed onto the stack may continuously increase, we can return stack.remove(size() - 1); } - /* Access stack top element */ + /* Return list for printing */ public int peek() { if (isEmpty()) throw new IndexOutOfBoundsException(); return stack.get(size() - 1); } - /* Convert the List to Array and return */ + /* Convert List to Array and return */ public Object[] toArray() { return stack.toArray(); } @@ -743,95 +1312,517 @@ Since the elements to be pushed onto the stack may continuously increase, we can === "C#" ```csharp title="array_stack.cs" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + List stack; + public ArrayStack() { + // Initialize list (dynamic array) + stack = []; + } + + /* Get the length of the stack */ + public int Size() { + return stack.Count; + } + + /* Check if the stack is empty */ + public bool IsEmpty() { + return Size() == 0; + } + + /* Push */ + public void Push(int num) { + stack.Add(num); + } + + /* Pop */ + public int Pop() { + if (IsEmpty()) + throw new Exception(); + var val = Peek(); + stack.RemoveAt(Size() - 1); + return val; + } + + /* Return list for printing */ + public int Peek() { + if (IsEmpty()) + throw new Exception(); + return stack[Size() - 1]; + } + + /* Convert List to Array and return */ + public int[] ToArray() { + return [.. stack]; + } + } ``` === "Go" ```go title="array_stack.go" - [class]{arrayStack}-[func]{} + /* Stack based on array implementation */ + type arrayStack struct { + data []int // Data + } + + /* Access top of the stack element */ + func newArrayStack() *arrayStack { + return &arrayStack{ + // Set stack length to 0, capacity to 16 + data: make([]int, 0, 16), + } + } + + /* Stack length */ + func (s *arrayStack) size() int { + return len(s.data) + } + + /* Is stack empty */ + func (s *arrayStack) isEmpty() bool { + return s.size() == 0 + } + + /* Push */ + func (s *arrayStack) push(v int) { + // Slice will automatically expand + s.data = append(s.data, v) + } + + /* Pop */ + func (s *arrayStack) pop() any { + val := s.peek() + s.data = s.data[:len(s.data)-1] + return val + } + + /* Get stack top element */ + func (s *arrayStack) peek() any { + if s.isEmpty() { + return nil + } + val := s.data[len(s.data)-1] + return val + } + + /* Get Slice for printing */ + func (s *arrayStack) toSlice() []int { + return s.data + } ``` === "Swift" ```swift title="array_stack.swift" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + private var stack: [Int] + + init() { + // Initialize list (dynamic array) + stack = [] + } + + /* Get the length of the stack */ + func size() -> Int { + stack.count + } + + /* Check if the stack is empty */ + func isEmpty() -> Bool { + stack.isEmpty + } + + /* Push */ + func push(num: Int) { + stack.append(num) + } + + /* Pop */ + @discardableResult + func pop() -> Int { + if isEmpty() { + fatalError("Stack is empty") + } + return stack.removeLast() + } + + /* Return list for printing */ + func peek() -> Int { + if isEmpty() { + fatalError("Stack is empty") + } + return stack.last! + } + + /* Convert List to Array and return */ + func toArray() -> [Int] { + stack + } + } ``` === "JS" ```javascript title="array_stack.js" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + #stack; + constructor() { + this.#stack = []; + } + + /* Get the length of the stack */ + get size() { + return this.#stack.length; + } + + /* Check if the stack is empty */ + isEmpty() { + return this.#stack.length === 0; + } + + /* Push */ + push(num) { + this.#stack.push(num); + } + + /* Pop */ + pop() { + if (this.isEmpty()) throw new Error('Stack is empty'); + return this.#stack.pop(); + } + + /* Return list for printing */ + top() { + if (this.isEmpty()) throw new Error('Stack is empty'); + return this.#stack[this.#stack.length - 1]; + } + + /* Return Array */ + toArray() { + return this.#stack; + } + } ``` === "TS" ```typescript title="array_stack.ts" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + private stack: number[]; + constructor() { + this.stack = []; + } + + /* Get the length of the stack */ + get size(): number { + return this.stack.length; + } + + /* Check if the stack is empty */ + isEmpty(): boolean { + return this.stack.length === 0; + } + + /* Push */ + push(num: number): void { + this.stack.push(num); + } + + /* Pop */ + pop(): number | undefined { + if (this.isEmpty()) throw new Error('Stack is empty'); + return this.stack.pop(); + } + + /* Return list for printing */ + top(): number | undefined { + if (this.isEmpty()) throw new Error('Stack is empty'); + return this.stack[this.stack.length - 1]; + } + + /* Return Array */ + toArray() { + return this.stack; + } + } ``` === "Dart" ```dart title="array_stack.dart" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + late List _stack; + ArrayStack() { + _stack = []; + } + + /* Get the length of the stack */ + int size() { + return _stack.length; + } + + /* Check if the stack is empty */ + bool isEmpty() { + return _stack.isEmpty; + } + + /* Push */ + void push(int _num) { + _stack.add(_num); + } + + /* Pop */ + int pop() { + if (isEmpty()) { + throw Exception("Stack is empty"); + } + return _stack.removeLast(); + } + + /* Return list for printing */ + int peek() { + if (isEmpty()) { + throw Exception("Stack is empty"); + } + return _stack.last; + } + + /* Convert stack to Array and return */ + List toArray() => _stack; + } ``` === "Rust" ```rust title="array_stack.rs" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + struct ArrayStack { + stack: Vec, + } + + impl ArrayStack { + /* Access top of the stack element */ + fn new() -> ArrayStack { + ArrayStack:: { + stack: Vec::::new(), + } + } + + /* Get the length of the stack */ + fn size(&self) -> usize { + self.stack.len() + } + + /* Check if the stack is empty */ + fn is_empty(&self) -> bool { + self.size() == 0 + } + + /* Push */ + fn push(&mut self, num: T) { + self.stack.push(num); + } + + /* Pop */ + fn pop(&mut self) -> Option { + self.stack.pop() + } + + /* Return list for printing */ + fn peek(&self) -> Option<&T> { + if self.is_empty() { + panic!("Stack is empty") + }; + self.stack.last() + } + + /* Return &Vec */ + fn to_array(&self) -> &Vec { + &self.stack + } + } ``` === "C" ```c title="array_stack.c" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + typedef struct { + int *data; + int size; + } ArrayStack; + + /* Constructor */ + ArrayStack *newArrayStack() { + ArrayStack *stack = malloc(sizeof(ArrayStack)); + // Initialize with large capacity to avoid expansion + stack->data = malloc(sizeof(int) * MAX_SIZE); + stack->size = 0; + return stack; + } + + /* Destructor */ + void delArrayStack(ArrayStack *stack) { + free(stack->data); + free(stack); + } + + /* Get the length of the stack */ + int size(ArrayStack *stack) { + return stack->size; + } + + /* Check if the stack is empty */ + bool isEmpty(ArrayStack *stack) { + return stack->size == 0; + } + + /* Push */ + void push(ArrayStack *stack, int num) { + if (stack->size == MAX_SIZE) { + printf("Stack is full\n"); + return; + } + stack->data[stack->size] = num; + stack->size++; + } + + /* Return list for printing */ + int peek(ArrayStack *stack) { + if (stack->size == 0) { + printf("Stack is empty\n"); + return INT_MAX; + } + return stack->data[stack->size - 1]; + } + + /* Pop */ + int pop(ArrayStack *stack) { + int val = peek(stack); + stack->size--; + return val; + } ``` === "Kotlin" ```kotlin title="array_stack.kt" - [class]{ArrayStack}-[func]{} + /* Stack based on array implementation */ + class ArrayStack { + // Initialize list (dynamic array) + private val stack = mutableListOf() + + /* Get the length of the stack */ + fun size(): Int { + return stack.size + } + + /* Check if the stack is empty */ + fun isEmpty(): Boolean { + return size() == 0 + } + + /* Push */ + fun push(num: Int) { + stack.add(num) + } + + /* Pop */ + fun pop(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack.removeAt(size() - 1) + } + + /* Return list for printing */ + fun peek(): Int { + if (isEmpty()) throw IndexOutOfBoundsException() + return stack[size() - 1] + } + + /* Convert List to Array and return */ + fun toArray(): Array { + return stack.toTypedArray() + } + } ``` === "Ruby" ```ruby title="array_stack.rb" - [class]{ArrayStack}-[func]{} + ### Stack based on array ### + class ArrayStack + ### Constructor ### + def initialize + @stack = [] + end + + ### Get stack length ### + def size + @stack.length + end + + ### Check if stack is empty ### + def is_empty? + @stack.empty? + end + + ### Push ### + def push(item) + @stack << item + end + + ### Pop ### + def pop + raise IndexError, 'Stack is empty' if is_empty? + + @stack.pop + end + + ### Access top element ### + def peek + raise IndexError, 'Stack is empty' if is_empty? + + @stack.last + end + + ### Return list for printing ### + def to_array + @stack + end + end ``` -=== "Zig" - - ```zig title="array_stack.zig" - [class]{ArrayStack}-[func]{} - ``` - -## 5.1.3   Comparison of the two implementations +## 5.1.3   Comparison of the Two Implementations **Supported Operations** -Both implementations support all the operations defined in a stack. The array implementation additionally supports random access, but this is beyond the scope of a stack definition and is generally not used. +Both implementations support all operations defined by the stack. The array implementation additionally supports random access, but this goes beyond the stack definition and is generally not used. **Time Efficiency** -In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and therefore higher efficiency. However, if the push operation exceeds the array capacity, it triggers a resizing mechanism, making the time complexity of that push operation $O(n)$. +In the array-based implementation, both push and pop operations occur in pre-allocated contiguous memory, which has good cache locality and is therefore more efficient. However, if pushing exceeds the array capacity, it triggers an expansion mechanism, causing the time complexity of that particular push operation to become $O(n)$. -In the linked list implementation, list expansion is very flexible, and there is no efficiency decrease issue as in array expansion. However, the push operation requires initializing a node object and modifying pointers, so its efficiency is relatively lower. If the elements being pushed are already node objects, then the initialization step can be skipped, improving efficiency. +In the linked list-based implementation, list expansion is very flexible, and there is no issue of reduced efficiency due to array expansion. However, the push operation requires initializing a node object and modifying pointers, so it is relatively less efficient. Nevertheless, if the pushed elements are already node objects, the initialization step can be omitted, thereby improving efficiency. -Thus, when the elements for push and pop operations are basic data types like `int` or `double`, we can draw the following conclusions: +In summary, when the elements pushed and popped are basic data types such as `int` or `double`, we can draw the following conclusions: -- The array-based stack implementation's efficiency decreases during expansion, but since expansion is a low-frequency operation, its average efficiency is higher. -- The linked list-based stack implementation provides more stable efficiency performance. +- The array-based stack implementation has reduced efficiency when expansion is triggered, but since expansion is an infrequent operation, the average efficiency is higher. +- The linked list-based stack implementation can provide more stable efficiency performance. **Space Efficiency** -When initializing a list, the system allocates an "initial capacity," which might exceed the actual need; moreover, the expansion mechanism usually increases capacity by a specific factor (like doubling), which may also exceed the actual need. Therefore, **the array-based stack might waste some space**. +When initializing a list, the system allocates an "initial capacity" that may exceed the actual need. Additionally, the expansion mechanism typically expands at a specific ratio (e.g., 2x), and the capacity after expansion may also exceed actual needs. Therefore, **the array-based stack implementation may cause some space wastage**. -However, since linked list nodes require extra space for storing pointers, **the space occupied by linked list nodes is relatively larger**. +However, since linked list nodes need to store additional pointers, **the space occupied by linked list nodes is relatively large**. -In summary, we cannot simply determine which implementation is more memory-efficient. It requires analysis based on specific circumstances. +In summary, we cannot simply determine which implementation is more memory-efficient and need to analyze the specific situation. -## 5.1.4   Typical applications of stack +## 5.1.4   Typical Applications of Stack -- **Back and forward in browsers, undo and redo in software**. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to go back to the previous page through the back operation, which is essentially a pop operation. To support both back and forward, two stacks are needed to work together. -- **Memory management in programs**. Each time a function is called, the system adds a stack frame at the top of the stack to record the function's context information. In recursive functions, the downward recursion phase keeps pushing onto the stack, while the upward backtracking phase keeps popping from the stack. +- **Back and forward in browsers, undo and redo in software**. Every time we open a new webpage, the browser pushes the previous page onto the stack, allowing us to return to the previous page via the back operation. The back operation is essentially performing a pop. To support both back and forward, two stacks are needed to work together. +- **Program memory management**. Each time a function is called, the system adds a stack frame to the top of the stack to record the function's context information. During recursion, the downward recursive phase continuously performs push operations, while the upward backtracking phase continuously performs pop operations. diff --git a/en/docs/chapter_stack_and_queue/summary.md b/en/docs/chapter_stack_and_queue/summary.md index 34cb292ca..71a904ef9 100644 --- a/en/docs/chapter_stack_and_queue/summary.md +++ b/en/docs/chapter_stack_and_queue/summary.md @@ -4,32 +4,32 @@ comments: true # 5.4   Summary -### 1.   Key review +### 1.   Key Review -- Stack is a data structure that follows the Last-In-First-Out (LIFO) principle and can be implemented using arrays or linked lists. -- In terms of time efficiency, the array implementation of the stack has a higher average efficiency. However, during expansion, the time complexity for a single push operation can degrade to $O(n)$. In contrast, the linked list implementation of a stack offers more stable efficiency. -- Regarding space efficiency, the array implementation of the stack may lead to a certain degree of space wastage. However, it's important to note that the memory space occupied by nodes in a linked list is generally larger than that for elements in an array. -- A queue is a data structure that follows the First-In-First-Out (FIFO) principle, and it can also be implemented using arrays or linked lists. The conclusions regarding time and space efficiency for queues are similar to those for stacks. -- A double-ended queue (deque) is a more flexible type of queue that allows adding and removing elements at both ends. +- A stack is a data structure that follows the LIFO principle and can be implemented using arrays or linked lists. +- In terms of time efficiency, the array implementation of a stack has higher average efficiency, but during expansion, the time complexity of a single push operation degrades to $O(n)$. In contrast, the linked list implementation of a stack provides more stable efficiency performance. +- In terms of space efficiency, the array implementation of a stack may lead to some degree of space wastage. However, it should be noted that the memory space occupied by linked list nodes is larger than that of array elements. +- A queue is a data structure that follows the FIFO principle and can also be implemented using arrays or linked lists. The conclusions regarding time efficiency and space efficiency comparisons for queues are similar to those for stacks mentioned above. +- A deque is a queue with greater flexibility that allows adding and removing elements at both ends. ### 2.   Q & A **Q**: Is the browser's forward and backward functionality implemented with a doubly linked list? -A browser's forward and backward navigation is essentially a manifestation of the "stack" concept. When a user visits a new page, the page is added to the top of the stack; when they click the back button, the page is popped from the top of the stack. A double-ended queue (deque) can conveniently implement some additional operations, as mentioned in the "Double-Ended Queue" section. +The forward and backward functionality of a browser is essentially a manifestation of a "stack." When a user visits a new page, that page is added to the top of the stack; when the user clicks the back button, that page is popped from the top of the stack. Using a deque can conveniently implement some additional operations, as mentioned in the "Deque" section. -**Q**: After popping from a stack, is it necessary to free the memory of the popped node? +**Q**: After popping from the stack, do we need to free the memory of the popped node? -If the popped node will still be used later, it's not necessary to free its memory. In languages like Java and Python that have automatic garbage collection, manual memory release is not necessary; in C and C++, manual memory release is required. +If the popped node will still be needed later, then memory does not need to be freed. If it won't be used afterward, languages like Java and Python have automatic garbage collection, so manual memory deallocation is not required; in C and C++, manual memory deallocation is necessary. -**Q**: A double-ended queue seems like two stacks joined together. What are its uses? +**Q**: A deque seems like two stacks joined together. What is its purpose? -A double-ended queue, which is a combination of a stack and a queue or two stacks joined together, exhibits both stack and queue logic. Thus, it can implement all applications of stacks and queues while offering more flexibility. +A deque is like a combination of a stack and a queue, or two stacks joined together. It exhibits the logic of both stack and queue, so it can implement all applications of stacks and queues, and is more flexible. -**Q**: How exactly are undo and redo implemented? +**Q**: How are undo and redo specifically implemented? -Undo and redo operations are implemented using two stacks: Stack `A` for undo and Stack `B` for redo. +Use two stacks: stack `A` for undo and stack `B` for redo. -1. Each time a user performs an operation, it is pushed onto Stack `A`, and Stack `B` is cleared. -2. When the user executes an "undo", the most recent operation is popped from Stack `A` and pushed onto Stack `B`. -3. When the user executes a "redo", the most recent operation is popped from Stack `B` and pushed back onto Stack `A`. +1. Whenever the user performs an operation, push this operation onto stack `A` and clear stack `B`. +2. When the user performs "undo," pop the most recent operation from stack `A` and push it onto stack `B`. +3. When the user performs "redo," pop the most recent operation from stack `B` and push it onto stack `A`. diff --git a/en/docs/chapter_tree/array_representation_of_tree.md b/en/docs/chapter_tree/array_representation_of_tree.md index caf46b9ca..de9800185 100644 --- a/en/docs/chapter_tree/array_representation_of_tree.md +++ b/en/docs/chapter_tree/array_representation_of_tree.md @@ -2,17 +2,17 @@ comments: true --- -# 7.3   Array representation of binary trees +# 7.3   Array Representation of Binary Trees -Under the linked list representation, the storage unit of a binary tree is a node `TreeNode`, with nodes connected by pointers. The basic operations of binary trees under the linked list representation were introduced in the previous section. +Under the linked list representation, the storage unit of a binary tree is a node `TreeNode`, and nodes are connected by pointers. The previous section introduced the basic operations of binary trees under the linked list representation. So, can we use an array to represent a binary tree? The answer is yes. -## 7.3.1   Representing perfect binary trees +## 7.3.1   Representing Perfect Binary Trees Let's analyze a simple case first. Given a perfect binary tree, we store all nodes in an array according to the order of level-order traversal, where each node corresponds to a unique array index. -Based on the characteristics of level-order traversal, we can deduce a "mapping formula" between the index of a parent node and its children: **If a node's index is $i$, then the index of its left child is $2i + 1$ and the right child is $2i + 2$**. Figure 7-12 shows the mapping relationship between the indices of various nodes. +Based on the characteristics of level-order traversal, we can derive a "mapping formula" between parent node index and child node indices: **If a node's index is $i$, then its left child index is $2i + 1$ and its right child index is $2i + 2$**. Figure 7-12 shows the mapping relationships between various node indices. ![Array representation of a perfect binary tree](array_representation_of_tree.assets/array_representation_binary_tree.png){ class="animation-figure" } @@ -20,9 +20,9 @@ Based on the characteristics of level-order traversal, we can deduce a "mapping **The mapping formula plays a role similar to the node references (pointers) in linked lists**. Given any node in the array, we can access its left (right) child node using the mapping formula. -## 7.3.2   Representing any binary tree +## 7.3.2   Representing Any Binary Tree -Perfect binary trees are a special case; there are often many `None` values in the middle levels of a binary tree. Since the sequence of level-order traversal does not include these `None` values, we cannot solely rely on this sequence to deduce the number and distribution of `None` values. **This means that multiple binary tree structures can match the same level-order traversal sequence**. +Perfect binary trees are a special case; in the middle levels of a binary tree, there are typically many `None` values. Since the level-order traversal sequence does not include these `None` values, we cannot infer the number and distribution of `None` values based on this sequence alone. **This means multiple binary tree structures can correspond to the same level-order traversal sequence**. As shown in Figure 7-13, given a non-perfect binary tree, the above method of array representation fails. @@ -125,26 +125,22 @@ To solve this problem, **we can consider explicitly writing out all `None` value ```kotlin title="" /* Array representation of a binary tree */ // Using null to represent empty slots - val tree = mutableListOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) + val tree = arrayOf( 1, 2, 3, 4, null, 6, 7, 8, 9, null, null, 12, null, null, 15 ) ``` === "Ruby" ```ruby title="" - - ``` - -=== "Zig" - - ```zig title="" - + ### Array representation of a binary tree ### + # Using nil to represent empty slots + tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` ![Array representation of any type of binary tree](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }

Figure 7-14   Array representation of any type of binary tree

-It's worth noting that **complete binary trees are very suitable for array representation**. Recalling the definition of a complete binary tree, `None` appears only at the bottom level and towards the right, **meaning all `None` values definitely appear at the end of the level-order traversal sequence**. +It's worth noting that **complete binary trees are very well-suited for array representation**. Recalling the definition of a complete binary tree, `None` only appears at the bottom level and towards the right, **meaning all `None` values must appear at the end of the level-order traversal sequence**. This means that when using an array to represent a complete binary tree, it's possible to omit storing all `None` values, which is very convenient. Figure 7-15 gives an example. @@ -154,14 +150,14 @@ This means that when using an array to represent a complete binary tree, it's po The following code implements a binary tree based on array representation, including the following operations: -- Given a node, obtain its value, left (right) child node, and parent node. -- Obtain the pre-order, in-order, post-order, and level-order traversal sequences. +- Given a certain node, obtain its value, left (right) child node, and parent node. +- Obtain the preorder, inorder, postorder, and level-order traversal sequences. === "Python" ```python title="array_binary_tree.py" class ArrayBinaryTree: - """Array-based binary tree class""" + """Binary tree class represented by array""" def __init__(self, arr: list[int | None]): """Constructor""" @@ -172,28 +168,28 @@ The following code implements a binary tree based on array representation, inclu return len(self._tree) def val(self, i: int) -> int | None: - """Get the value of the node at index i""" - # If the index is out of bounds, return None, representing a vacancy + """Get value of node at index i""" + # If index is out of bounds, return None, representing empty position if i < 0 or i >= self.size(): return None return self._tree[i] def left(self, i: int) -> int | None: - """Get the index of the left child of the node at index i""" + """Get index of left child node of node at index i""" return 2 * i + 1 def right(self, i: int) -> int | None: - """Get the index of the right child of the node at index i""" + """Get index of right child node of node at index i""" return 2 * i + 2 def parent(self, i: int) -> int | None: - """Get the index of the parent of the node at index i""" + """Get index of parent node of node at index i""" return (i - 1) // 2 def level_order(self) -> list[int]: """Level-order traversal""" self.res = [] - # Traverse array + # Traverse array directly for i in range(self.size()): if self.val(i) is not None: self.res.append(self.val(i)) @@ -203,32 +199,32 @@ The following code implements a binary tree based on array representation, inclu """Depth-first traversal""" if self.val(i) is None: return - # Pre-order traversal + # Preorder traversal if order == "pre": self.res.append(self.val(i)) self.dfs(self.left(i), order) - # In-order traversal + # Inorder traversal if order == "in": self.res.append(self.val(i)) self.dfs(self.right(i), order) - # Post-order traversal + # Postorder traversal if order == "post": self.res.append(self.val(i)) def pre_order(self) -> list[int]: - """Pre-order traversal""" + """Preorder traversal""" self.res = [] self.dfs(0, order="pre") return self.res def in_order(self) -> list[int]: - """In-order traversal""" + """Inorder traversal""" self.res = [] self.dfs(0, order="in") return self.res def post_order(self) -> list[int]: - """Post-order traversal""" + """Postorder traversal""" self.res = [] self.dfs(0, order="post") return self.res @@ -237,7 +233,7 @@ The following code implements a binary tree based on array representation, inclu === "C++" ```cpp title="array_binary_tree.cpp" - /* Array-based binary tree class */ + /* Binary tree class represented by array */ class ArrayBinaryTree { public: /* Constructor */ @@ -250,25 +246,25 @@ The following code implements a binary tree based on array representation, inclu return tree.size(); } - /* Get the value of the node at index i */ + /* Get value of node at index i */ int val(int i) { - // If index is out of bounds, return INT_MAX, representing a null + // Return INT_MAX if index out of bounds, representing empty position if (i < 0 || i >= size()) return INT_MAX; return tree[i]; } - /* Get the index of the left child of the node at index i */ + /* Get index of left child node of node at index i */ int left(int i) { return 2 * i + 1; } - /* Get the index of the right child of the node at index i */ + /* Get index of right child node of node at index i */ int right(int i) { return 2 * i + 2; } - /* Get the index of the parent of the node at index i */ + /* Get index of parent node of node at index i */ int parent(int i) { return (i - 1) / 2; } @@ -276,7 +272,7 @@ The following code implements a binary tree based on array representation, inclu /* Level-order traversal */ vector levelOrder() { vector res; - // Traverse array + // Traverse array directly for (int i = 0; i < size(); i++) { if (val(i) != INT_MAX) res.push_back(val(i)); @@ -284,21 +280,21 @@ The following code implements a binary tree based on array representation, inclu return res; } - /* Pre-order traversal */ + /* Preorder traversal */ vector preOrder() { vector res; dfs(0, "pre", res); return res; } - /* In-order traversal */ + /* Inorder traversal */ vector inOrder() { vector res; dfs(0, "in", res); return res; } - /* Post-order traversal */ + /* Postorder traversal */ vector postOrder() { vector res; dfs(0, "post", res); @@ -310,18 +306,18 @@ The following code implements a binary tree based on array representation, inclu /* Depth-first traversal */ void dfs(int i, string order, vector &res) { - // If it is an empty spot, return + // If empty position, return if (val(i) == INT_MAX) return; - // Pre-order traversal + // Preorder traversal if (order == "pre") res.push_back(val(i)); dfs(left(i), order, res); - // In-order traversal + // Inorder traversal if (order == "in") res.push_back(val(i)); dfs(right(i), order, res); - // Post-order traversal + // Postorder traversal if (order == "post") res.push_back(val(i)); } @@ -331,7 +327,7 @@ The following code implements a binary tree based on array representation, inclu === "Java" ```java title="array_binary_tree.java" - /* Array-based binary tree class */ + /* Binary tree class represented by array */ class ArrayBinaryTree { private List tree; @@ -345,25 +341,25 @@ The following code implements a binary tree based on array representation, inclu return tree.size(); } - /* Get the value of the node at index i */ + /* Get value of node at index i */ public Integer val(int i) { - // If the index is out of bounds, return null, representing an empty spot + // If index out of bounds, return null to represent empty position if (i < 0 || i >= size()) return null; return tree.get(i); } - /* Get the index of the left child of the node at index i */ + /* Get index of left child node of node at index i */ public Integer left(int i) { return 2 * i + 1; } - /* Get the index of the right child of the node at index i */ + /* Get index of right child node of node at index i */ public Integer right(int i) { return 2 * i + 2; } - /* Get the index of the parent of the node at index i */ + /* Get index of parent node of node at index i */ public Integer parent(int i) { return (i - 1) / 2; } @@ -371,7 +367,7 @@ The following code implements a binary tree based on array representation, inclu /* Level-order traversal */ public List levelOrder() { List res = new ArrayList<>(); - // Traverse array + // Traverse array directly for (int i = 0; i < size(); i++) { if (val(i) != null) res.add(val(i)); @@ -381,37 +377,37 @@ The following code implements a binary tree based on array representation, inclu /* Depth-first traversal */ private void dfs(Integer i, String order, List res) { - // If it is an empty spot, return + // If empty position, return if (val(i) == null) return; - // Pre-order traversal + // Preorder traversal if ("pre".equals(order)) res.add(val(i)); dfs(left(i), order, res); - // In-order traversal + // Inorder traversal if ("in".equals(order)) res.add(val(i)); dfs(right(i), order, res); - // Post-order traversal + // Postorder traversal if ("post".equals(order)) res.add(val(i)); } - /* Pre-order traversal */ + /* Preorder traversal */ public List preOrder() { List res = new ArrayList<>(); dfs(0, "pre", res); return res; } - /* In-order traversal */ + /* Inorder traversal */ public List inOrder() { List res = new ArrayList<>(); dfs(0, "in", res); return res; } - /* Post-order traversal */ + /* Postorder traversal */ public List postOrder() { List res = new ArrayList<>(); dfs(0, "post", res); @@ -423,79 +419,925 @@ The following code implements a binary tree based on array representation, inclu === "C#" ```csharp title="array_binary_tree.cs" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree(List arr) { + List tree = new(arr); + + /* List capacity */ + public int Size() { + return tree.Count; + } + + /* Get value of node at index i */ + public int? Val(int i) { + // If index out of bounds, return null to represent empty position + if (i < 0 || i >= Size()) + return null; + return tree[i]; + } + + /* Get index of left child node of node at index i */ + public int Left(int i) { + return 2 * i + 1; + } + + /* Get index of right child node of node at index i */ + public int Right(int i) { + return 2 * i + 2; + } + + /* Get index of parent node of node at index i */ + public int Parent(int i) { + return (i - 1) / 2; + } + + /* Level-order traversal */ + public List LevelOrder() { + List res = []; + // Traverse array directly + for (int i = 0; i < Size(); i++) { + if (Val(i).HasValue) + res.Add(Val(i)!.Value); + } + return res; + } + + /* Depth-first traversal */ + void DFS(int i, string order, List res) { + // If empty position, return + if (!Val(i).HasValue) + return; + // Preorder traversal + if (order == "pre") + res.Add(Val(i)!.Value); + DFS(Left(i), order, res); + // Inorder traversal + if (order == "in") + res.Add(Val(i)!.Value); + DFS(Right(i), order, res); + // Postorder traversal + if (order == "post") + res.Add(Val(i)!.Value); + } + + /* Preorder traversal */ + public List PreOrder() { + List res = []; + DFS(0, "pre", res); + return res; + } + + /* Inorder traversal */ + public List InOrder() { + List res = []; + DFS(0, "in", res); + return res; + } + + /* Postorder traversal */ + public List PostOrder() { + List res = []; + DFS(0, "post", res); + return res; + } + } ``` === "Go" ```go title="array_binary_tree.go" - [class]{arrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + type arrayBinaryTree struct { + tree []any + } + + /* Constructor */ + func newArrayBinaryTree(arr []any) *arrayBinaryTree { + return &arrayBinaryTree{ + tree: arr, + } + } + + /* List capacity */ + func (abt *arrayBinaryTree) size() int { + return len(abt.tree) + } + + /* Get value of node at index i */ + func (abt *arrayBinaryTree) val(i int) any { + // If index out of bounds, return null to represent empty position + if i < 0 || i >= abt.size() { + return nil + } + return abt.tree[i] + } + + /* Get index of left child node of node at index i */ + func (abt *arrayBinaryTree) left(i int) int { + return 2*i + 1 + } + + /* Get index of right child node of node at index i */ + func (abt *arrayBinaryTree) right(i int) int { + return 2*i + 2 + } + + /* Get index of parent node of node at index i */ + func (abt *arrayBinaryTree) parent(i int) int { + return (i - 1) / 2 + } + + /* Level-order traversal */ + func (abt *arrayBinaryTree) levelOrder() []any { + var res []any + // Traverse array directly + for i := 0; i < abt.size(); i++ { + if abt.val(i) != nil { + res = append(res, abt.val(i)) + } + } + return res + } + + /* Depth-first traversal */ + func (abt *arrayBinaryTree) dfs(i int, order string, res *[]any) { + // If empty position, return + if abt.val(i) == nil { + return + } + // Preorder traversal + if order == "pre" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.left(i), order, res) + // Inorder traversal + if order == "in" { + *res = append(*res, abt.val(i)) + } + abt.dfs(abt.right(i), order, res) + // Postorder traversal + if order == "post" { + *res = append(*res, abt.val(i)) + } + } + + /* Preorder traversal */ + func (abt *arrayBinaryTree) preOrder() []any { + var res []any + abt.dfs(0, "pre", &res) + return res + } + + /* Inorder traversal */ + func (abt *arrayBinaryTree) inOrder() []any { + var res []any + abt.dfs(0, "in", &res) + return res + } + + /* Postorder traversal */ + func (abt *arrayBinaryTree) postOrder() []any { + var res []any + abt.dfs(0, "post", &res) + return res + } ``` === "Swift" ```swift title="array_binary_tree.swift" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree { + private var tree: [Int?] + + /* Constructor */ + init(arr: [Int?]) { + tree = arr + } + + /* List capacity */ + func size() -> Int { + tree.count + } + + /* Get value of node at index i */ + func val(i: Int) -> Int? { + // If index out of bounds, return null to represent empty position + if i < 0 || i >= size() { + return nil + } + return tree[i] + } + + /* Get index of left child node of node at index i */ + func left(i: Int) -> Int { + 2 * i + 1 + } + + /* Get index of right child node of node at index i */ + func right(i: Int) -> Int { + 2 * i + 2 + } + + /* Get index of parent node of node at index i */ + func parent(i: Int) -> Int { + (i - 1) / 2 + } + + /* Level-order traversal */ + func levelOrder() -> [Int] { + var res: [Int] = [] + // Traverse array directly + for i in 0 ..< size() { + if let val = val(i: i) { + res.append(val) + } + } + return res + } + + /* Depth-first traversal */ + private func dfs(i: Int, order: String, res: inout [Int]) { + // If empty position, return + guard let val = val(i: i) else { + return + } + // Preorder traversal + if order == "pre" { + res.append(val) + } + dfs(i: left(i: i), order: order, res: &res) + // Inorder traversal + if order == "in" { + res.append(val) + } + dfs(i: right(i: i), order: order, res: &res) + // Postorder traversal + if order == "post" { + res.append(val) + } + } + + /* Preorder traversal */ + func preOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "pre", res: &res) + return res + } + + /* Inorder traversal */ + func inOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "in", res: &res) + return res + } + + /* Postorder traversal */ + func postOrder() -> [Int] { + var res: [Int] = [] + dfs(i: 0, order: "post", res: &res) + return res + } + } ``` === "JS" ```javascript title="array_binary_tree.js" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree { + #tree; + + /* Constructor */ + constructor(arr) { + this.#tree = arr; + } + + /* List capacity */ + size() { + return this.#tree.length; + } + + /* Get value of node at index i */ + val(i) { + // If index out of bounds, return null to represent empty position + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* Get index of left child node of node at index i */ + left(i) { + return 2 * i + 1; + } + + /* Get index of right child node of node at index i */ + right(i) { + return 2 * i + 2; + } + + /* Get index of parent node of node at index i */ + parent(i) { + return Math.floor((i - 1) / 2); // Floor division + } + + /* Level-order traversal */ + levelOrder() { + let res = []; + // Traverse array directly + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* Depth-first traversal */ + #dfs(i, order, res) { + // If empty position, return + if (this.val(i) === null) return; + // Preorder traversal + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // Inorder traversal + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // Postorder traversal + if (order === 'post') res.push(this.val(i)); + } + + /* Preorder traversal */ + preOrder() { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* Inorder traversal */ + inOrder() { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* Postorder traversal */ + postOrder() { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "TS" ```typescript title="array_binary_tree.ts" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree { + #tree: (number | null)[]; + + /* Constructor */ + constructor(arr: (number | null)[]) { + this.#tree = arr; + } + + /* List capacity */ + size(): number { + return this.#tree.length; + } + + /* Get value of node at index i */ + val(i: number): number | null { + // If index out of bounds, return null to represent empty position + if (i < 0 || i >= this.size()) return null; + return this.#tree[i]; + } + + /* Get index of left child node of node at index i */ + left(i: number): number { + return 2 * i + 1; + } + + /* Get index of right child node of node at index i */ + right(i: number): number { + return 2 * i + 2; + } + + /* Get index of parent node of node at index i */ + parent(i: number): number { + return Math.floor((i - 1) / 2); // Floor division + } + + /* Level-order traversal */ + levelOrder(): number[] { + let res = []; + // Traverse array directly + for (let i = 0; i < this.size(); i++) { + if (this.val(i) !== null) res.push(this.val(i)); + } + return res; + } + + /* Depth-first traversal */ + #dfs(i: number, order: Order, res: (number | null)[]): void { + // If empty position, return + if (this.val(i) === null) return; + // Preorder traversal + if (order === 'pre') res.push(this.val(i)); + this.#dfs(this.left(i), order, res); + // Inorder traversal + if (order === 'in') res.push(this.val(i)); + this.#dfs(this.right(i), order, res); + // Postorder traversal + if (order === 'post') res.push(this.val(i)); + } + + /* Preorder traversal */ + preOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'pre', res); + return res; + } + + /* Inorder traversal */ + inOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'in', res); + return res; + } + + /* Postorder traversal */ + postOrder(): (number | null)[] { + const res = []; + this.#dfs(0, 'post', res); + return res; + } + } ``` === "Dart" ```dart title="array_binary_tree.dart" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree { + late List _tree; + + /* Constructor */ + ArrayBinaryTree(this._tree); + + /* List capacity */ + int size() { + return _tree.length; + } + + /* Get value of node at index i */ + int? val(int i) { + // If index out of bounds, return null to represent empty position + if (i < 0 || i >= size()) { + return null; + } + return _tree[i]; + } + + /* Get index of left child node of node at index i */ + int? left(int i) { + return 2 * i + 1; + } + + /* Get index of right child node of node at index i */ + int? right(int i) { + return 2 * i + 2; + } + + /* Get index of parent node of node at index i */ + int? parent(int i) { + return (i - 1) ~/ 2; + } + + /* Level-order traversal */ + List levelOrder() { + List res = []; + for (int i = 0; i < size(); i++) { + if (val(i) != null) { + res.add(val(i)!); + } + } + return res; + } + + /* Depth-first traversal */ + void dfs(int i, String order, List res) { + // If empty position, return + if (val(i) == null) { + return; + } + // Preorder traversal + if (order == 'pre') { + res.add(val(i)); + } + dfs(left(i)!, order, res); + // Inorder traversal + if (order == 'in') { + res.add(val(i)); + } + dfs(right(i)!, order, res); + // Postorder traversal + if (order == 'post') { + res.add(val(i)); + } + } + + /* Preorder traversal */ + List preOrder() { + List res = []; + dfs(0, 'pre', res); + return res; + } + + /* Inorder traversal */ + List inOrder() { + List res = []; + dfs(0, 'in', res); + return res; + } + + /* Postorder traversal */ + List postOrder() { + List res = []; + dfs(0, 'post', res); + return res; + } + } ``` === "Rust" ```rust title="array_binary_tree.rs" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + struct ArrayBinaryTree { + tree: Vec>, + } + + impl ArrayBinaryTree { + /* Constructor */ + fn new(arr: Vec>) -> Self { + Self { tree: arr } + } + + /* List capacity */ + fn size(&self) -> i32 { + self.tree.len() as i32 + } + + /* Get value of node at index i */ + fn val(&self, i: i32) -> Option { + // If index is out of bounds, return None, representing empty position + if i < 0 || i >= self.size() { + None + } else { + self.tree[i as usize] + } + } + + /* Get index of left child node of node at index i */ + fn left(&self, i: i32) -> i32 { + 2 * i + 1 + } + + /* Get index of right child node of node at index i */ + fn right(&self, i: i32) -> i32 { + 2 * i + 2 + } + + /* Get index of parent node of node at index i */ + fn parent(&self, i: i32) -> i32 { + (i - 1) / 2 + } + + /* Level-order traversal */ + fn level_order(&self) -> Vec { + self.tree.iter().filter_map(|&x| x).collect() + } + + /* Depth-first traversal */ + fn dfs(&self, i: i32, order: &'static str, res: &mut Vec) { + if self.val(i).is_none() { + return; + } + let val = self.val(i).unwrap(); + // Preorder traversal + if order == "pre" { + res.push(val); + } + self.dfs(self.left(i), order, res); + // Inorder traversal + if order == "in" { + res.push(val); + } + self.dfs(self.right(i), order, res); + // Postorder traversal + if order == "post" { + res.push(val); + } + } + + /* Preorder traversal */ + fn pre_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "pre", &mut res); + res + } + + /* Inorder traversal */ + fn in_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "in", &mut res); + res + } + + /* Postorder traversal */ + fn post_order(&self) -> Vec { + let mut res = vec![]; + self.dfs(0, "post", &mut res); + res + } + } ``` === "C" ```c title="array_binary_tree.c" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree structure in array representation */ + typedef struct { + int *tree; + int size; + } ArrayBinaryTree; + + /* Constructor */ + ArrayBinaryTree *newArrayBinaryTree(int *arr, int arrSize) { + ArrayBinaryTree *abt = (ArrayBinaryTree *)malloc(sizeof(ArrayBinaryTree)); + abt->tree = malloc(sizeof(int) * arrSize); + memcpy(abt->tree, arr, sizeof(int) * arrSize); + abt->size = arrSize; + return abt; + } + + /* Destructor */ + void delArrayBinaryTree(ArrayBinaryTree *abt) { + free(abt->tree); + free(abt); + } + + /* List capacity */ + int size(ArrayBinaryTree *abt) { + return abt->size; + } + + /* Get value of node at index i */ + int val(ArrayBinaryTree *abt, int i) { + // Return INT_MAX if index out of bounds, representing empty position + if (i < 0 || i >= size(abt)) + return INT_MAX; + return abt->tree[i]; + } + + /* Level-order traversal */ + int *levelOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + // Traverse array directly + for (int i = 0; i < size(abt); i++) { + if (val(abt, i) != INT_MAX) + res[index++] = val(abt, i); + } + *returnSize = index; + return res; + } + + /* Depth-first traversal */ + void dfs(ArrayBinaryTree *abt, int i, char *order, int *res, int *index) { + // If empty position, return + if (val(abt, i) == INT_MAX) + return; + // Preorder traversal + if (strcmp(order, "pre") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, left(i), order, res, index); + // Inorder traversal + if (strcmp(order, "in") == 0) + res[(*index)++] = val(abt, i); + dfs(abt, right(i), order, res, index); + // Postorder traversal + if (strcmp(order, "post") == 0) + res[(*index)++] = val(abt, i); + } + + /* Preorder traversal */ + int *preOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "pre", res, &index); + *returnSize = index; + return res; + } + + /* Inorder traversal */ + int *inOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "in", res, &index); + *returnSize = index; + return res; + } + + /* Postorder traversal */ + int *postOrder(ArrayBinaryTree *abt, int *returnSize) { + int *res = (int *)malloc(sizeof(int) * size(abt)); + int index = 0; + dfs(abt, 0, "post", res, &index); + *returnSize = index; + return res; + } ``` === "Kotlin" ```kotlin title="array_binary_tree.kt" - [class]{ArrayBinaryTree}-[func]{} + /* Binary tree class represented by array */ + class ArrayBinaryTree(val tree: MutableList) { + /* List capacity */ + fun size(): Int { + return tree.size + } + + /* Get value of node at index i */ + fun _val(i: Int): Int? { + // If index out of bounds, return null to represent empty position + if (i < 0 || i >= size()) return null + return tree[i] + } + + /* Get index of left child node of node at index i */ + fun left(i: Int): Int { + return 2 * i + 1 + } + + /* Get index of right child node of node at index i */ + fun right(i: Int): Int { + return 2 * i + 2 + } + + /* Get index of parent node of node at index i */ + fun parent(i: Int): Int { + return (i - 1) / 2 + } + + /* Level-order traversal */ + fun levelOrder(): MutableList { + val res = mutableListOf() + // Traverse array directly + for (i in 0..) { + // If empty position, return + if (_val(i) == null) + return + // Preorder traversal + if ("pre" == order) + res.add(_val(i)) + dfs(left(i), order, res) + // Inorder traversal + if ("in" == order) + res.add(_val(i)) + dfs(right(i), order, res) + // Postorder traversal + if ("post" == order) + res.add(_val(i)) + } + + /* Preorder traversal */ + fun preOrder(): MutableList { + val res = mutableListOf() + dfs(0, "pre", res) + return res + } + + /* Inorder traversal */ + fun inOrder(): MutableList { + val res = mutableListOf() + dfs(0, "in", res) + return res + } + + /* Postorder traversal */ + fun postOrder(): MutableList { + val res = mutableListOf() + dfs(0, "post", res) + return res + } + } ``` === "Ruby" ```ruby title="array_binary_tree.rb" - [class]{ArrayBinaryTree}-[func]{} + ### Array representation of binary tree class ### + class ArrayBinaryTree + ### Constructor ### + def initialize(arr) + @tree = arr.to_a + end + + ### List capacity ### + def size + @tree.length + end + + ### Get value of node at index i ### + def val(i) + # Return nil if index out of bounds, representing empty position + return if i < 0 || i >= size + + @tree[i] + end + + ### Get left child index of node at index i ### + def left(i) + 2 * i + 1 + end + + ### Get right child index of node at index i ### + def right(i) + 2 * i + 2 + end + + ### Get parent node index of node at index i ### + def parent(i) + (i - 1) / 2 + end + + ### Level-order traversal ### + def level_order + @res = [] + + # Traverse array directly + for i in 0...size + @res << val(i) unless val(i).nil? + end + + @res + end + + ### Depth-first traversal ### + def dfs(i, order) + return if val(i).nil? + # Preorder traversal + @res << val(i) if order == :pre + dfs(left(i), order) + # Inorder traversal + @res << val(i) if order == :in + dfs(right(i), order) + # Postorder traversal + @res << val(i) if order == :post + end + + ### Pre-order traversal ### + def pre_order + @res = [] + dfs(0, :pre) + @res + end + + ### In-order traversal ### + def in_order + @res = [] + dfs(0, :in) + @res + end + + ### Post-order traversal ### + def post_order + @res = [] + dfs(0, :post) + @res + end + end ``` -=== "Zig" - - ```zig title="array_binary_tree.zig" - [class]{ArrayBinaryTree}-[func]{} - ``` - -## 7.3.3   Advantages and limitations +## 7.3.3   Advantages and Limitations The array representation of binary trees has the following advantages: -- Arrays are stored in contiguous memory spaces, which is cache-friendly and allows for faster access and traversal. +- Arrays are stored in contiguous memory space, which is cache-friendly, allowing faster access and traversal. - It does not require storing pointers, which saves space. - It allows random access to nodes. However, the array representation also has some limitations: - Array storage requires contiguous memory space, so it is not suitable for storing trees with a large amount of data. -- Adding or deleting nodes requires array insertion and deletion operations, which are less efficient. +- Adding or removing nodes requires array insertion and deletion operations, which have lower efficiency. - When there are many `None` values in the binary tree, the proportion of node data contained in the array is low, leading to lower space utilization. diff --git a/en/docs/chapter_tree/avl_tree.md b/en/docs/chapter_tree/avl_tree.md index f1ec95dbc..7c13c52fc 100644 --- a/en/docs/chapter_tree/avl_tree.md +++ b/en/docs/chapter_tree/avl_tree.md @@ -2,9 +2,9 @@ comments: true --- -# 7.5   AVL tree * +# 7.5   Avl Tree * -In the "Binary Search Tree" section, we mentioned that after multiple insertions and removals, a binary search tree might degrade to a linked list. In such cases, the time complexity of all operations degrades from $O(\log n)$ to $O(n)$. +In the "Binary Search Tree" section, we mentioned that after multiple insertion and removal operations, a binary search tree may degenerate into a linked list. In this case, the time complexity of all operations degrades from $O(\log n)$ to $O(n)$. As shown in Figure 7-24, after two node removal operations, this binary search tree will degrade into a linked list. @@ -18,13 +18,13 @@ For example, in the perfect binary tree shown in Figure 7-25, after inserting tw

Figure 7-25   Degradation of an AVL tree after inserting nodes

-In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL Tree in their paper "An algorithm for the organization of information". The paper detailed a series of operations to ensure that after continuously adding and removing nodes, the AVL tree would not degrade, thus maintaining the time complexity of various operations at $O(\log n)$ level. In other words, in scenarios where frequent additions, removals, searches, and modifications are needed, the AVL tree can always maintain efficient data operation performance, which has great application value. +In 1962, G. M. Adelson-Velsky and E. M. Landis proposed the AVL tree in their paper "An algorithm for the organization of information". The paper described in detail a series of operations ensuring that after continuously adding and removing nodes, the AVL tree does not degenerate, thus keeping the time complexity of various operations at the $O(\log n)$ level. In other words, in scenarios requiring frequent insertions, deletions, searches, and modifications, the AVL tree can always maintain efficient data operation performance, making it very valuable in applications. -## 7.5.1   Common terminology in AVL trees +## 7.5.1   Common Terminology in Avl Trees -An AVL tree is both a binary search tree and a balanced binary tree, satisfying all properties of these two types of binary trees, hence it is a balanced binary search tree. +An AVL tree is both a binary search tree and a balanced binary tree, simultaneously satisfying all the properties of these two types of binary trees, hence it is a balanced binary search tree. -### 1.   Node height +### 1.   Node Height Since the operations related to AVL trees require obtaining node heights, we need to add a `height` variable to the node class: @@ -188,7 +188,7 @@ Since the operations related to AVL trees require obtaining node heights, we nee ```c title="" /* AVL tree node */ - TreeNode struct TreeNode { + typedef struct TreeNode { int val; int height; struct TreeNode *left; @@ -222,13 +222,18 @@ Since the operations related to AVL trees require obtaining node heights, we nee === "Ruby" ```ruby title="" + ### AVL tree node class ### + class TreeNode + attr_accessor :val # Node value + attr_accessor :height # Node height + attr_accessor :left # Left child reference + attr_accessor :right # Right child reference - ``` - -=== "Zig" - - ```zig title="" - + def initialize(val) + @val = val + @height = 0 + end + end ``` The "node height" refers to the distance from that node to its farthest leaf node, i.e., the number of "edges" passed. It is important to note that the height of a leaf node is $0$, and the height of a null node is $-1$. We will create two utility functions for getting and updating the height of a node: @@ -284,94 +289,195 @@ The "node height" refers to the distance from that node to its farthest leaf nod === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Height} + /* Get node height */ + int Height(TreeNode? node) { + // Empty node height is -1, leaf node height is 0 + return node == null ? -1 : node.height; + } - [class]{AVLTree}-[func]{UpdateHeight} + /* Update node height */ + void UpdateHeight(TreeNode node) { + // Node height equals the height of the tallest subtree + 1 + node.height = Math.Max(Height(node.left), Height(node.right)) + 1; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{height} + /* Get node height */ + func (t *aVLTree) height(node *TreeNode) int { + // Empty node height is -1, leaf node height is 0 + if node != nil { + return node.Height + } + return -1 + } - [class]{aVLTree}-[func]{updateHeight} + /* Update node height */ + func (t *aVLTree) updateHeight(node *TreeNode) { + lh := t.height(node.Left) + rh := t.height(node.Right) + // Node height equals the height of the tallest subtree + 1 + if lh > rh { + node.Height = lh + 1 + } else { + node.Height = rh + 1 + } + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{height} + /* Get node height */ + func height(node: TreeNode?) -> Int { + // Empty node height is -1, leaf node height is 0 + node?.height ?? -1 + } - [class]{AVLTree}-[func]{updateHeight} + /* Update node height */ + func updateHeight(node: TreeNode?) { + // Node height equals the height of the tallest subtree + 1 + node?.height = max(height(node: node?.left), height(node: node?.right)) + 1 + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{height} + /* Get node height */ + height(node) { + // Empty node height is -1, leaf node height is 0 + return node === null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* Update node height */ + #updateHeight(node) { + // Node height equals the height of the tallest subtree + 1 + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{height} + /* Get node height */ + height(node: TreeNode): number { + // Empty node height is -1, leaf node height is 0 + return node === null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* Update node height */ + updateHeight(node: TreeNode): void { + // Node height equals the height of the tallest subtree + 1 + node.height = + Math.max(this.height(node.left), this.height(node.right)) + 1; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{height} + /* Get node height */ + int height(TreeNode? node) { + // Empty node height is -1, leaf node height is 0 + return node == null ? -1 : node.height; + } - [class]{AVLTree}-[func]{updateHeight} + /* Update node height */ + void updateHeight(TreeNode? node) { + // Node height equals the height of the tallest subtree + 1 + node!.height = max(height(node.left), height(node.right)) + 1; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{height} + /* Get node height */ + fn height(node: OptionTreeNodeRc) -> i32 { + // Empty node height is -1, leaf node height is 0 + match node { + Some(node) => node.borrow().height, + None => -1, + } + } - [class]{AVLTree}-[func]{update_height} + /* Update node height */ + fn update_height(node: OptionTreeNodeRc) { + if let Some(node) = node { + let left = node.borrow().left.clone(); + let right = node.borrow().right.clone(); + // Node height equals the height of the tallest subtree + 1 + node.borrow_mut().height = std::cmp::max(Self::height(left), Self::height(right)) + 1; + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{height} + /* Get node height */ + int height(TreeNode *node) { + // Empty node height is -1, leaf node height is 0 + if (node != NULL) { + return node->height; + } + return -1; + } - [class]{}-[func]{updateHeight} + /* Update node height */ + void updateHeight(TreeNode *node) { + int lh = height(node->left); + int rh = height(node->right); + // Node height equals the height of the tallest subtree + 1 + if (lh > rh) { + node->height = lh + 1; + } else { + node->height = rh + 1; + } + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{height} + /* Get node height */ + fun height(node: TreeNode?): Int { + // Empty node height is -1, leaf node height is 0 + return node?.height ?: -1 + } - [class]{AVLTree}-[func]{updateHeight} + /* Update node height */ + fun updateHeight(node: TreeNode?) { + // Node height equals the height of the tallest subtree + 1 + node?.height = max(height(node?.left), height(node?.right)) + 1 + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{height} + ### Get node height ### + def height(node) + # Empty node height is -1, leaf node height is 0 + return node.height unless node.nil? - [class]{AVLTree}-[func]{update_height} + -1 + end + + ### Update node height ### + def update_height(node) + # Node height equals the height of the tallest subtree + 1 + node.height = [height(node.left), height(node.right)].max + 1 + end ``` -=== "Zig" +### 2.   Node Balance Factor - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{height} - - [class]{AVLTree}-[func]{updateHeight} - ``` - -### 2.   Node balance factor - -The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, with the balance factor of a null node defined as $0$. We will also encapsulate the functionality of obtaining the node balance factor into a function for easy use later on: +The balance factor of a node is defined as the height of the node's left subtree minus the height of its right subtree, and the balance factor of a null node is defined as $0$. We also encapsulate the function to obtain the node's balance factor for convenient subsequent use: === "Python" @@ -414,82 +520,145 @@ The balance factor of a node is defined as the height of the node's left === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{BalanceFactor} + /* Get balance factor */ + int BalanceFactor(TreeNode? node) { + // Empty node balance factor is 0 + if (node == null) return 0; + // Node balance factor = left subtree height - right subtree height + return Height(node.left) - Height(node.right); + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{balanceFactor} + /* Get balance factor */ + func (t *aVLTree) balanceFactor(node *TreeNode) int { + // Empty node balance factor is 0 + if node == nil { + return 0 + } + // Node balance factor = left subtree height - right subtree height + return t.height(node.Left) - t.height(node.Right) + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{balanceFactor} + /* Get balance factor */ + func balanceFactor(node: TreeNode?) -> Int { + // Empty node balance factor is 0 + guard let node = node else { return 0 } + // Node balance factor = left subtree height - right subtree height + return height(node: node.left) - height(node: node.right) + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{balanceFactor} + /* Get balance factor */ + balanceFactor(node) { + // Empty node balance factor is 0 + if (node === null) return 0; + // Node balance factor = left subtree height - right subtree height + return this.height(node.left) - this.height(node.right); + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{balanceFactor} + /* Get balance factor */ + balanceFactor(node: TreeNode): number { + // Empty node balance factor is 0 + if (node === null) return 0; + // Node balance factor = left subtree height - right subtree height + return this.height(node.left) - this.height(node.right); + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{balanceFactor} + /* Get balance factor */ + int balanceFactor(TreeNode? node) { + // Empty node balance factor is 0 + if (node == null) return 0; + // Node balance factor = left subtree height - right subtree height + return height(node.left) - height(node.right); + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{balance_factor} + /* Get balance factor */ + fn balance_factor(node: OptionTreeNodeRc) -> i32 { + match node { + // Empty node balance factor is 0 + None => 0, + // Node balance factor = left subtree height - right subtree height + Some(node) => { + Self::height(node.borrow().left.clone()) - Self::height(node.borrow().right.clone()) + } + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{balanceFactor} + /* Get balance factor */ + int balanceFactor(TreeNode *node) { + // Empty node balance factor is 0 + if (node == NULL) { + return 0; + } + // Node balance factor = left subtree height - right subtree height + return height(node->left) - height(node->right); + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{balanceFactor} + /* Get balance factor */ + fun balanceFactor(node: TreeNode?): Int { + // Empty node balance factor is 0 + if (node == null) return 0 + // Node balance factor = left subtree height - right subtree height + return height(node.left) - height(node.right) + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{balance_factor} - ``` + ### Get balance factor ### + def balance_factor(node) + # Empty node balance factor is 0 + return 0 if node.nil? -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{balanceFactor} + # Node balance factor = left subtree height - right subtree height + height(node.left) - height(node.right) + end ``` !!! tip Let the balance factor be $f$, then the balance factor of any node in an AVL tree satisfies $-1 \le f \le 1$. -## 7.5.2   Rotations in AVL trees +## 7.5.2   Rotations in Avl Trees -The characteristic feature of an AVL tree is the "rotation" operation, which can restore balance to an unbalanced node without affecting the in-order traversal sequence of the binary tree. In other words, **the rotation operation can maintain the property of a "binary search tree" while also turning the tree back into a "balanced binary tree"**. +The characteristic of AVL trees lies in the "rotation" operation, which can restore balance to unbalanced nodes without affecting the inorder traversal sequence of the binary tree. In other words, **rotation operations can both maintain the property of a "binary search tree" and make the tree return to a "balanced binary tree"**. -We call nodes with an absolute balance factor $> 1$ "unbalanced nodes". Depending on the type of imbalance, there are four kinds of rotations: right rotation, left rotation, right-left rotation, and left-right rotation. Below, we detail these rotation operations. +We call nodes with a balance factor absolute value $> 1$ "unbalanced nodes". Depending on the imbalance situation, rotation operations are divided into four types: right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. Below we describe these rotation operations in detail. -### 1.   Right rotation +### 1.   Right Rotation -As shown in Figure 7-26, the first unbalanced node from the bottom up in the binary tree is "node 3". Focusing on the subtree with this unbalanced node as the root, denoted as `node`, and its left child as `child`, perform a "right rotation". After the right rotation, the subtree is balanced again while still maintaining the properties of a binary search tree. +As shown in Figure 7-26, the value below the node is the balance factor. From bottom to top, the first unbalanced node in the binary tree is "node 3". We focus on the subtree with this unbalanced node as the root, denoting the node as `node` and its left child as `child`, and perform a "right rotation" operation. After the right rotation is completed, the subtree regains balance and still maintains the properties of a binary search tree. === "<1>" ![Steps of right rotation](avl_tree.assets/avltree_right_rotate_step1.png){ class="animation-figure" } @@ -520,13 +689,13 @@ As shown in Figure 7-27, when the `child` node has a right child (denoted as `gr """Right rotation operation""" child = node.left grand_child = child.right - # Rotate node to the right around child + # Using child as pivot, rotate node to the right child.right = node node.left = grand_child # Update node height self.update_height(node) self.update_height(child) - # Return the root of the subtree after rotation + # Return root node of subtree after rotation return child ``` @@ -537,13 +706,13 @@ As shown in Figure 7-27, when the `child` node has a right child (denoted as `gr TreeNode *rightRotate(TreeNode *node) { TreeNode *child = node->left; TreeNode *grandChild = child->right; - // Rotate node to the right around child + // Using child as pivot, rotate node to the right child->right = node; node->left = grandChild; // Update node height updateHeight(node); updateHeight(child); - // Return the root of the subtree after rotation + // Return root node of subtree after rotation return child; } ``` @@ -555,13 +724,13 @@ As shown in Figure 7-27, when the `child` node has a right child (denoted as `gr TreeNode rightRotate(TreeNode node) { TreeNode child = node.left; TreeNode grandChild = child.right; - // Rotate node to the right around child + // Using child as pivot, rotate node to the right child.right = node; node.left = grandChild; // Update node height updateHeight(node); updateHeight(child); - // Return the root of the subtree after rotation + // Return root node of subtree after rotation return child; } ``` @@ -569,70 +738,190 @@ As shown in Figure 7-27, when the `child` node has a right child (denoted as `gr === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{RightRotate} + /* Right rotation operation */ + TreeNode? RightRotate(TreeNode? node) { + TreeNode? child = node?.left; + TreeNode? grandChild = child?.right; + // Using child as pivot, rotate node to the right + child.right = node; + node.left = grandChild; + // Update node height + UpdateHeight(node); + UpdateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{rightRotate} + /* Right rotation operation */ + func (t *aVLTree) rightRotate(node *TreeNode) *TreeNode { + child := node.Left + grandChild := child.Right + // Using child as pivot, rotate node to the right + child.Right = node + node.Left = grandChild + // Update node height + t.updateHeight(node) + t.updateHeight(child) + // Return root node of subtree after rotation + return child + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{rightRotate} + /* Right rotation operation */ + func rightRotate(node: TreeNode?) -> TreeNode? { + let child = node?.left + let grandChild = child?.right + // Using child as pivot, rotate node to the right + child?.right = node + node?.left = grandChild + // Update node height + updateHeight(node: node) + updateHeight(node: child) + // Return root node of subtree after rotation + return child + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{rightRotate} + /* Right rotation operation */ + #rightRotate(node) { + const child = node.left; + const grandChild = child.right; + // Using child as pivot, rotate node to the right + child.right = node; + node.left = grandChild; + // Update node height + this.#updateHeight(node); + this.#updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{rightRotate} + /* Right rotation operation */ + rightRotate(node: TreeNode): TreeNode { + const child = node.left; + const grandChild = child.right; + // Using child as pivot, rotate node to the right + child.right = node; + node.left = grandChild; + // Update node height + this.updateHeight(node); + this.updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{rightRotate} + /* Right rotation operation */ + TreeNode? rightRotate(TreeNode? node) { + TreeNode? child = node!.left; + TreeNode? grandChild = child!.right; + // Using child as pivot, rotate node to the right + child.right = node; + node.left = grandChild; + // Update node height + updateHeight(node); + updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{right_rotate} + /* Right rotation operation */ + fn right_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().left.clone().unwrap(); + let grand_child = child.borrow().right.clone(); + // Using child as pivot, rotate node to the right + child.borrow_mut().right = Some(node.clone()); + node.borrow_mut().left = grand_child; + // Update node height + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // Return root node of subtree after rotation + Some(child) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{rightRotate} + /* Right rotation operation */ + TreeNode *rightRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->left; + grandChild = child->right; + // Using child as pivot, rotate node to the right + child->right = node; + node->left = grandChild; + // Update node height + updateHeight(node); + updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{rightRotate} + /* Right rotation operation */ + fun rightRotate(node: TreeNode?): TreeNode { + val child = node!!.left + val grandChild = child!!.right + // Using child as pivot, rotate node to the right + child.right = node + node.left = grandChild + // Update node height + updateHeight(node) + updateHeight(child) + // Return root node of subtree after rotation + return child + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{right_rotate} + ### Right rotation ### + def right_rotate(node) + child = node.left + grand_child = child.right + # Using child as pivot, rotate node to the right + child.right = node + node.left = grand_child + # Update node height + update_height(node) + update_height(child) + # Return root node of subtree after rotation + child + end ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{rightRotate} - ``` - -### 2.   Left rotation +### 2.   Left Rotation Correspondingly, if considering the "mirror" of the above unbalanced binary tree, the "left rotation" operation shown in Figure 7-28 needs to be performed. @@ -646,7 +935,7 @@ Similarly, as shown in Figure 7-29, when the `child` node has a left child (deno

Figure 7-29   Left rotation with grand_child

-It can be observed that **the right and left rotation operations are logically symmetrical, and they solve two symmetrical types of imbalance**. Based on symmetry, by replacing all `left` with `right`, and all `right` with `left` in the implementation code of right rotation, we can get the implementation code for left rotation: +It can be observed that **right rotation and left rotation operations are mirror symmetric in logic, and the two imbalance cases they solve are also symmetric**. Based on symmetry, we only need to replace all `left` in the right rotation implementation code with `right`, and all `right` with `left`, to obtain the left rotation implementation code: === "Python" @@ -655,13 +944,13 @@ It can be observed that **the right and left rotation operations are logically s """Left rotation operation""" child = node.right grand_child = child.left - # Rotate node to the left around child + # Using child as pivot, rotate node to the left child.left = node node.right = grand_child # Update node height self.update_height(node) self.update_height(child) - # Return the root of the subtree after rotation + # Return root node of subtree after rotation return child ``` @@ -672,13 +961,13 @@ It can be observed that **the right and left rotation operations are logically s TreeNode *leftRotate(TreeNode *node) { TreeNode *child = node->right; TreeNode *grandChild = child->left; - // Rotate node to the left around child + // Using child as pivot, rotate node to the left child->left = node; node->right = grandChild; // Update node height updateHeight(node); updateHeight(child); - // Return the root of the subtree after rotation + // Return root node of subtree after rotation return child; } ``` @@ -690,13 +979,13 @@ It can be observed that **the right and left rotation operations are logically s TreeNode leftRotate(TreeNode node) { TreeNode child = node.right; TreeNode grandChild = child.left; - // Rotate node to the left around child + // Using child as pivot, rotate node to the left child.left = node; node.right = grandChild; // Update node height updateHeight(node); updateHeight(child); - // Return the root of the subtree after rotation + // Return root node of subtree after rotation return child; } ``` @@ -704,116 +993,236 @@ It can be observed that **the right and left rotation operations are logically s === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{LeftRotate} + /* Left rotation operation */ + TreeNode? LeftRotate(TreeNode? node) { + TreeNode? child = node?.right; + TreeNode? grandChild = child?.left; + // Using child as pivot, rotate node to the left + child.left = node; + node.right = grandChild; + // Update node height + UpdateHeight(node); + UpdateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{leftRotate} + /* Left rotation operation */ + func (t *aVLTree) leftRotate(node *TreeNode) *TreeNode { + child := node.Right + grandChild := child.Left + // Using child as pivot, rotate node to the left + child.Left = node + node.Right = grandChild + // Update node height + t.updateHeight(node) + t.updateHeight(child) + // Return root node of subtree after rotation + return child + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{leftRotate} + /* Left rotation operation */ + func leftRotate(node: TreeNode?) -> TreeNode? { + let child = node?.right + let grandChild = child?.left + // Using child as pivot, rotate node to the left + child?.left = node + node?.right = grandChild + // Update node height + updateHeight(node: node) + updateHeight(node: child) + // Return root node of subtree after rotation + return child + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{leftRotate} + /* Left rotation operation */ + #leftRotate(node) { + const child = node.right; + const grandChild = child.left; + // Using child as pivot, rotate node to the left + child.left = node; + node.right = grandChild; + // Update node height + this.#updateHeight(node); + this.#updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{leftRotate} + /* Left rotation operation */ + leftRotate(node: TreeNode): TreeNode { + const child = node.right; + const grandChild = child.left; + // Using child as pivot, rotate node to the left + child.left = node; + node.right = grandChild; + // Update node height + this.updateHeight(node); + this.updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{leftRotate} + /* Left rotation operation */ + TreeNode? leftRotate(TreeNode? node) { + TreeNode? child = node!.right; + TreeNode? grandChild = child!.left; + // Using child as pivot, rotate node to the left + child.left = node; + node.right = grandChild; + // Update node height + updateHeight(node); + updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{left_rotate} + /* Left rotation operation */ + fn left_rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + match node { + Some(node) => { + let child = node.borrow().right.clone().unwrap(); + let grand_child = child.borrow().left.clone(); + // Using child as pivot, rotate node to the left + child.borrow_mut().left = Some(node.clone()); + node.borrow_mut().right = grand_child; + // Update node height + Self::update_height(Some(node)); + Self::update_height(Some(child.clone())); + // Return root node of subtree after rotation + Some(child) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{leftRotate} + /* Left rotation operation */ + TreeNode *leftRotate(TreeNode *node) { + TreeNode *child, *grandChild; + child = node->right; + grandChild = child->left; + // Using child as pivot, rotate node to the left + child->left = node; + node->right = grandChild; + // Update node height + updateHeight(node); + updateHeight(child); + // Return root node of subtree after rotation + return child; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{leftRotate} + /* Left rotation operation */ + fun leftRotate(node: TreeNode?): TreeNode { + val child = node!!.right + val grandChild = child!!.left + // Using child as pivot, rotate node to the left + child.left = node + node.right = grandChild + // Update node height + updateHeight(node) + updateHeight(child) + // Return root node of subtree after rotation + return child + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{left_rotate} + ### Left rotation ### + def left_rotate(node) + child = node.right + grand_child = child.left + # Using child as pivot, rotate node to the left + child.left = node + node.right = grand_child + # Update node height + update_height(node) + update_height(child) + # Return root node of subtree after rotation + child + end ``` -=== "Zig" +### 3.   Left Rotation Then Right Rotation - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{leftRotate} - ``` - -### 3.   Left-right rotation - -For the unbalanced node 3 shown in Figure 7-30, using either left or right rotation alone cannot restore balance to the subtree. In this case, a "left rotation" needs to be performed on `child` first, followed by a "right rotation" on `node`. +For the unbalanced node 3 in Figure 7-30, using either left rotation or right rotation alone cannot restore the subtree to balance. In this case, a "left rotation" needs to be performed on `child` first, followed by a "right rotation" on `node`. ![Left-right rotation](avl_tree.assets/avltree_left_right_rotate.png){ class="animation-figure" }

Figure 7-30   Left-right rotation

-### 4.   Right-left rotation +### 4.   Right Rotation Then Left Rotation -As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a "right rotation" needs to be performed on `child` first, followed by a "left rotation" on `node`. +As shown in Figure 7-31, for the mirror case of the above unbalanced binary tree, a "right rotation" needs to be performed on `child` first, then a "left rotation" on `node`. ![Right-left rotation](avl_tree.assets/avltree_right_left_rotate.png){ class="animation-figure" }

Figure 7-31   Right-left rotation

-### 5.   Choice of rotation +### 5.   Choice of Rotation -The four kinds of imbalances shown in Figure 7-32 correspond to the cases described above, respectively requiring right rotation, left-right rotation, right-left rotation, and left rotation. +The four imbalances shown in Figure 7-32 correspond one-to-one with the above cases, requiring right rotation, left rotation then right rotation, right rotation then left rotation, and left rotation operations respectively. ![The four rotation cases of AVL tree](avl_tree.assets/avltree_rotation_cases.png){ class="animation-figure" }

Figure 7-32   The four rotation cases of AVL tree

-As shown in Table 7-3, we determine which of the above cases an unbalanced node belongs to by judging the sign of the balance factor of the unbalanced node and its higher-side child's balance factor. +As shown in Table 7-3, we determine which case the unbalanced node belongs to by judging the signs of the balance factor of the unbalanced node and the balance factor of its taller-side child node.

Table 7-3   Conditions for Choosing Among the Four Rotation Cases

-| Balance factor of unbalanced node | Balance factor of child node | Rotation method to use | -| --------------------------------- | ---------------------------- | --------------------------------- | -| $> 1$ (Left-leaning tree) | $\geq 0$ | Right rotation | -| $> 1$ (Left-leaning tree) | $<0$ | Left rotation then right rotation | -| $< -1$ (Right-leaning tree) | $\leq 0$ | Left rotation | -| $< -1$ (Right-leaning tree) | $>0$ | Right rotation then left rotation | +| Balance factor of the unbalanced node | Balance factor of the child node | Rotation method to apply | +| -------------------------------------- | --------------------------------- | --------------------------------- | +| $> 1$ (left-leaning tree) | $\geq 0$ | Right rotation | +| $> 1$ (left-leaning tree) | $<0$ | Left rotation then right rotation | +| $< -1$ (right-leaning tree) | $\leq 0$ | Left rotation | +| $< -1$ (right-leaning tree) | $>0$ | Right rotation then left rotation |
-For convenience, we encapsulate the rotation operations into a function. **With this function, we can perform rotations on various kinds of imbalances, restoring balance to unbalanced nodes**. The code is as follows: +For ease of use, we encapsulate the rotation operations into a function. **With this function, we can perform rotations for various imbalance situations, restoring balance to unbalanced nodes**. The code is as follows: === "Python" ```python title="avl_tree.py" def rotate(self, node: TreeNode | None) -> TreeNode | None: - """Perform rotation operation to restore balance to the subtree""" - # Get the balance factor of node + """Perform rotation operation to restore balance to this subtree""" + # Get balance factor of node balance_factor = self.balance_factor(node) # Left-leaning tree if balance_factor > 1: @@ -833,16 +1242,16 @@ For convenience, we encapsulate the rotation operations into a function. **With # First right rotation then left rotation node.right = self.right_rotate(node.right) return self.left_rotate(node) - # Balanced tree, no rotation needed, return + # Balanced tree, no rotation needed, return directly return node ``` === "C++" ```cpp title="avl_tree.cpp" - /* Perform rotation operation to restore balance to the subtree */ + /* Perform rotation operation to restore balance to this subtree */ TreeNode *rotate(TreeNode *node) { - // Get the balance factor of node + // Get balance factor of node int _balanceFactor = balanceFactor(node); // Left-leaning tree if (_balanceFactor > 1) { @@ -866,7 +1275,7 @@ For convenience, we encapsulate the rotation operations into a function. **With return leftRotate(node); } } - // Balanced tree, no rotation needed, return + // Balanced tree, no rotation needed, return directly return node; } ``` @@ -874,9 +1283,9 @@ For convenience, we encapsulate the rotation operations into a function. **With === "Java" ```java title="avl_tree.java" - /* Perform rotation operation to restore balance to the subtree */ + /* Perform rotation operation to restore balance to this subtree */ TreeNode rotate(TreeNode node) { - // Get the balance factor of node + // Get balance factor of node int balanceFactor = balanceFactor(node); // Left-leaning tree if (balanceFactor > 1) { @@ -900,7 +1309,7 @@ For convenience, we encapsulate the rotation operations into a function. **With return leftRotate(node); } } - // Balanced tree, no rotation needed, return + // Balanced tree, no rotation needed, return directly return node; } ``` @@ -908,74 +1317,353 @@ For convenience, we encapsulate the rotation operations into a function. **With === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Rotate} + /* Perform rotation operation to restore balance to this subtree */ + TreeNode? Rotate(TreeNode? node) { + // Get balance factor of node + int balanceFactorInt = BalanceFactor(node); + // Left-leaning tree + if (balanceFactorInt > 1) { + if (BalanceFactor(node?.left) >= 0) { + // Right rotation + return RightRotate(node); + } else { + // First left rotation then right rotation + node!.left = LeftRotate(node!.left); + return RightRotate(node); + } + } + // Right-leaning tree + if (balanceFactorInt < -1) { + if (BalanceFactor(node?.right) <= 0) { + // Left rotation + return LeftRotate(node); + } else { + // First right rotation then left rotation + node!.right = RightRotate(node!.right); + return LeftRotate(node); + } + } + // Balanced tree, no rotation needed, return directly + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + func (t *aVLTree) rotate(node *TreeNode) *TreeNode { + // Get balance factor of node + // Go recommends short variables, here bf refers to t.balanceFactor + bf := t.balanceFactor(node) + // Left-leaning tree + if bf > 1 { + if t.balanceFactor(node.Left) >= 0 { + // Right rotation + return t.rightRotate(node) + } else { + // First left rotation then right rotation + node.Left = t.leftRotate(node.Left) + return t.rightRotate(node) + } + } + // Right-leaning tree + if bf < -1 { + if t.balanceFactor(node.Right) <= 0 { + // Left rotation + return t.leftRotate(node) + } else { + // First right rotation then left rotation + node.Right = t.rightRotate(node.Right) + return t.leftRotate(node) + } + } + // Balanced tree, no rotation needed, return directly + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + func rotate(node: TreeNode?) -> TreeNode? { + // Get balance factor of node + let balanceFactor = balanceFactor(node: node) + // Left-leaning tree + if balanceFactor > 1 { + if self.balanceFactor(node: node?.left) >= 0 { + // Right rotation + return rightRotate(node: node) + } else { + // First left rotation then right rotation + node?.left = leftRotate(node: node?.left) + return rightRotate(node: node) + } + } + // Right-leaning tree + if balanceFactor < -1 { + if self.balanceFactor(node: node?.right) <= 0 { + // Left rotation + return leftRotate(node: node) + } else { + // First right rotation then left rotation + node?.right = rightRotate(node: node?.right) + return leftRotate(node: node) + } + } + // Balanced tree, no rotation needed, return directly + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + #rotate(node) { + // Get balance factor of node + const balanceFactor = this.balanceFactor(node); + // Left-leaning tree + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // Right rotation + return this.#rightRotate(node); + } else { + // First left rotation then right rotation + node.left = this.#leftRotate(node.left); + return this.#rightRotate(node); + } + } + // Right-leaning tree + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // Left rotation + return this.#leftRotate(node); + } else { + // First right rotation then left rotation + node.right = this.#rightRotate(node.right); + return this.#leftRotate(node); + } + } + // Balanced tree, no rotation needed, return directly + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + rotate(node: TreeNode): TreeNode { + // Get balance factor of node + const balanceFactor = this.balanceFactor(node); + // Left-leaning tree + if (balanceFactor > 1) { + if (this.balanceFactor(node.left) >= 0) { + // Right rotation + return this.rightRotate(node); + } else { + // First left rotation then right rotation + node.left = this.leftRotate(node.left); + return this.rightRotate(node); + } + } + // Right-leaning tree + if (balanceFactor < -1) { + if (this.balanceFactor(node.right) <= 0) { + // Left rotation + return this.leftRotate(node); + } else { + // First right rotation then left rotation + node.right = this.rightRotate(node.right); + return this.leftRotate(node); + } + } + // Balanced tree, no rotation needed, return directly + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + TreeNode? rotate(TreeNode? node) { + // Get balance factor of node + int factor = balanceFactor(node); + // Left-leaning tree + if (factor > 1) { + if (balanceFactor(node!.left) >= 0) { + // Right rotation + return rightRotate(node); + } else { + // First left rotation then right rotation + node.left = leftRotate(node.left); + return rightRotate(node); + } + } + // Right-leaning tree + if (factor < -1) { + if (balanceFactor(node!.right) <= 0) { + // Left rotation + return leftRotate(node); + } else { + // First right rotation then left rotation + node.right = rightRotate(node.right); + return leftRotate(node); + } + } + // Balanced tree, no rotation needed, return directly + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + fn rotate(node: OptionTreeNodeRc) -> OptionTreeNodeRc { + // Get balance factor of node + let balance_factor = Self::balance_factor(node.clone()); + // Left-leaning tree + if balance_factor > 1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().left.clone()) >= 0 { + // Right rotation + Self::right_rotate(Some(node)) + } else { + // First left rotation then right rotation + let left = node.borrow().left.clone(); + node.borrow_mut().left = Self::left_rotate(left); + Self::right_rotate(Some(node)) + } + } + // Right-leaning tree + else if balance_factor < -1 { + let node = node.unwrap(); + if Self::balance_factor(node.borrow().right.clone()) <= 0 { + // Left rotation + Self::left_rotate(Some(node)) + } else { + // First right rotation then left rotation + let right = node.borrow().right.clone(); + node.borrow_mut().right = Self::right_rotate(right); + Self::left_rotate(Some(node)) + } + } else { + // Balanced tree, no rotation needed, return directly + node + } + } ``` === "C" ```c title="avl_tree.c" - [class]{}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + TreeNode *rotate(TreeNode *node) { + // Get balance factor of node + int bf = balanceFactor(node); + // Left-leaning tree + if (bf > 1) { + if (balanceFactor(node->left) >= 0) { + // Right rotation + return rightRotate(node); + } else { + // First left rotation then right rotation + node->left = leftRotate(node->left); + return rightRotate(node); + } + } + // Right-leaning tree + if (bf < -1) { + if (balanceFactor(node->right) <= 0) { + // Left rotation + return leftRotate(node); + } else { + // First right rotation then left rotation + node->right = rightRotate(node->right); + return leftRotate(node); + } + } + // Balanced tree, no rotation needed, return directly + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{rotate} + /* Perform rotation operation to restore balance to this subtree */ + fun rotate(node: TreeNode): TreeNode { + // Get balance factor of node + val balanceFactor = balanceFactor(node) + // Left-leaning tree + if (balanceFactor > 1) { + if (balanceFactor(node.left) >= 0) { + // Right rotation + return rightRotate(node) + } else { + // First left rotation then right rotation + node.left = leftRotate(node.left) + return rightRotate(node) + } + } + // Right-leaning tree + if (balanceFactor < -1) { + if (balanceFactor(node.right) <= 0) { + // Left rotation + return leftRotate(node) + } else { + // First right rotation then left rotation + node.right = rightRotate(node.right) + return leftRotate(node) + } + } + // Balanced tree, no rotation needed, return directly + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{rotate} + ### Perform rotation to rebalance subtree ### + def rotate(node) + # Get balance factor of node + balance_factor = balance_factor(node) + # Left-heavy tree + if balance_factor > 1 + if balance_factor(node.left) >= 0 + # Right rotation + return right_rotate(node) + else + # First left rotation then right rotation + node.left = left_rotate(node.left) + return right_rotate(node) + end + # Right-heavy tree + elsif balance_factor < -1 + if balance_factor(node.right) <= 0 + # Left rotation + return left_rotate(node) + else + # First right rotation then left rotation + node.right = right_rotate(node.right) + return left_rotate(node) + end + end + # Balanced tree, no rotation needed, return directly + node + end ``` -=== "Zig" +## 7.5.3   Common Operations in Avl Trees - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{rotate} - ``` +### 1.   Node Insertion -## 7.5.3   Common operations in AVL trees - -### 1.   Node insertion - -The node insertion operation in AVL trees is similar to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear along the path from that node to the root node. Therefore, **we need to start from this node and perform rotation operations upwards to restore balance to all unbalanced nodes**. The code is as follows: +The node insertion operation in AVL trees is similar in principle to that in binary search trees. The only difference is that after inserting a node in an AVL tree, a series of unbalanced nodes may appear on the path from that node to the root. Therefore, **we need to start from this node and perform rotation operations from bottom to top, restoring balance to all unbalanced nodes**. The code is as follows: === "Python" @@ -994,11 +1682,11 @@ The node insertion operation in AVL trees is similar to that in binary search tr elif val > node.val: node.right = self.insert_helper(node.right, val) else: - # Do not insert duplicate nodes, return + # Duplicate node not inserted, return directly return node # Update node height self.update_height(node) - # 2. Perform rotation operation to restore balance to the subtree + # 2. Perform rotation operation to restore balance to this subtree return self.rotate(node) ``` @@ -1020,11 +1708,11 @@ The node insertion operation in AVL trees is similar to that in binary search tr else if (val > node->val) node->right = insertHelper(node->right, val); else - return node; // Do not insert duplicate nodes, return + return node; // Duplicate node not inserted, return directly updateHeight(node); // Update node height - /* 2. Perform rotation operation to restore balance to the subtree */ + /* 2. Perform rotation operation to restore balance to this subtree */ node = rotate(node); - // Return the root node of the subtree + // Return root node of subtree return node; } ``` @@ -1047,11 +1735,11 @@ The node insertion operation in AVL trees is similar to that in binary search tr else if (val > node.val) node.right = insertHelper(node.right, val); else - return node; // Do not insert duplicate nodes, return + return node; // Duplicate node not inserted, return directly updateHeight(node); // Update node height - /* 2. Perform rotation operation to restore balance to the subtree */ + /* 2. Perform rotation operation to restore balance to this subtree */ node = rotate(node); - // Return the root node of the subtree + // Return root node of subtree return node; } ``` @@ -1059,107 +1747,312 @@ The node insertion operation in AVL trees is similar to that in binary search tr === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Insert} + /* Insert node */ + void Insert(int val) { + root = InsertHelper(root, val); + } - [class]{AVLTree}-[func]{InsertHelper} + /* Recursively insert node (helper method) */ + TreeNode? InsertHelper(TreeNode? node, int val) { + if (node == null) return new TreeNode(val); + /* 1. Find insertion position and insert node */ + if (val < node.val) + node.left = InsertHelper(node.left, val); + else if (val > node.val) + node.right = InsertHelper(node.right, val); + else + return node; // Duplicate node not inserted, return directly + UpdateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = Rotate(node); + // Return root node of subtree + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{insert} + /* Insert node */ + func (t *aVLTree) insert(val int) { + t.root = t.insertHelper(t.root, val) + } - [class]{aVLTree}-[func]{insertHelper} + /* Recursively insert node (helper function) */ + func (t *aVLTree) insertHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return NewTreeNode(val) + } + /* 1. Find insertion position and insert node */ + 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 { + // Duplicate node not inserted, return directly + return node + } + // Update node height + t.updateHeight(node) + /* 2. Perform rotation operation to restore balance to this subtree */ + node = t.rotate(node) + // Return root node of subtree + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + func insert(val: Int) { + root = insertHelper(node: root, val: val) + } - [class]{AVLTree}-[func]{insertHelper} + /* Recursively insert node (helper method) */ + func insertHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return TreeNode(x: val) + } + /* 1. Find insertion position and insert node */ + 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 // Duplicate node not inserted, return directly + } + updateHeight(node: node) // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node: node) + // Return root node of subtree + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + insert(val) { + this.root = this.#insertHelper(this.root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* Recursively insert node (helper method) */ + #insertHelper(node, val) { + if (node === null) return new TreeNode(val); + /* 1. Find insertion position and insert node */ + 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; // Duplicate node not inserted, return directly + this.#updateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = this.#rotate(node); + // Return root node of subtree + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + insert(val: number): void { + this.root = this.insertHelper(this.root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* Recursively insert node (helper method) */ + insertHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return new TreeNode(val); + /* 1. Find insertion position and insert node */ + 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; // Duplicate node not inserted, return directly + } + this.updateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = this.rotate(node); + // Return root node of subtree + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + void insert(int val) { + root = insertHelper(root, val); + } - [class]{AVLTree}-[func]{insertHelper} + /* Recursively insert node (helper method) */ + TreeNode? insertHelper(TreeNode? node, int val) { + if (node == null) return TreeNode(val); + /* 1. Find insertion position and insert node */ + if (val < node.val) + node.left = insertHelper(node.left, val); + else if (val > node.val) + node.right = insertHelper(node.right, val); + else + return node; // Duplicate node not inserted, return directly + updateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node); + // Return root node of subtree + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + fn insert(&mut self, val: i32) { + self.root = Self::insert_helper(self.root.clone(), val); + } - [class]{AVLTree}-[func]{insert_helper} + /* Recursively insert node (helper method) */ + fn insert_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. Find insertion position and insert node */ + 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); // Duplicate node not inserted, return directly + } + } + Self::update_height(Some(node.clone())); // Update node height + + /* 2. Perform rotation operation to restore balance to this subtree */ + node = Self::rotate(Some(node)).unwrap(); + // Return root node of subtree + Some(node) + } + None => Some(TreeNode::new(val)), + } + } ``` === "C" ```c title="avl_tree.c" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + void insert(AVLTree *tree, int val) { + tree->root = insertHelper(tree->root, val); + } - [class]{}-[func]{insertHelper} + /* Recursively insert node (helper function) */ + TreeNode *insertHelper(TreeNode *node, int val) { + if (node == NULL) { + return newTreeNode(val); + } + /* 1. Find insertion position and insert node */ + if (val < node->val) { + node->left = insertHelper(node->left, val); + } else if (val > node->val) { + node->right = insertHelper(node->right, val); + } else { + // Duplicate node not inserted, return directly + return node; + } + // Update node height + updateHeight(node); + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node); + // Return root node of subtree + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{insert} + /* Insert node */ + fun insert(_val: Int) { + root = insertHelper(root, _val) + } - [class]{AVLTree}-[func]{insertHelper} + /* Recursively insert node (helper method) */ + fun insertHelper(n: TreeNode?, _val: Int): TreeNode { + if (n == null) + return TreeNode(_val) + var node = n + /* 1. Find insertion position and insert node */ + if (_val < node._val) + node.left = insertHelper(node.left, _val) + else if (_val > node._val) + node.right = insertHelper(node.right, _val) + else + return node // Duplicate node not inserted, return directly + updateHeight(node) // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node) + // Return root node of subtree + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{insert} + ### Insert node ### + def insert(val) + @root = insert_helper(@root, val) + end - [class]{AVLTree}-[func]{insert_helper} + ### Recursively insert node (helper method) ### + def insert_helper(node, val) + return TreeNode.new(val) if node.nil? + # 1. Find insertion position and insert node + if val < node.val + node.left = insert_helper(node.left, val) + elsif val > node.val + node.right = insert_helper(node.right, val) + else + # Duplicate node not inserted, return directly + return node + end + # Update node height + update_height(node) + # 2. Perform rotation operation to restore balance to this subtree + rotate(node) + end ``` -=== "Zig" +### 2.   Node Removal - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{insert} - - [class]{AVLTree}-[func]{insertHelper} - ``` - -### 2.   Node removal - -Similarly, based on the method of removing nodes in binary search trees, rotation operations need to be performed from the bottom up to restore balance to all unbalanced nodes. The code is as follows: +Similarly, on the basis of the binary search tree's node removal method, rotation operations need to be performed from bottom to top to restore balance to all unbalanced nodes. The code is as follows: === "Python" ```python title="avl_tree.py" def remove(self, val: int): - """Remove node""" + """Delete node""" self._root = self.remove_helper(self._root, val) def remove_helper(self, node: TreeNode | None, val: int) -> TreeNode | None: - """Recursively remove node (helper method)""" + """Recursively delete node (helper method)""" if node is None: return None - # 1. Find and remove the node + # 1. Find node and delete if val < node.val: node.left = self.remove_helper(node.left, val) elif val > node.val: @@ -1167,14 +2060,14 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio else: if node.left is None or node.right is None: child = node.left or node.right - # Number of child nodes = 0, remove node and return + # Number of child nodes = 0, delete node directly and return if child is None: return None - # Number of child nodes = 1, remove node + # Number of child nodes = 1, delete node directly else: node = child else: - # Number of child nodes = 2, remove the next node in in-order traversal and replace the current node with it + # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it temp = node.right while temp.left is not None: temp = temp.left @@ -1182,7 +2075,7 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio node.val = temp.val # Update node height self.update_height(node) - # 2. Perform rotation operation to restore balance to the subtree + # 2. Perform rotation operation to restore balance to this subtree return self.rotate(node) ``` @@ -1194,11 +2087,11 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio root = removeHelper(root, val); } - /* Recursively remove node (helper method) */ + /* Recursively delete node (helper method) */ TreeNode *removeHelper(TreeNode *node, int val) { if (node == nullptr) return nullptr; - /* 1. Find and remove the node */ + /* 1. Find node and delete */ if (val < node->val) node->left = removeHelper(node->left, val); else if (val > node->val) @@ -1206,18 +2099,18 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio else { if (node->left == nullptr || node->right == nullptr) { TreeNode *child = node->left != nullptr ? node->left : node->right; - // Number of child nodes = 0, remove node and return + // Number of child nodes = 0, delete node directly and return if (child == nullptr) { delete node; return nullptr; } - // Number of child nodes = 1, remove node + // Number of child nodes = 1, delete node directly else { delete node; node = child; } } else { - // Number of child nodes = 2, remove the next node in in-order traversal and replace the current node with it + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it TreeNode *temp = node->right; while (temp->left != nullptr) { temp = temp->left; @@ -1228,9 +2121,9 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio } } updateHeight(node); // Update node height - /* 2. Perform rotation operation to restore balance to the subtree */ + /* 2. Perform rotation operation to restore balance to this subtree */ node = rotate(node); - // Return the root node of the subtree + // Return root node of subtree return node; } ``` @@ -1243,11 +2136,11 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio root = removeHelper(root, val); } - /* Recursively remove node (helper method) */ + /* Recursively delete node (helper method) */ TreeNode removeHelper(TreeNode node, int val) { if (node == null) return null; - /* 1. Find and remove the node */ + /* 1. Find node and delete */ if (val < node.val) node.left = removeHelper(node.left, val); else if (val > node.val) @@ -1255,14 +2148,14 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio else { if (node.left == null || node.right == null) { TreeNode child = node.left != null ? node.left : node.right; - // Number of child nodes = 0, remove node and return + // Number of child nodes = 0, delete node directly and return if (child == null) return null; - // Number of child nodes = 1, remove node + // Number of child nodes = 1, delete node directly else node = child; } else { - // Number of child nodes = 2, remove the next node in in-order traversal and replace the current node with it + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it TreeNode temp = node.right; while (temp.left != null) { temp = temp.left; @@ -1272,9 +2165,9 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio } } updateHeight(node); // Update node height - /* 2. Perform rotation operation to restore balance to the subtree */ + /* 2. Perform rotation operation to restore balance to this subtree */ node = rotate(node); - // Return the root node of the subtree + // Return root node of subtree return node; } ``` @@ -1282,97 +2175,475 @@ Similarly, based on the method of removing nodes in binary search trees, rotatio === "C#" ```csharp title="avl_tree.cs" - [class]{AVLTree}-[func]{Remove} + /* Remove node */ + void Remove(int val) { + root = RemoveHelper(root, val); + } - [class]{AVLTree}-[func]{RemoveHelper} + /* Recursively delete node (helper method) */ + TreeNode? RemoveHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. Find node and delete */ + 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; + // Number of child nodes = 0, delete node directly and return + if (child == null) + return null; + // Number of child nodes = 1, delete node directly + else + node = child; + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + TreeNode? temp = node.right; + while (temp.left != null) { + temp = temp.left; + } + node.right = RemoveHelper(node.right, temp.val!.Value); + node.val = temp.val; + } + } + UpdateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = Rotate(node); + // Return root node of subtree + return node; + } ``` === "Go" ```go title="avl_tree.go" - [class]{aVLTree}-[func]{remove} + /* Remove node */ + func (t *aVLTree) remove(val int) { + t.root = t.removeHelper(t.root, val) + } - [class]{aVLTree}-[func]{removeHelper} + /* Recursively remove node (helper function) */ + func (t *aVLTree) removeHelper(node *TreeNode, val int) *TreeNode { + if node == nil { + return nil + } + /* 1. Find node and delete */ + 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 { + // Number of child nodes = 0, delete node directly and return + return nil + } else { + // Number of child nodes = 1, delete node directly + node = child + } + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + temp := node.Right + for temp.Left != nil { + temp = temp.Left + } + node.Right = t.removeHelper(node.Right, temp.Val.(int)) + node.Val = temp.Val + } + } + // Update node height + t.updateHeight(node) + /* 2. Perform rotation operation to restore balance to this subtree */ + node = t.rotate(node) + // Return root node of subtree + return node + } ``` === "Swift" ```swift title="avl_tree.swift" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + func remove(val: Int) { + root = removeHelper(node: root, val: val) + } - [class]{AVLTree}-[func]{removeHelper} + /* Recursively delete node (helper method) */ + func removeHelper(node: TreeNode?, val: Int) -> TreeNode? { + var node = node + if node == nil { + return nil + } + /* 1. Find node and delete */ + if val < node!.val { + node?.left = removeHelper(node: node?.left, val: val) + } else if val > node!.val { + node?.right = removeHelper(node: node?.right, val: val) + } else { + if node?.left == nil || node?.right == nil { + let child = node?.left ?? node?.right + // Number of child nodes = 0, delete node directly and return + if child == nil { + return nil + } + // Number of child nodes = 1, delete node directly + else { + node = child + } + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + 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) // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node: node) + // Return root node of subtree + return node + } ``` === "JS" ```javascript title="avl_tree.js" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + remove(val) { + this.root = this.#removeHelper(this.root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* Recursively delete node (helper method) */ + #removeHelper(node, val) { + if (node === null) return null; + /* 1. Find node and delete */ + 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; + // Number of child nodes = 0, delete node directly and return + if (child === null) return null; + // Number of child nodes = 1, delete node directly + else node = child; + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + 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); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = this.#rotate(node); + // Return root node of subtree + return node; + } ``` === "TS" ```typescript title="avl_tree.ts" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + remove(val: number): void { + this.root = this.removeHelper(this.root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* Recursively delete node (helper method) */ + removeHelper(node: TreeNode, val: number): TreeNode { + if (node === null) return null; + /* 1. Find node and delete */ + 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; + // Number of child nodes = 0, delete node directly and return + if (child === null) { + return null; + } else { + // Number of child nodes = 1, delete node directly + node = child; + } + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + 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); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = this.rotate(node); + // Return root node of subtree + return node; + } ``` === "Dart" ```dart title="avl_tree.dart" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + void remove(int val) { + root = removeHelper(root, val); + } - [class]{AVLTree}-[func]{removeHelper} + /* Recursively delete node (helper method) */ + TreeNode? removeHelper(TreeNode? node, int val) { + if (node == null) return null; + /* 1. Find node and delete */ + 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; + // Number of child nodes = 0, delete node directly and return + if (child == null) + return null; + // Number of child nodes = 1, delete node directly + else + node = child; + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + TreeNode? temp = node.right; + while (temp!.left != null) { + temp = temp.left; + } + node.right = removeHelper(node.right, temp.val); + node.val = temp.val; + } + } + updateHeight(node); // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node); + // Return root node of subtree + return node; + } ``` === "Rust" ```rust title="avl_tree.rs" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + fn remove(&self, val: i32) { + Self::remove_helper(self.root.clone(), val); + } - [class]{AVLTree}-[func]{remove_helper} + /* Recursively delete node (helper method) */ + fn remove_helper(node: OptionTreeNodeRc, val: i32) -> OptionTreeNodeRc { + match node { + Some(mut node) => { + /* 1. Find node and delete */ + 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 { + // Number of child nodes = 0, delete node directly and return + None => { + return None; + } + // Number of child nodes = 1, delete node directly + Some(child) => node = child, + } + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + 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())); // Update node height + + /* 2. Perform rotation operation to restore balance to this subtree */ + node = Self::rotate(Some(node)).unwrap(); + // Return root node of subtree + Some(node) + } + None => None, + } + } ``` === "C" ```c title="avl_tree.c" - [class]{AVLTree}-[func]{removeItem} + /* Remove node */ + // Cannot use remove keyword here due to stdio.h inclusion + void removeItem(AVLTree *tree, int val) { + TreeNode *root = removeHelper(tree->root, val); + } - [class]{}-[func]{removeHelper} + /* Recursively remove node (helper function) */ + TreeNode *removeHelper(TreeNode *node, int val) { + TreeNode *child, *grandChild; + if (node == NULL) { + return NULL; + } + /* 1. Find node and delete */ + 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; + } + // Number of child nodes = 0, delete node directly and return + if (child == NULL) { + return NULL; + } else { + // Number of child nodes = 1, delete node directly + node = child; + } + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + 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; + } + } + // Update node height + updateHeight(node); + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node); + // Return root node of subtree + return node; + } ``` === "Kotlin" ```kotlin title="avl_tree.kt" - [class]{AVLTree}-[func]{remove} + /* Remove node */ + fun remove(_val: Int) { + root = removeHelper(root, _val) + } - [class]{AVLTree}-[func]{removeHelper} + /* Recursively delete node (helper method) */ + fun removeHelper(n: TreeNode?, _val: Int): TreeNode? { + var node = n ?: return null + /* 1. Find node and delete */ + 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) { + val child = if (node.left != null) + node.left + else + node.right + // Number of child nodes = 0, delete node directly and return + if (child == null) + return null + // Number of child nodes = 1, delete node directly + else + node = child + } else { + // Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + var temp = node.right + while (temp!!.left != null) { + temp = temp.left + } + node.right = removeHelper(node.right, temp._val) + node._val = temp._val + } + } + updateHeight(node) // Update node height + /* 2. Perform rotation operation to restore balance to this subtree */ + node = rotate(node) + // Return root node of subtree + return node + } ``` === "Ruby" ```ruby title="avl_tree.rb" - [class]{AVLTree}-[func]{remove} + ### Delete node ### + def remove(val) + @root = remove_helper(@root, val) + end - [class]{AVLTree}-[func]{remove_helper} + ### Recursively delete node (helper method) ### + def remove_helper(node, val) + return if node.nil? + # 1. Find node and delete + if val < node.val + node.left = remove_helper(node.left, val) + elsif val > node.val + node.right = remove_helper(node.right, val) + else + if node.left.nil? || node.right.nil? + child = node.left || node.right + # Number of child nodes = 0, delete node directly and return + return if child.nil? + # Number of child nodes = 1, delete node directly + node = child + else + # Number of child nodes = 2, delete the next node in inorder traversal and replace current node with it + temp = node.right + while !temp.left.nil? + temp = temp.left + end + node.right = remove_helper(node.right, temp.val) + node.val = temp.val + end + end + # Update node height + update_height(node) + # 2. Perform rotation operation to restore balance to this subtree + rotate(node) + end ``` -=== "Zig" +### 3.   Node Search - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{remove} +The node search operation in AVL trees is consistent with that in binary search trees, and will not be elaborated here. - [class]{AVLTree}-[func]{removeHelper} - ``` +## 7.5.4   Typical Applications of Avl Trees -### 3.   Node search - -The node search operation in AVL trees is consistent with that in binary search trees and will not be detailed here. - -## 7.5.4   Typical applications of AVL trees - -- Organizing and storing large amounts of data, suitable for scenarios with high-frequency searches and low-frequency intertions and removals. +- Organizing and storing large-scale data, suitable for scenarios with high-frequency searches and low-frequency insertions and deletions. - Used to build index systems in databases. -- Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balancing conditions, require fewer rotations for node insertion and removal, and have a higher average efficiency for node addition and removal operations. +- Red-black trees are also a common type of balanced binary search tree. Compared to AVL trees, red-black trees have more relaxed balance conditions, require fewer rotation operations for node insertion and deletion, and have higher average efficiency for node addition and deletion operations. diff --git a/en/docs/chapter_tree/binary_search_tree.md b/en/docs/chapter_tree/binary_search_tree.md index 26792571c..2f3384b3a 100755 --- a/en/docs/chapter_tree/binary_search_tree.md +++ b/en/docs/chapter_tree/binary_search_tree.md @@ -2,7 +2,7 @@ comments: true --- -# 7.4   Binary search tree +# 7.4   Binary Search Tree As shown in Figure 7-16, a binary search tree satisfies the following conditions. @@ -13,13 +13,13 @@ As shown in Figure 7-16, a binary search tree satisfies the following con

Figure 7-16   Binary search tree

-## 7.4.1   Operations on a binary search tree +## 7.4.1   Operations on a Binary Search Tree We encapsulate the binary search tree as a class `BinarySearchTree` and declare a member variable `root` pointing to the tree's root node. -### 1.   Searching for a node +### 1.   Searching for a Node -Given a target node value `num`, one can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node `cur`, start from the binary tree's root node `root`, and loop to compare the size between the node value `cur.val` and `num`. +Given a target node value `num`, we can search according to the properties of the binary search tree. As shown in Figure 7-17, we declare a node `cur` and start from the binary tree's root node `root`, looping to compare the node value `cur.val` with `num`. - If `cur.val < num`, it means the target node is in `cur`'s right subtree, thus execute `cur = cur.right`. - If `cur.val > num`, it means the target node is in `cur`'s left subtree, thus execute `cur = cur.left`. @@ -39,7 +39,7 @@ Given a target node value `num`, one can search according to the properties of t

Figure 7-17   Example of searching for a node in a binary search tree

-The search operation in a binary search tree works on the same principle as the binary search algorithm, eliminating half of the cases in each round. The number of loops is at most the height of the binary tree. When the binary tree is balanced, it uses $O(\log n)$ time. The example code is as follows: +The search operation in a binary search tree works on the same principle as the binary search algorithm, both eliminating half of the cases in each round. The number of loop iterations is at most the height of the binary tree. When the binary tree is balanced, it uses $O(\log n)$ time. The example code is as follows: === "Python" @@ -47,7 +47,7 @@ The search operation in a binary search tree works on the same principle as the def search(self, num: int) -> TreeNode | None: """Search node""" cur = self._root - # Loop find, break after passing leaf nodes + # Loop search, exit after passing leaf node while cur is not None: # Target node is in cur's right subtree if cur.val < num: @@ -55,7 +55,7 @@ The search operation in a binary search tree works on the same principle as the # Target node is in cur's left subtree elif cur.val > num: cur = cur.left - # Found target node, break loop + # Found target node, exit loop else: break return cur @@ -67,7 +67,7 @@ The search operation in a binary search tree works on the same principle as the /* Search node */ TreeNode *search(int num) { TreeNode *cur = root; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != nullptr) { // Target node is in cur's right subtree if (cur->val < num) @@ -75,7 +75,7 @@ The search operation in a binary search tree works on the same principle as the // Target node is in cur's left subtree else if (cur->val > num) cur = cur->left; - // Found target node, break loop + // Found target node, exit loop else break; } @@ -90,7 +90,7 @@ The search operation in a binary search tree works on the same principle as the /* Search node */ TreeNode search(int num) { TreeNode cur = root; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != null) { // Target node is in cur's right subtree if (cur.val < num) @@ -98,7 +98,7 @@ The search operation in a binary search tree works on the same principle as the // Target node is in cur's left subtree else if (cur.val > num) cur = cur.left; - // Found target node, break loop + // Found target node, exit loop else break; } @@ -110,84 +110,249 @@ The search operation in a binary search tree works on the same principle as the === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Search} + /* Search node */ + TreeNode? Search(int num) { + TreeNode? cur = root; + // Loop search, exit after passing leaf node + while (cur != null) { + // Target node is in cur's right subtree + if (cur.val < num) cur = + cur.right; + // Target node is in cur's left subtree + else if (cur.val > num) + cur = cur.left; + // Found target node, exit loop + else + break; + } + // Return target node + return cur; + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{search} + /* Search node */ + func (bst *binarySearchTree) search(num int) *TreeNode { + node := bst.root + // Loop search, exit after passing leaf node + for node != nil { + if node.Val.(int) < num { + // Target node is in cur's right subtree + node = node.Right + } else if node.Val.(int) > num { + // Target node is in cur's left subtree + node = node.Left + } else { + // Found target node, exit loop + break + } + } + // Return target node + return node + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + func search(num: Int) -> TreeNode? { + var cur = root + // Loop search, exit after passing leaf node + while cur != nil { + // Target node is in cur's right subtree + if cur!.val < num { + cur = cur?.right + } + // Target node is in cur's left subtree + else if cur!.val > num { + cur = cur?.left + } + // Found target node, exit loop + else { + break + } + } + // Return target node + return cur + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + search(num) { + let cur = this.root; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Target node is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Target node is in cur's left subtree + else if (cur.val > num) cur = cur.left; + // Found target node, exit loop + else break; + } + // Return target node + return cur; + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + search(num: number): TreeNode | null { + let cur = this.root; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Target node is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Target node is in cur's left subtree + else if (cur.val > num) cur = cur.left; + // Found target node, exit loop + else break; + } + // Return target node + return cur; + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + TreeNode? search(int _num) { + TreeNode? cur = _root; + // Loop search, exit after passing leaf node + while (cur != null) { + // Target node is in cur's right subtree + if (cur.val < _num) + cur = cur.right; + // Target node is in cur's left subtree + else if (cur.val > _num) + cur = cur.left; + // Found target node, exit loop + else + break; + } + // Return target node + return cur; + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + pub fn search(&self, num: i32) -> OptionTreeNodeRc { + let mut cur = self.root.clone(); + // Loop search, exit after passing leaf node + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // Target node is in cur's right subtree + Ordering::Greater => cur = node.borrow().right.clone(), + // Target node is in cur's left subtree + Ordering::Less => cur = node.borrow().left.clone(), + // Found target node, exit loop + Ordering::Equal => break, + } + } + + // Return target node + cur + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + TreeNode *search(BinarySearchTree *bst, int num) { + TreeNode *cur = bst->root; + // Loop search, exit after passing leaf node + while (cur != NULL) { + if (cur->val < num) { + // Target node is in cur's right subtree + cur = cur->right; + } else if (cur->val > num) { + // Target node is in cur's left subtree + cur = cur->left; + } else { + // Found target node, exit loop + break; + } + } + // Return target node + return cur; + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{search} + /* Search node */ + fun search(num: Int): TreeNode? { + var cur = root + // Loop search, exit after passing leaf node + while (cur != null) { + // Target node is in cur's right subtree + cur = if (cur._val < num) + cur.right + // Target node is in cur's left subtree + else if (cur._val > num) + cur.left + // Found target node, exit loop + else + break + } + // Return target node + return cur + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{search} + ### Search node ### + def search(num) + cur = @root + + # Loop search, exit after passing leaf node + while !cur.nil? + # Target node is in cur's right subtree + if cur.val < num + cur = cur.right + # Target node is in cur's left subtree + elsif cur.val > num + cur = cur.left + # Found target node, exit loop + else + break + end + end + + cur + end ``` -=== "Zig" +### 2.   Inserting a Node - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{search} - ``` +Given an element `num` to be inserted, in order to maintain the property of the binary search tree "left subtree < root node < right subtree," the insertion process is as shown in Figure 7-18. -### 2.   Inserting a node - -Given an element `num` to be inserted, to maintain the property of the binary search tree "left subtree < root node < right subtree," the insertion operation proceeds as shown in Figure 7-18. - -1. **Finding insertion position**: Similar to the search operation, start from the root node, loop downwards according to the size relationship between the current node value and `num`, until the leaf node is passed (traversed to `None`), then exit the loop. -2. **Insert the node at this position**: Initialize the node `num` and place it where `None` was. +1. **Finding the insertion position**: Similar to the search operation, start from the root node and loop downward searching according to the size relationship between the current node value and `num`, until passing the leaf node (traversing to `None`) and then exit the loop. +2. **Insert the node at that position**: Initialize node `num` and place it at the `None` position. ![Inserting a node into a binary search tree](binary_search_tree.assets/bst_insert.png){ class="animation-figure" }

Figure 7-18   Inserting a node into a binary search tree

-In the code implementation, note the following two points. +In the code implementation, note the following two points: -- The binary search tree does not allow duplicate nodes to exist; otherwise, its definition would be violated. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed, and the node returns directly. -- To perform the insertion operation, we need to use the node `pre` to save the node from the previous loop. This way, when traversing to `None`, we can get its parent node, thus completing the node insertion operation. +- Binary search trees do not allow duplicate nodes; otherwise, it would violate its definition. Therefore, if the node to be inserted already exists in the tree, the insertion is not performed and it returns directly. +- To implement the node insertion, we need to use node `pre` to save the node from the previous loop iteration. This way, when traversing to `None`, we can obtain its parent node, thereby completing the node insertion operation. === "Python" @@ -198,10 +363,10 @@ In the code implementation, note the following two points. if self._root is None: self._root = TreeNode(num) return - # Loop find, break after passing leaf nodes + # Loop search, exit after passing leaf node cur, pre = self._root, None while cur is not None: - # Found duplicate node, thus return + # Found duplicate node, return directly if cur.val == num: return pre = cur @@ -230,9 +395,9 @@ In the code implementation, note the following two points. return; } TreeNode *cur = root, *pre = nullptr; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != nullptr) { - // Found duplicate node, thus return + // Found duplicate node, return directly if (cur->val == num) return; pre = cur; @@ -263,9 +428,9 @@ In the code implementation, note the following two points. return; } TreeNode cur = root, pre = null; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != null) { - // Found duplicate node, thus return + // Found duplicate node, return directly if (cur.val == num) return; pre = cur; @@ -288,74 +453,355 @@ In the code implementation, note the following two points. === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Insert} + /* Insert node */ + void Insert(int num) { + // If tree is empty, initialize root node + if (root == null) { + root = new TreeNode(num); + return; + } + TreeNode? cur = root, pre = null; + // Loop search, exit after passing leaf node + while (cur != null) { + // Found duplicate node, return directly + if (cur.val == num) + return; + pre = cur; + // Insertion position is in cur's right subtree + if (cur.val < num) + cur = cur.right; + // Insertion position is in cur's left subtree + else + cur = cur.left; + } + + // Insert node + TreeNode node = new(num); + if (pre != null) { + if (pre.val < num) + pre.right = node; + else + pre.left = node; + } + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{insert} + /* Insert node */ + func (bst *binarySearchTree) insert(num int) { + cur := bst.root + // If tree is empty, initialize root node + if cur == nil { + bst.root = NewTreeNode(num) + return + } + // Node position before the node to be inserted + var pre *TreeNode = nil + // Loop search, exit after passing leaf node + for cur != nil { + if cur.Val == num { + return + } + pre = cur + if cur.Val.(int) < num { + cur = cur.Right + } else { + cur = cur.Left + } + } + // Insert node + node := NewTreeNode(num) + if pre.Val.(int) < num { + pre.Right = node + } else { + pre.Left = node + } + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + func insert(num: Int) { + // If tree is empty, initialize root node + if root == nil { + root = TreeNode(x: num) + return + } + var cur = root + var pre: TreeNode? + // Loop search, exit after passing leaf node + while cur != nil { + // Found duplicate node, return directly + if cur!.val == num { + return + } + pre = cur + // Insertion position is in cur's right subtree + if cur!.val < num { + cur = cur?.right + } + // Insertion position is in cur's left subtree + else { + cur = cur?.left + } + } + // Insert node + let node = TreeNode(x: num) + if pre!.val < num { + pre?.right = node + } else { + pre?.left = node + } + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + insert(num) { + // If tree is empty, initialize root node + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur = this.root, + pre = null; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Found duplicate node, return directly + if (cur.val === num) return; + pre = cur; + // Insertion position is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Insertion position is in cur's left subtree + else cur = cur.left; + } + // Insert node + const node = new TreeNode(num); + if (pre.val < num) pre.right = node; + else pre.left = node; + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + insert(num: number): void { + // If tree is empty, initialize root node + if (this.root === null) { + this.root = new TreeNode(num); + return; + } + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Found duplicate node, return directly + if (cur.val === num) return; + pre = cur; + // Insertion position is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Insertion position is in cur's left subtree + else cur = cur.left; + } + // Insert node + const node = new TreeNode(num); + if (pre!.val < num) pre!.right = node; + else pre!.left = node; + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + void insert(int _num) { + // If tree is empty, initialize root node + if (_root == null) { + _root = TreeNode(_num); + return; + } + TreeNode? cur = _root; + TreeNode? pre = null; + // Loop search, exit after passing leaf node + while (cur != null) { + // Found duplicate node, return directly + if (cur.val == _num) return; + pre = cur; + // Insertion position is in cur's right subtree + if (cur.val < _num) + cur = cur.right; + // Insertion position is in cur's left subtree + else + cur = cur.left; + } + // Insert node + TreeNode? node = TreeNode(_num); + if (pre!.val < _num) + pre.right = node; + else + pre.left = node; + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + pub fn insert(&mut self, num: i32) { + // If tree is empty, initialize root node + if self.root.is_none() { + self.root = Some(TreeNode::new(num)); + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // Loop search, exit after passing leaf node + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // Found duplicate node, return directly + Ordering::Equal => return, + // Insertion position is in cur's right subtree + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // Insertion position is in cur's left subtree + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // Insert node + let pre = pre.unwrap(); + let node = Some(TreeNode::new(num)); + if num > pre.borrow().val { + pre.borrow_mut().right = node; + } else { + pre.borrow_mut().left = node; + } + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + void insert(BinarySearchTree *bst, int num) { + // If tree is empty, initialize root node + if (bst->root == NULL) { + bst->root = newTreeNode(num); + return; + } + TreeNode *cur = bst->root, *pre = NULL; + // Loop search, exit after passing leaf node + while (cur != NULL) { + // Found duplicate node, return directly + if (cur->val == num) { + return; + } + pre = cur; + if (cur->val < num) { + // Insertion position is in cur's right subtree + cur = cur->right; + } else { + // Insertion position is in cur's left subtree + cur = cur->left; + } + } + // Insert node + TreeNode *node = newTreeNode(num); + if (pre->val < num) { + pre->right = node; + } else { + pre->left = node; + } + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{insert} + /* Insert node */ + fun insert(num: Int) { + // If tree is empty, initialize root node + if (root == null) { + root = TreeNode(num) + return + } + var cur = root + var pre: TreeNode? = null + // Loop search, exit after passing leaf node + while (cur != null) { + // Found duplicate node, return directly + if (cur._val == num) + return + pre = cur + // Insertion position is in cur's right subtree + cur = if (cur._val < num) + cur.right + // Insertion position is in cur's left subtree + else + cur.left + } + // Insert node + val node = TreeNode(num) + if (pre?._val!! < num) + pre.right = node + else + pre.left = node + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{insert} - ``` + ### Insert node ### + def insert(num) + # If tree is empty, initialize root node + if @root.nil? + @root = TreeNode.new(num) + return + end -=== "Zig" + # Loop search, exit after passing leaf node + cur, pre = @root, nil + while !cur.nil? + # Found duplicate node, return directly + return if cur.val == num - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{insert} + pre = cur + # Insertion position is in cur's right subtree + if cur.val < num + cur = cur.right + # Insertion position is in cur's left subtree + else + cur = cur.left + end + end + + # Insert node + node = TreeNode.new(num) + if pre.val < num + pre.right = node + else + pre.left = node + end + end ``` Similar to searching for a node, inserting a node uses $O(\log n)$ time. -### 3.   Removing a node +### 3.   Removing a Node -First, find the target node in the binary tree, then remove it. Similar to inserting a node, we need to ensure that after the removal operation is completed, the property of the binary search tree "left subtree < root node < right subtree" is still satisfied. Therefore, based on the number of child nodes of the target node, we divide it into three cases: 0, 1, and 2, and perform the corresponding node removal operations. +First, find the target node in the binary tree, then remove it. Similar to node insertion, we need to ensure that after the removal operation is completed, the binary search tree's property of "left subtree $<$ root node $<$ right subtree" is still maintained. Therefore, depending on the number of child nodes the target node has, we divide it into 0, 1, and 2 three cases, and execute the corresponding node removal operations. As shown in Figure 7-19, when the degree of the node to be removed is $0$, it means the node is a leaf node and can be directly removed. @@ -369,12 +815,12 @@ As shown in Figure 7-20, when the degree of the node to be removed is $1$, repla

Figure 7-20   Removing a node in a binary search tree (degree 1)

-When the degree of the node to be removed is $2$, we cannot remove it directly, but need to use a node to replace it. To maintain the property of the binary search tree "left subtree $<$ root node $<$ right subtree," **this node can be either the smallest node of the right subtree or the largest node of the left subtree**. +When the degree of the node to be removed is $2$, we cannot directly remove it; instead, we need to use a node to replace it. To maintain the binary search tree's property of "left subtree $<$ root node $<$ right subtree," **this node can be either the smallest node in the right subtree or the largest node in the left subtree**. -Assuming we choose the smallest node of the right subtree (the next node in in-order traversal), then the removal operation proceeds as shown in Figure 7-21. +Assuming we choose the smallest node in the right subtree (the next node in the inorder traversal), the removal process is as shown in Figure 7-21. -1. Find the next node in the "in-order traversal sequence" of the node to be removed, denoted as `tmp`. -2. Replace the value of the node to be removed with `tmp`'s value, and recursively remove the node `tmp` in the tree. +1. Find the next node of the node to be removed in the "inorder traversal sequence," denoted as `tmp`. +2. Replace the value of the node to be removed with the value of `tmp`, and recursively remove node `tmp` in the tree. === "<1>" ![Removing a node in a binary search tree (degree 2)](binary_search_tree.assets/bst_remove_case3_step1.png){ class="animation-figure" } @@ -390,53 +836,53 @@ Assuming we choose the smallest node of the right subtree (the next node in in-o

Figure 7-21   Removing a node in a binary search tree (degree 2)

-The operation of removing a node also uses $O(\log n)$ time, where finding the node to be removed requires $O(\log n)$ time, and obtaining the in-order traversal successor node requires $O(\log n)$ time. Example code is as follows: +The node removal operation also uses $O(\log n)$ time, where finding the node to be removed requires $O(\log n)$ time, and obtaining the inorder successor node requires $O(\log n)$ time. Example code is as follows: === "Python" ```python title="binary_search_tree.py" def remove(self, num: int): - """Remove node""" - # If tree is empty, return + """Delete node""" + # If tree is empty, return directly if self._root is None: return - # Loop find, break after passing leaf nodes + # Loop search, exit after passing leaf node cur, pre = self._root, None while cur is not None: - # Found node to be removed, break loop + # Found node to delete, exit loop if cur.val == num: break pre = cur - # Node to be removed is in cur's right subtree + # Node to delete is in cur's right subtree if cur.val < num: cur = cur.right - # Node to be removed is in cur's left subtree + # Node to delete is in cur's left subtree else: cur = cur.left - # If no node to be removed, return + # If no node to delete, return directly if cur is None: return # Number of child nodes = 0 or 1 if cur.left is None or cur.right is None: - # When the number of child nodes = 0/1, child = null/that child node + # When number of child nodes = 0 / 1, child = null / that child node child = cur.left or cur.right - # Remove node cur + # Delete node cur if cur != self._root: if pre.left == cur: pre.left = child else: pre.right = child else: - # If the removed node is the root, reassign the root + # If deleted node is root node, reassign root node self._root = child # Number of child nodes = 2 else: - # Get the next node in in-order traversal of cur + # Get next node of cur in inorder traversal tmp: TreeNode = cur.right while tmp.left is not None: tmp = tmp.left - # Recursively remove node tmp + # Recursively delete node tmp self.remove(tmp.val) # Replace cur with tmp cur.val = tmp.val @@ -447,38 +893,38 @@ The operation of removing a node also uses $O(\log n)$ time, where finding the n ```cpp title="binary_search_tree.cpp" /* Remove node */ void remove(int num) { - // If tree is empty, return + // If tree is empty, return directly if (root == nullptr) return; TreeNode *cur = root, *pre = nullptr; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != nullptr) { - // Found node to be removed, break loop + // Found node to delete, exit loop if (cur->val == num) break; pre = cur; - // Node to be removed is in cur's right subtree + // Node to delete is in cur's right subtree if (cur->val < num) cur = cur->right; - // Node to be removed is in cur's left subtree + // Node to delete is in cur's left subtree else cur = cur->left; } - // If no node to be removed, return + // If no node to delete, return directly if (cur == nullptr) return; // Number of child nodes = 0 or 1 if (cur->left == nullptr || cur->right == nullptr) { - // When the number of child nodes = 0 / 1, child = nullptr / that child node + // When number of child nodes = 0 / 1, child = nullptr / that child node TreeNode *child = cur->left != nullptr ? cur->left : cur->right; - // Remove node cur + // Delete node cur if (cur != root) { if (pre->left == cur) pre->left = child; else pre->right = child; } else { - // If the removed node is the root, reassign the root + // If deleted node is root node, reassign root node root = child; } // Free memory @@ -486,13 +932,13 @@ The operation of removing a node also uses $O(\log n)$ time, where finding the n } // Number of child nodes = 2 else { - // Get the next node in in-order traversal of cur + // Get next node of cur in inorder traversal TreeNode *tmp = cur->right; while (tmp->left != nullptr) { tmp = tmp->left; } int tmpVal = tmp->val; - // Recursively remove node tmp + // Recursively delete node tmp remove(tmp->val); // Replace cur with tmp cur->val = tmpVal; @@ -505,49 +951,49 @@ The operation of removing a node also uses $O(\log n)$ time, where finding the n ```java title="binary_search_tree.java" /* Remove node */ void remove(int num) { - // If tree is empty, return + // If tree is empty, return directly if (root == null) return; TreeNode cur = root, pre = null; - // Loop find, break after passing leaf nodes + // Loop search, exit after passing leaf node while (cur != null) { - // Found node to be removed, break loop + // Found node to delete, exit loop if (cur.val == num) break; pre = cur; - // Node to be removed is in cur's right subtree + // Node to delete is in cur's right subtree if (cur.val < num) cur = cur.right; - // Node to be removed is in cur's left subtree + // Node to delete is in cur's left subtree else cur = cur.left; } - // If no node to be removed, return + // If no node to delete, return directly if (cur == null) return; // Number of child nodes = 0 or 1 if (cur.left == null || cur.right == null) { - // When the number of child nodes = 0/1, child = null/that child node + // When number of child nodes = 0 / 1, child = null / that child node TreeNode child = cur.left != null ? cur.left : cur.right; - // Remove node cur + // Delete node cur if (cur != root) { if (pre.left == cur) pre.left = child; else pre.right = child; } else { - // If the removed node is the root, reassign the root + // If deleted node is root node, reassign root node root = child; } } // Number of child nodes = 2 else { - // Get the next node in in-order traversal of cur + // Get next node of cur in inorder traversal TreeNode tmp = cur.right; while (tmp.left != null) { tmp = tmp.left; } - // Recursively remove node tmp + // Recursively delete node tmp remove(tmp.val); // Replace cur with tmp cur.val = tmp.val; @@ -558,84 +1004,595 @@ The operation of removing a node also uses $O(\log n)$ time, where finding the n === "C#" ```csharp title="binary_search_tree.cs" - [class]{BinarySearchTree}-[func]{Remove} + /* Remove node */ + void Remove(int num) { + // If tree is empty, return directly + if (root == null) + return; + TreeNode? cur = root, pre = null; + // Loop search, exit after passing leaf node + while (cur != null) { + // Found node to delete, exit loop + if (cur.val == num) + break; + pre = cur; + // Node to delete is in cur's right subtree + if (cur.val < num) + cur = cur.right; + // Node to delete is in cur's left subtree + else + cur = cur.left; + } + // If no node to delete, return directly + if (cur == null) + return; + // Number of child nodes = 0 or 1 + if (cur.left == null || cur.right == null) { + // When number of child nodes = 0 / 1, child = null / that child node + TreeNode? child = cur.left ?? cur.right; + // Delete node cur + if (cur != root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // If deleted node is root node, reassign root node + root = child; + } + } + // Number of child nodes = 2 + else { + // Get next node of cur in inorder traversal + TreeNode? tmp = cur.right; + while (tmp.left != null) { + tmp = tmp.left; + } + // Recursively delete node tmp + Remove(tmp.val!.Value); + // Replace cur with tmp + cur.val = tmp.val; + } + } ``` === "Go" ```go title="binary_search_tree.go" - [class]{binarySearchTree}-[func]{remove} + /* Remove node */ + func (bst *binarySearchTree) remove(num int) { + cur := bst.root + // If tree is empty, return directly + if cur == nil { + return + } + // Node position before the node to be removed + var pre *TreeNode = nil + // Loop search, exit after passing leaf node + for cur != nil { + if cur.Val == num { + break + } + pre = cur + if cur.Val.(int) < num { + // Node to be removed is in right subtree + cur = cur.Right + } else { + // Node to be removed is in left subtree + cur = cur.Left + } + } + // If no node to delete, return directly + if cur == nil { + return + } + // Number of child nodes is 0 or 1 + if cur.Left == nil || cur.Right == nil { + var child *TreeNode = nil + // Get child node of node to be removed + if cur.Left != nil { + child = cur.Left + } else { + child = cur.Right + } + // Delete node cur + if cur != bst.root { + if pre.Left == cur { + pre.Left = child + } else { + pre.Right = child + } + } else { + // If deleted node is root node, reassign root node + bst.root = child + } + // Number of child nodes is 2 + } else { + // Get next node of node cur to be removed in in-order traversal + tmp := cur.Right + for tmp.Left != nil { + tmp = tmp.Left + } + // Recursively delete node tmp + bst.remove(tmp.Val.(int)) + // Replace cur with tmp + cur.Val = tmp.Val + } + } ``` === "Swift" ```swift title="binary_search_tree.swift" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + func remove(num: Int) { + // If tree is empty, return directly + if root == nil { + return + } + var cur = root + var pre: TreeNode? + // Loop search, exit after passing leaf node + while cur != nil { + // Found node to delete, exit loop + if cur!.val == num { + break + } + pre = cur + // Node to delete is in cur's right subtree + if cur!.val < num { + cur = cur?.right + } + // Node to delete is in cur's left subtree + else { + cur = cur?.left + } + } + // If no node to delete, return directly + if cur == nil { + return + } + // Number of child nodes = 0 or 1 + if cur?.left == nil || cur?.right == nil { + // When number of child nodes = 0 / 1, child = null / that child node + let child = cur?.left ?? cur?.right + // Delete node cur + if cur !== root { + if pre?.left === cur { + pre?.left = child + } else { + pre?.right = child + } + } else { + // If deleted node is root node, reassign root node + root = child + } + } + // Number of child nodes = 2 + else { + // Get next node of cur in inorder traversal + var tmp = cur?.right + while tmp?.left != nil { + tmp = tmp?.left + } + // Recursively delete node tmp + remove(num: tmp!.val) + // Replace cur with tmp + cur?.val = tmp!.val + } + } ``` === "JS" ```javascript title="binary_search_tree.js" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + remove(num) { + // If tree is empty, return directly + if (this.root === null) return; + let cur = this.root, + pre = null; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Found node to delete, exit loop + if (cur.val === num) break; + pre = cur; + // Node to delete is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Node to delete is in cur's left subtree + else cur = cur.left; + } + // If no node to delete, return directly + if (cur === null) return; + // Number of child nodes = 0 or 1 + if (cur.left === null || cur.right === null) { + // When number of child nodes = 0 / 1, child = null / that child node + const child = cur.left !== null ? cur.left : cur.right; + // Delete node cur + if (cur !== this.root) { + if (pre.left === cur) pre.left = child; + else pre.right = child; + } else { + // If deleted node is root node, reassign root node + this.root = child; + } + } + // Number of child nodes = 2 + else { + // Get next node of cur in inorder traversal + let tmp = cur.right; + while (tmp.left !== null) { + tmp = tmp.left; + } + // Recursively delete node tmp + this.remove(tmp.val); + // Replace cur with tmp + cur.val = tmp.val; + } + } ``` === "TS" ```typescript title="binary_search_tree.ts" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + remove(num: number): void { + // If tree is empty, return directly + if (this.root === null) return; + let cur: TreeNode | null = this.root, + pre: TreeNode | null = null; + // Loop search, exit after passing leaf node + while (cur !== null) { + // Found node to delete, exit loop + if (cur.val === num) break; + pre = cur; + // Node to delete is in cur's right subtree + if (cur.val < num) cur = cur.right; + // Node to delete is in cur's left subtree + else cur = cur.left; + } + // If no node to delete, return directly + if (cur === null) return; + // Number of child nodes = 0 or 1 + if (cur.left === null || cur.right === null) { + // When number of child nodes = 0 / 1, child = null / that child node + const child: TreeNode | null = + cur.left !== null ? cur.left : cur.right; + // Delete node cur + if (cur !== this.root) { + if (pre!.left === cur) pre!.left = child; + else pre!.right = child; + } else { + // If deleted node is root node, reassign root node + this.root = child; + } + } + // Number of child nodes = 2 + else { + // Get next node of cur in inorder traversal + let tmp: TreeNode | null = cur.right; + while (tmp!.left !== null) { + tmp = tmp!.left; + } + // Recursively delete node tmp + this.remove(tmp!.val); + // Replace cur with tmp + cur.val = tmp!.val; + } + } ``` === "Dart" ```dart title="binary_search_tree.dart" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + void remove(int _num) { + // If tree is empty, return directly + if (_root == null) return; + TreeNode? cur = _root; + TreeNode? pre = null; + // Loop search, exit after passing leaf node + while (cur != null) { + // Found node to delete, exit loop + if (cur.val == _num) break; + pre = cur; + // Node to delete is in cur's right subtree + if (cur.val < _num) + cur = cur.right; + // Node to delete is in cur's left subtree + else + cur = cur.left; + } + // If no node to delete, return directly + if (cur == null) return; + // Number of child nodes = 0 or 1 + if (cur.left == null || cur.right == null) { + // When number of child nodes = 0 / 1, child = null / that child node + TreeNode? child = cur.left ?? cur.right; + // Delete node cur + if (cur != _root) { + if (pre!.left == cur) + pre.left = child; + else + pre.right = child; + } else { + // If deleted node is root node, reassign root node + _root = child; + } + } else { + // Number of child nodes = 2 + // Get next node of cur in inorder traversal + TreeNode? tmp = cur.right; + while (tmp!.left != null) { + tmp = tmp.left; + } + // Recursively delete node tmp + remove(tmp.val); + // Replace cur with tmp + cur.val = tmp.val; + } + } ``` === "Rust" ```rust title="binary_search_tree.rs" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + pub fn remove(&mut self, num: i32) { + // If tree is empty, return directly + if self.root.is_none() { + return; + } + let mut cur = self.root.clone(); + let mut pre = None; + // Loop search, exit after passing leaf node + while let Some(node) = cur.clone() { + match num.cmp(&node.borrow().val) { + // Found node to delete, exit loop + Ordering::Equal => break, + // Node to delete is in cur's right subtree + Ordering::Greater => { + pre = cur.clone(); + cur = node.borrow().right.clone(); + } + // Node to delete is in cur's left subtree + Ordering::Less => { + pre = cur.clone(); + cur = node.borrow().left.clone(); + } + } + } + // If no node to delete, return directly + if cur.is_none() { + return; + } + let cur = cur.unwrap(); + let (left_child, right_child) = (cur.borrow().left.clone(), cur.borrow().right.clone()); + match (left_child.clone(), right_child.clone()) { + // Number of child nodes = 0 or 1 + (None, None) | (Some(_), None) | (None, Some(_)) => { + // When number of child nodes = 0 / 1, child = nullptr / that child node + let child = left_child.or(right_child); + let pre = pre.unwrap(); + // Delete node cur + if !Rc::ptr_eq(&cur, self.root.as_ref().unwrap()) { + let left = pre.borrow().left.clone(); + if left.is_some() && Rc::ptr_eq(left.as_ref().unwrap(), &cur) { + pre.borrow_mut().left = child; + } else { + pre.borrow_mut().right = child; + } + } else { + // If deleted node is root node, reassign root node + self.root = child; + } + } + // Number of child nodes = 2 + (Some(_), Some(_)) => { + // Get next node of cur in inorder traversal + 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 tmp_val = tmp.unwrap().borrow().val; + // Recursively delete node tmp + self.remove(tmp_val); + // Replace cur with tmp + cur.borrow_mut().val = tmp_val; + } + } + } ``` === "C" ```c title="binary_search_tree.c" - [class]{BinarySearchTree}-[func]{removeItem} + /* Remove node */ + // Cannot use remove keyword here due to stdio.h inclusion + void removeItem(BinarySearchTree *bst, int num) { + // If tree is empty, return directly + if (bst->root == NULL) + return; + TreeNode *cur = bst->root, *pre = NULL; + // Loop search, exit after passing leaf node + while (cur != NULL) { + // Found node to delete, exit loop + if (cur->val == num) + break; + pre = cur; + if (cur->val < num) { + // Node to delete is in right subtree of root + cur = cur->right; + } else { + // Node to delete is in left subtree of root + cur = cur->left; + } + } + // If no node to delete, return directly + if (cur == NULL) + return; + // Check if node to delete has children + if (cur->left == NULL || cur->right == NULL) { + /* Number of child nodes = 0 or 1 */ + // When number of child nodes = 0 / 1, child = nullptr / that child node + TreeNode *child = cur->left != NULL ? cur->left : cur->right; + // Delete node cur + if (pre->left == cur) { + pre->left = child; + } else { + pre->right = child; + } + // Free memory + free(cur); + } else { + /* Number of child nodes = 2 */ + // Get next node of cur in inorder traversal + TreeNode *tmp = cur->right; + while (tmp->left != NULL) { + tmp = tmp->left; + } + int tmpVal = tmp->val; + // Recursively delete node tmp + removeItem(bst, tmp->val); + // Replace cur with tmp + cur->val = tmpVal; + } + } ``` === "Kotlin" ```kotlin title="binary_search_tree.kt" - [class]{BinarySearchTree}-[func]{remove} + /* Remove node */ + fun remove(num: Int) { + // If tree is empty, return directly + if (root == null) + return + var cur = root + var pre: TreeNode? = null + // Loop search, exit after passing leaf node + while (cur != null) { + // Found node to delete, exit loop + if (cur._val == num) + break + pre = cur + // Node to delete is in cur's right subtree + cur = if (cur._val < num) + cur.right + // Node to delete is in cur's left subtree + else + cur.left + } + // If no node to delete, return directly + if (cur == null) + return + // Number of child nodes = 0 or 1 + if (cur.left == null || cur.right == null) { + // When number of child nodes = 0 / 1, child = null / that child node + val child = if (cur.left != null) + cur.left + else + cur.right + // Delete node cur + if (cur != root) { + if (pre!!.left == cur) + pre.left = child + else + pre.right = child + } else { + // If deleted node is root node, reassign root node + root = child + } + // Number of child nodes = 2 + } else { + // Get next node of cur in inorder traversal + var tmp = cur.right + while (tmp!!.left != null) { + tmp = tmp.left + } + // Recursively delete node tmp + remove(tmp._val) + // Replace cur with tmp + cur._val = tmp._val + } + } ``` === "Ruby" ```ruby title="binary_search_tree.rb" - [class]{BinarySearchTree}-[func]{remove} + ### Delete node ### + def remove(num) + # If tree is empty, return directly + return if @root.nil? + + # Loop search, exit after passing leaf node + cur, pre = @root, nil + while !cur.nil? + # Found node to delete, exit loop + break if cur.val == num + + pre = cur + # Node to delete is in cur's right subtree + if cur.val < num + cur = cur.right + # Node to delete is in cur's left subtree + else + cur = cur.left + end + end + # If no node to delete, return directly + return if cur.nil? + + # Number of child nodes = 0 or 1 + if cur.left.nil? || cur.right.nil? + # When number of child nodes = 0 / 1, child = null / that child node + child = cur.left || cur.right + # Delete node cur + if cur != @root + if pre.left == cur + pre.left = child + else + pre.right = child + end + else + # If deleted node is root node, reassign root node + @root = child + end + # Number of child nodes = 2 + else + # Get next node of cur in inorder traversal + tmp = cur.right + while !tmp.left.nil? + tmp = tmp.left + end + # Recursively delete node tmp + remove(tmp.val) + # Replace cur with tmp + cur.val = tmp.val + end + end ``` -=== "Zig" +### 4.   Inorder Traversal Is Ordered - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{remove} - ``` +As shown in Figure 7-22, the inorder traversal of a binary tree follows the "left $\rightarrow$ root $\rightarrow$ right" traversal order, while the binary search tree satisfies the "left child node $<$ root node $<$ right child node" size relationship. -### 4.   In-order traversal is ordered +This means that when performing an inorder traversal in a binary search tree, the next smallest node is always traversed first, thus yielding an important property: **The inorder traversal sequence of a binary search tree is ascending**. -As shown in Figure 7-22, the in-order traversal of a binary tree follows the traversal order of "left $\rightarrow$ root $\rightarrow$ right," and a binary search tree satisfies the size relationship of "left child node $<$ root node $<$ right child node." +Using the property of inorder traversal being ascending, we can obtain ordered data in a binary search tree in only $O(n)$ time, without the need for additional sorting operations, which is very efficient. -This means that when performing in-order traversal in a binary search tree, the next smallest node will always be traversed first, thus leading to an important property: **The sequence of in-order traversal in a binary search tree is ascending**. +![Inorder traversal sequence of a binary search tree](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" } -Using the ascending property of in-order traversal, obtaining ordered data in a binary search tree requires only $O(n)$ time, without the need for additional sorting operations, which is very efficient. +

Figure 7-22   Inorder traversal sequence of a binary search tree

-![In-order traversal sequence of a binary search tree](binary_search_tree.assets/bst_inorder_traversal.png){ class="animation-figure" } +## 7.4.2   Efficiency of Binary Search Trees -

Figure 7-22   In-order traversal sequence of a binary search tree

- -## 7.4.2   Efficiency of binary search trees - -Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, the operations on a binary search tree all have logarithmic time complexity, which is stable and efficient. Arrays are more efficient than binary search trees only in scenarios involving frequent additions and infrequent searches or removals. +Given a set of data, we consider using an array or a binary search tree for storage. Observing Table 7-2, all operations in a binary search tree have logarithmic time complexity, providing stable and efficient performance. Arrays are more efficient than binary search trees only in scenarios with high-frequency additions and low-frequency searches and deletions.

Table 7-2   Efficiency comparison between arrays and search trees

@@ -649,7 +1606,7 @@ Given a set of data, we consider using an array or a binary search tree for stor -Ideally, the binary search tree is "balanced," allowing any node can be found within $\log n$ loops. +In the ideal case, a binary search tree is "balanced," such that any node can be found within $\log n$ loop iterations. However, if we continuously insert and remove nodes in a binary search tree, it may degenerate into a linked list as shown in Figure 7-23, where the time complexity of various operations also degrades to $O(n)$. @@ -657,7 +1614,7 @@ However, if we continuously insert and remove nodes in a binary search tree, it

Figure 7-23   Degradation of a binary search tree

-## 7.4.3   Common applications of binary search trees +## 7.4.3   Common Applications of Binary Search Trees - Used as multi-level indexes in systems to implement efficient search, insertion, and removal operations. - Serves as the underlying data structure for certain search algorithms. diff --git a/en/docs/chapter_tree/binary_tree.md b/en/docs/chapter_tree/binary_tree.md index 7a2f55b68..31665ab0b 100644 --- a/en/docs/chapter_tree/binary_tree.md +++ b/en/docs/chapter_tree/binary_tree.md @@ -2,9 +2,9 @@ comments: true --- -# 7.1   Binary tree +# 7.1   Binary Tree -A binary tree is a non-linear data structure that represents the hierarchical relationship between ancestors and descendants and embodies the divide-and-conquer logic of "splitting into two". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. +A binary tree is a non-linear data structure that represents the derivation relationship between "ancestors" and "descendants" and embodies the divide-and-conquer logic of "one divides into two". Similar to a linked list, the basic unit of a binary tree is a node, and each node contains a value, a reference to its left child node, and a reference to its right child node. === "Python" @@ -193,13 +193,16 @@ A binary tree is a non-linear data structure that represents the hierarch === "Ruby" ```ruby title="" + ### Binary tree node class ### + class TreeNode + attr_accessor :val # Node value + attr_accessor :left # Reference to left child node + attr_accessor :right # Reference to right child node - ``` - -=== "Zig" - - ```zig title="" - + def initialize(val) + @val = val + end + end ``` Each node has two references (pointers), pointing respectively to the left-child node and right-child node. This node is called the parent node of these two child nodes. When given a node of a binary tree, we call the tree formed by this node's left child and all nodes below it the left subtree of this node. Similarly, the right subtree can be defined. @@ -210,7 +213,7 @@ Each node has two references (pointers), pointing respectively to the left-ch

Figure 7-1   Parent Node, child Node, subtree

-## 7.1.1   Common terminology of binary trees +## 7.1.1   Common Terminology of Binary Trees The commonly used terminology of binary trees is shown in Figure 7-2. @@ -231,9 +234,9 @@ The commonly used terminology of binary trees is shown in Figure 7-2. Please note that we usually define "height" and "depth" as "the number of edges traversed", but some questions or textbooks may define them as "the number of nodes traversed". In this case, both height and depth need to be incremented by 1. -## 7.1.2   Basic operations of binary trees +## 7.1.2   Basic Operations of Binary Trees -### 1.   Initializing a binary tree +### 1.   Initializing a Binary Tree Similar to a linked list, the initialization of a binary tree involves first creating the nodes and then establishing the references (pointers) between them. @@ -440,20 +443,26 @@ Similar to a linked list, the initialization of a binary tree involves first cre === "Ruby" ```ruby title="binary_tree.rb" - + # Initializing a binary tree + # Initializing nodes + n1 = TreeNode.new(1) + n2 = TreeNode.new(2) + n3 = TreeNode.new(3) + n4 = TreeNode.new(4) + n5 = TreeNode.new(5) + # Linking references (pointers) between nodes + n1.left = n2 + n1.right = n3 + n2.left = n4 + n2.right = n5 ``` -=== "Zig" +??? pythontutor "Code Visualization" - ```zig title="binary_tree.zig" +
+ - ``` - -??? pythontutor "Code visualization" - - https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false - -### 2.   Inserting and removing nodes +### 2.   Inserting and Removing Nodes Similar to a linked list, inserting and removing nodes in a binary tree can be achieved by modifying pointers. Figure 7-3 provides an example. @@ -604,28 +613,29 @@ Similar to a linked list, inserting and removing nodes in a binary tree can be a === "Ruby" ```ruby title="binary_tree.rb" - + # Inserting and removing nodes + _p = TreeNode.new(0) + # Inserting node _p between n1 and n2 + n1.left = _p + _p.left = n2 + # Removing node _p + n1.left = n2 ``` -=== "Zig" +??? pythontutor "Code Visualization" - ```zig title="binary_tree.zig" - - ``` - -??? pythontutor "Code visualization" - - https://pythontutor.com/render.html#code=class%20TreeNode%3A%0A%20%20%20%20%22%22%22%E4%BA%8C%E5%8F%89%E6%A0%91%E8%8A%82%E7%82%B9%E7%B1%BB%22%22%22%0A%20%20%20%20def%20__init__%28self,%20val%3A%20int%29%3A%0A%20%20%20%20%20%20%20%20self.val%3A%20int%20%3D%20val%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%23%20%E8%8A%82%E7%82%B9%E5%80%BC%0A%20%20%20%20%20%20%20%20self.left%3A%20TreeNode%20%7C%20None%20%3D%20None%20%20%23%20%E5%B7%A6%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%20%20%20%20%20%20%20%20self.right%3A%20TreeNode%20%7C%20None%20%3D%20None%20%23%20%E5%8F%B3%E5%AD%90%E8%8A%82%E7%82%B9%E5%BC%95%E7%94%A8%0A%0A%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E4%BA%8C%E5%8F%89%E6%A0%91%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E8%8A%82%E7%82%B9%0A%20%20%20%20n1%20%3D%20TreeNode%28val%3D1%29%0A%20%20%20%20n2%20%3D%20TreeNode%28val%3D2%29%0A%20%20%20%20n3%20%3D%20TreeNode%28val%3D3%29%0A%20%20%20%20n4%20%3D%20TreeNode%28val%3D4%29%0A%20%20%20%20n5%20%3D%20TreeNode%28val%3D5%29%0A%20%20%20%20%23%20%E6%9E%84%E5%BB%BA%E8%8A%82%E7%82%B9%E4%B9%8B%E9%97%B4%E7%9A%84%E5%BC%95%E7%94%A8%EF%BC%88%E6%8C%87%E9%92%88%EF%BC%89%0A%20%20%20%20n1.left%20%3D%20n2%0A%20%20%20%20n1.right%20%3D%20n3%0A%20%20%20%20n2.left%20%3D%20n4%0A%20%20%20%20n2.right%20%3D%20n5%0A%0A%20%20%20%20%23%20%E6%8F%92%E5%85%A5%E4%B8%8E%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%0A%20%20%20%20p%20%3D%20TreeNode%280%29%0A%20%20%20%20%23%20%E5%9C%A8%20n1%20-%3E%20n2%20%E4%B8%AD%E9%97%B4%E6%8F%92%E5%85%A5%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20p%0A%20%20%20%20p.left%20%3D%20n2%0A%20%20%20%20%23%20%E5%88%A0%E9%99%A4%E8%8A%82%E7%82%B9%20P%0A%20%20%20%20n1.left%20%3D%20n2&cumulative=false&curInstr=37&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false +
+ !!! tip It should be noted that inserting nodes may change the original logical structure of the binary tree, while removing nodes typically involves removing the node and all its subtrees. Therefore, in a binary tree, insertion and removal are usually performed through a set of operations to achieve meaningful outcomes. -## 7.1.3   Common types of binary trees +## 7.1.3   Common Types of Binary Trees -### 1.   Perfect binary tree +### 1.   Perfect Binary Tree -As shown in Figure 7-4, in a perfect binary tree, all levels are completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. The total number of nodes can be calculated as $2^{h+1} - 1$, where $h$ is the height of the tree. This exhibits a standard exponential relationship, reflecting the common phenomenon of cell division in nature. +As shown in Figure 7-4, a perfect binary tree has all levels completely filled with nodes. In a perfect binary tree, leaf nodes have a degree of $0$, while all other nodes have a degree of $2$. If the tree height is $h$, the total number of nodes is $2^{h+1} - 1$, exhibiting a standard exponential relationship that reflects the common phenomenon of cell division in nature. !!! tip @@ -635,23 +645,23 @@ As shown in Figure 7-4, in a perfect binary tree, all levels are complete

Figure 7-4   Perfect binary tree

-### 2.   Complete binary tree +### 2.   Complete Binary Tree -As shown in Figure 7-5, a complete binary tree is a binary tree where only the bottom level is possibly not completely filled, and nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree. +As shown in Figure 7-5, a complete binary tree only allows the bottom level to be incompletely filled, and the nodes at the bottom level must be filled continuously from left to right. Note that a perfect binary tree is also a complete binary tree. ![Complete binary tree](binary_tree.assets/complete_binary_tree.png){ class="animation-figure" }

Figure 7-5   Complete binary tree

-### 3.   Full binary tree +### 3.   Full Binary Tree -As shown in Figure 7-6, a full binary tree, except for the leaf nodes, has two child nodes for all other nodes. +As shown in Figure 7-6, in a full binary tree, all nodes except leaf nodes have two child nodes. ![Full binary tree](binary_tree.assets/full_binary_tree.png){ class="animation-figure" }

Figure 7-6   Full binary tree

-### 4.   Balanced binary tree +### 4.   Balanced Binary Tree As shown in Figure 7-7, in a balanced binary tree, the absolute difference between the height of the left and right subtrees of any node does not exceed 1. @@ -659,12 +669,12 @@ As shown in Figure 7-7, in a balanced binary tree, the absolute differenc

Figure 7-7   Balanced binary tree

-## 7.1.4   Degeneration of binary trees +## 7.1.4   Degeneration of Binary Trees -Figure 7-8 shows the ideal and degenerate structures of binary trees. A binary tree becomes a "perfect binary tree" when every level is filled; while it degenerates into a "linked list" when all nodes are biased toward one side. +Figure 7-8 shows the ideal and degenerate structures of binary trees. When every level of a binary tree is filled, it reaches the "perfect binary tree" state; when all nodes are biased toward one side, the binary tree degenerates into a "linked list". -- A perfect binary tree is an ideal scenario where the "divide and conquer" advantage of a binary tree can be fully utilized. -- On the other hand, a linked list represents another extreme where all operations become linear, resulting in a time complexity of $O(n)$. +- A perfect binary tree is the ideal case, fully leveraging the "divide and conquer" advantage of binary trees. +- A linked list represents the other extreme, where all operations become linear operations with time complexity degrading to $O(n)$. ![The Best and Worst Structures of Binary Trees](binary_tree.assets/binary_tree_best_worst_cases.png){ class="animation-figure" } diff --git a/en/docs/chapter_tree/binary_tree_traversal.md b/en/docs/chapter_tree/binary_tree_traversal.md index aeaa3ae92..2b63af245 100755 --- a/en/docs/chapter_tree/binary_tree_traversal.md +++ b/en/docs/chapter_tree/binary_tree_traversal.md @@ -2,25 +2,25 @@ comments: true --- -# 7.2   Binary tree traversal +# 7.2   Binary Tree Traversal From a physical structure perspective, a tree is a data structure based on linked lists. Hence, its traversal method involves accessing nodes one by one through pointers. However, a tree is a non-linear data structure, which makes traversing a tree more complex than traversing a linked list, requiring the assistance of search algorithms. The common traversal methods for binary trees include level-order traversal, pre-order traversal, in-order traversal, and post-order traversal. -## 7.2.1   Level-order traversal +## 7.2.1   Level-Order Traversal As shown in Figure 7-9, level-order traversal traverses the binary tree from top to bottom, layer by layer. Within each level, it visits nodes from left to right. -Level-order traversal is essentially a type of breadth-first traversal, also known as breadth-first search (BFS), which embodies a "circumferentially outward expanding" layer-by-layer traversal method. +Level-order traversal is essentially breadth-first traversal, also known as breadth-first search (BFS), which embodies a "expanding outward circle by circle" layer-by-layer traversal method. ![Level-order traversal of a binary tree](binary_tree_traversal.assets/binary_tree_bfs.png){ class="animation-figure" }

Figure 7-9   Level-order traversal of a binary tree

-### 1.   Code implementation +### 1.   Code Implementation -Breadth-first traversal is usually implemented with the help of a "queue". The queue follows the "first in, first out" rule, while breadth-first traversal follows the "layer-by-layer progression" rule, the underlying ideas of the two are consistent. The implementation code is as follows: +Breadth-first traversal is typically implemented with the help of a "queue". The queue follows the "first in, first out" rule, while breadth-first traversal follows the "layer-by-layer progression" rule; the underlying ideas of the two are consistent. The implementation code is as follows: === "Python" @@ -30,15 +30,15 @@ Breadth-first traversal is usually implemented with the help of a "queue". The q # Initialize queue, add root node queue: deque[TreeNode] = deque() queue.append(root) - # Initialize a list to store the traversal sequence + # Initialize a list to save the traversal sequence res = [] while queue: - node: TreeNode = queue.popleft() # Queue dequeues + node: TreeNode = queue.popleft() # Dequeue res.append(node.val) # Save node value if node.left is not None: - queue.append(node.left) # Left child node enqueues + queue.append(node.left) # Left child node enqueue if node.right is not None: - queue.append(node.right) # Right child node enqueues + queue.append(node.right) # Right child node enqueue return res ``` @@ -50,16 +50,16 @@ Breadth-first traversal is usually implemented with the help of a "queue". The q // Initialize queue, add root node queue queue; queue.push(root); - // Initialize a list to store the traversal sequence + // Initialize a list to save the traversal sequence vector vec; while (!queue.empty()) { TreeNode *node = queue.front(); - queue.pop(); // Queue dequeues + queue.pop(); // Dequeue vec.push_back(node->val); // Save node value if (node->left != nullptr) - queue.push(node->left); // Left child node enqueues + queue.push(node->left); // Left child node enqueue if (node->right != nullptr) - queue.push(node->right); // Right child node enqueues + queue.push(node->right); // Right child node enqueue } return vec; } @@ -73,15 +73,15 @@ Breadth-first traversal is usually implemented with the help of a "queue". The q // Initialize queue, add root node Queue queue = new LinkedList<>(); queue.add(root); - // Initialize a list to store the traversal sequence + // Initialize a list to save the traversal sequence List list = new ArrayList<>(); while (!queue.isEmpty()) { - TreeNode node = queue.poll(); // Queue dequeues + TreeNode node = queue.poll(); // Dequeue list.add(node.val); // Save node value if (node.left != null) - queue.offer(node.left); // Left child node enqueues + queue.offer(node.left); // Left child node enqueue if (node.right != null) - queue.offer(node.right); // Right child node enqueues + queue.offer(node.right); // Right child node enqueue } return list; } @@ -90,85 +90,266 @@ Breadth-first traversal is usually implemented with the help of a "queue". The q === "C#" ```csharp title="binary_tree_bfs.cs" - [class]{binary_tree_bfs}-[func]{LevelOrder} + /* Level-order traversal */ + List LevelOrder(TreeNode root) { + // Initialize queue, add root node + Queue queue = new(); + queue.Enqueue(root); + // Initialize a list to save the traversal sequence + List list = []; + while (queue.Count != 0) { + TreeNode node = queue.Dequeue(); // Dequeue + list.Add(node.val!.Value); // Save node value + if (node.left != null) + queue.Enqueue(node.left); // Left child node enqueue + if (node.right != null) + queue.Enqueue(node.right); // Right child node enqueue + } + return list; + } ``` === "Go" ```go title="binary_tree_bfs.go" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + func levelOrder(root *TreeNode) []any { + // Initialize queue, add root node + queue := list.New() + queue.PushBack(root) + // Initialize a slice to save traversal sequence + nums := make([]any, 0) + for queue.Len() > 0 { + // Dequeue + node := queue.Remove(queue.Front()).(*TreeNode) + // Save node value + nums = append(nums, node.Val) + if node.Left != nil { + // Left child node enqueue + queue.PushBack(node.Left) + } + if node.Right != nil { + // Right child node enqueue + queue.PushBack(node.Right) + } + } + return nums + } ``` === "Swift" ```swift title="binary_tree_bfs.swift" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + func levelOrder(root: TreeNode) -> [Int] { + // Initialize queue, add root node + var queue: [TreeNode] = [root] + // Initialize a list to save the traversal sequence + var list: [Int] = [] + while !queue.isEmpty { + let node = queue.removeFirst() // Dequeue + list.append(node.val) // Save node value + if let left = node.left { + queue.append(left) // Left child node enqueue + } + if let right = node.right { + queue.append(right) // Right child node enqueue + } + } + return list + } ``` === "JS" ```javascript title="binary_tree_bfs.js" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + function levelOrder(root) { + // Initialize queue, add root node + const queue = [root]; + // Initialize a list to save the traversal sequence + const list = []; + while (queue.length) { + let node = queue.shift(); // Dequeue + list.push(node.val); // Save node value + if (node.left) queue.push(node.left); // Left child node enqueue + if (node.right) queue.push(node.right); // Right child node enqueue + } + return list; + } ``` === "TS" ```typescript title="binary_tree_bfs.ts" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + function levelOrder(root: TreeNode | null): number[] { + // Initialize queue, add root node + const queue = [root]; + // Initialize a list to save the traversal sequence + const list: number[] = []; + while (queue.length) { + let node = queue.shift() as TreeNode; // Dequeue + list.push(node.val); // Save node value + if (node.left) { + queue.push(node.left); // Left child node enqueue + } + if (node.right) { + queue.push(node.right); // Right child node enqueue + } + } + return list; + } ``` === "Dart" ```dart title="binary_tree_bfs.dart" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + List levelOrder(TreeNode? root) { + // Initialize queue, add root node + Queue queue = Queue(); + queue.add(root); + // Initialize a list to save the traversal sequence + List res = []; + while (queue.isNotEmpty) { + TreeNode? node = queue.removeFirst(); // Dequeue + res.add(node!.val); // Save node value + if (node.left != null) queue.add(node.left); // Left child node enqueue + if (node.right != null) queue.add(node.right); // Right child node enqueue + } + return res; + } ``` === "Rust" ```rust title="binary_tree_bfs.rs" - [class]{}-[func]{level_order} + /* Level-order traversal */ + fn level_order(root: &Rc>) -> Vec { + // Initialize queue, add root node + let mut que = VecDeque::new(); + que.push_back(root.clone()); + // Initialize a list to save the traversal sequence + let mut vec = Vec::new(); + + while let Some(node) = que.pop_front() { + // Dequeue + vec.push(node.borrow().val); // Save node value + if let Some(left) = node.borrow().left.as_ref() { + que.push_back(left.clone()); // Left child node enqueue + } + if let Some(right) = node.borrow().right.as_ref() { + que.push_back(right.clone()); // Right child node enqueue + }; + } + vec + } ``` === "C" ```c title="binary_tree_bfs.c" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + int *levelOrder(TreeNode *root, int *size) { + /* Auxiliary queue */ + int front, rear; + int index, *arr; + TreeNode *node; + TreeNode **queue; + + /* Auxiliary queue */ + queue = (TreeNode **)malloc(sizeof(TreeNode *) * MAX_SIZE); + // Queue pointer + front = 0, rear = 0; + // Add root node + queue[rear++] = root; + // Initialize a list to save the traversal sequence + /* Auxiliary array */ + arr = (int *)malloc(sizeof(int) * MAX_SIZE); + // Array pointer + index = 0; + while (front < rear) { + // Dequeue + node = queue[front++]; + // Save node value + arr[index++] = node->val; + if (node->left != NULL) { + // Left child node enqueue + queue[rear++] = node->left; + } + if (node->right != NULL) { + // Right child node enqueue + queue[rear++] = node->right; + } + } + // Update array length value + *size = index; + arr = realloc(arr, sizeof(int) * (*size)); + + // Free auxiliary array space + free(queue); + return arr; + } ``` === "Kotlin" ```kotlin title="binary_tree_bfs.kt" - [class]{}-[func]{levelOrder} + /* Level-order traversal */ + fun levelOrder(root: TreeNode?): MutableList { + // Initialize queue, add root node + val queue = LinkedList() + queue.add(root) + // Initialize a list to save the traversal sequence + val list = mutableListOf() + while (queue.isNotEmpty()) { + val node = queue.poll() // Dequeue + list.add(node?._val!!) // Save node value + if (node.left != null) + queue.offer(node.left) // Left child node enqueue + if (node.right != null) + queue.offer(node.right) // Right child node enqueue + } + return list + } ``` === "Ruby" ```ruby title="binary_tree_bfs.rb" - [class]{}-[func]{level_order} + ### Level-order traversal ### + def level_order(root) + # Initialize queue, add root node + queue = [root] + # Initialize a list to save the traversal sequence + res = [] + while !queue.empty? + node = queue.shift # Dequeue + res << node.val # Save node value + queue << node.left unless node.left.nil? # Left child node enqueue + queue << node.right unless node.right.nil? # Right child node enqueue + end + res + end ``` -=== "Zig" +### 2.   Complexity Analysis - ```zig title="binary_tree_bfs.zig" - [class]{}-[func]{levelOrder} - ``` +- **Time complexity is $O(n)$**: All nodes are visited once, using $O(n)$ time, where $n$ is the number of nodes. +- **Space complexity is $O(n)$**: In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue contains at most $(n + 1) / 2$ nodes simultaneously, occupying $O(n)$ space. -### 2.   Complexity analysis +## 7.2.2   Preorder, Inorder, and Postorder Traversal -- **Time complexity is $O(n)$**: All nodes are visited once, taking $O(n)$ time, where $n$ is the number of nodes. -- **Space complexity is $O(n)$**: In the worst case, i.e., a full binary tree, before traversing to the bottom level, the queue can contain at most $(n + 1) / 2$ nodes simultaneously, occupying $O(n)$ space. +Correspondingly, preorder, inorder, and postorder traversals all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a "first go to the end, then backtrack and continue" traversal method. -## 7.2.2   Preorder, in-order, and post-order traversal +Figure 7-10 shows how depth-first traversal works on a binary tree. **Depth-first traversal is like "walking" around the perimeter of the entire binary tree**, encountering three positions at each node, corresponding to preorder, inorder, and postorder traversal. -Correspondingly, pre-order, in-order, and post-order traversal all belong to depth-first traversal, also known as depth-first search (DFS), which embodies a "proceed to the end first, then backtrack and continue" traversal method. +![Preorder, inorder, and postorder traversal of a binary tree](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } -Figure 7-10 shows the working principle of performing a depth-first traversal on a binary tree. **Depth-first traversal is like "walking" around the entire binary tree**, encountering three positions at each node, corresponding to pre-order, in-order, and post-order traversal. +

Figure 7-10   Preorder, inorder, and postorder traversal of a binary tree

-![Preorder, in-order, and post-order traversal of a binary search tree](binary_tree_traversal.assets/binary_tree_dfs.png){ class="animation-figure" } - -

Figure 7-10   Preorder, in-order, and post-order traversal of a binary search tree

- -### 1.   Code implementation +### 1.   Code Implementation Depth-first search is usually implemented based on recursion: @@ -176,7 +357,7 @@ Depth-first search is usually implemented based on recursion: ```python title="binary_tree_dfs.py" def pre_order(root: TreeNode | None): - """Pre-order traversal""" + """Preorder traversal""" if root is None: return # Visit priority: root node -> left subtree -> right subtree @@ -185,7 +366,7 @@ Depth-first search is usually implemented based on recursion: pre_order(root=root.right) def in_order(root: TreeNode | None): - """In-order traversal""" + """Inorder traversal""" if root is None: return # Visit priority: left subtree -> root node -> right subtree @@ -194,7 +375,7 @@ Depth-first search is usually implemented based on recursion: in_order(root=root.right) def post_order(root: TreeNode | None): - """Post-order traversal""" + """Postorder traversal""" if root is None: return # Visit priority: left subtree -> right subtree -> root node @@ -206,7 +387,7 @@ Depth-first search is usually implemented based on recursion: === "C++" ```cpp title="binary_tree_dfs.cpp" - /* Pre-order traversal */ + /* Preorder traversal */ void preOrder(TreeNode *root) { if (root == nullptr) return; @@ -216,7 +397,7 @@ Depth-first search is usually implemented based on recursion: preOrder(root->right); } - /* In-order traversal */ + /* Inorder traversal */ void inOrder(TreeNode *root) { if (root == nullptr) return; @@ -226,7 +407,7 @@ Depth-first search is usually implemented based on recursion: inOrder(root->right); } - /* Post-order traversal */ + /* Postorder traversal */ void postOrder(TreeNode *root) { if (root == nullptr) return; @@ -240,7 +421,7 @@ Depth-first search is usually implemented based on recursion: === "Java" ```java title="binary_tree_dfs.java" - /* Pre-order traversal */ + /* Preorder traversal */ void preOrder(TreeNode root) { if (root == null) return; @@ -250,7 +431,7 @@ Depth-first search is usually implemented based on recursion: preOrder(root.right); } - /* In-order traversal */ + /* Inorder traversal */ void inOrder(TreeNode root) { if (root == null) return; @@ -260,7 +441,7 @@ Depth-first search is usually implemented based on recursion: inOrder(root.right); } - /* Post-order traversal */ + /* Postorder traversal */ void postOrder(TreeNode root) { if (root == null) return; @@ -274,124 +455,376 @@ Depth-first search is usually implemented based on recursion: === "C#" ```csharp title="binary_tree_dfs.cs" - [class]{binary_tree_dfs}-[func]{PreOrder} + /* Preorder traversal */ + void PreOrder(TreeNode? root) { + if (root == null) return; + // Visit priority: root node -> left subtree -> right subtree + list.Add(root.val!.Value); + PreOrder(root.left); + PreOrder(root.right); + } - [class]{binary_tree_dfs}-[func]{InOrder} + /* Inorder traversal */ + void InOrder(TreeNode? root) { + if (root == null) return; + // Visit priority: left subtree -> root node -> right subtree + InOrder(root.left); + list.Add(root.val!.Value); + InOrder(root.right); + } - [class]{binary_tree_dfs}-[func]{PostOrder} + /* Postorder traversal */ + void PostOrder(TreeNode? root) { + if (root == null) return; + // Visit priority: left subtree -> right subtree -> root node + PostOrder(root.left); + PostOrder(root.right); + list.Add(root.val!.Value); + } ``` === "Go" ```go title="binary_tree_dfs.go" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + func preOrder(node *TreeNode) { + if node == nil { + return + } + // Visit priority: root node -> left subtree -> right subtree + nums = append(nums, node.Val) + preOrder(node.Left) + preOrder(node.Right) + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + func inOrder(node *TreeNode) { + if node == nil { + return + } + // Visit priority: left subtree -> root node -> right subtree + inOrder(node.Left) + nums = append(nums, node.Val) + inOrder(node.Right) + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + func postOrder(node *TreeNode) { + if node == nil { + return + } + // Visit priority: left subtree -> right subtree -> root node + postOrder(node.Left) + postOrder(node.Right) + nums = append(nums, node.Val) + } ``` === "Swift" ```swift title="binary_tree_dfs.swift" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + func preOrder(root: TreeNode?) { + guard let root = root else { + return + } + // Visit priority: root node -> left subtree -> right subtree + list.append(root.val) + preOrder(root: root.left) + preOrder(root: root.right) + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + func inOrder(root: TreeNode?) { + guard let root = root else { + return + } + // Visit priority: left subtree -> root node -> right subtree + inOrder(root: root.left) + list.append(root.val) + inOrder(root: root.right) + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + func postOrder(root: TreeNode?) { + guard let root = root else { + return + } + // Visit priority: left subtree -> right subtree -> root node + postOrder(root: root.left) + postOrder(root: root.right) + list.append(root.val) + } ``` === "JS" ```javascript title="binary_tree_dfs.js" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + function preOrder(root) { + if (root === null) return; + // Visit priority: root node -> left subtree -> right subtree + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + function inOrder(root) { + if (root === null) return; + // Visit priority: left subtree -> root node -> right subtree + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + function postOrder(root) { + if (root === null) return; + // Visit priority: left subtree -> right subtree -> root node + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } ``` === "TS" ```typescript title="binary_tree_dfs.ts" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + function preOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // Visit priority: root node -> left subtree -> right subtree + list.push(root.val); + preOrder(root.left); + preOrder(root.right); + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + function inOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // Visit priority: left subtree -> root node -> right subtree + inOrder(root.left); + list.push(root.val); + inOrder(root.right); + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + function postOrder(root: TreeNode | null): void { + if (root === null) { + return; + } + // Visit priority: left subtree -> right subtree -> root node + postOrder(root.left); + postOrder(root.right); + list.push(root.val); + } ``` === "Dart" ```dart title="binary_tree_dfs.dart" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + void preOrder(TreeNode? node) { + if (node == null) return; + // Visit priority: root node -> left subtree -> right subtree + list.add(node.val); + preOrder(node.left); + preOrder(node.right); + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + void inOrder(TreeNode? node) { + if (node == null) return; + // Visit priority: left subtree -> root node -> right subtree + inOrder(node.left); + list.add(node.val); + inOrder(node.right); + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + void postOrder(TreeNode? node) { + if (node == null) return; + // Visit priority: left subtree -> right subtree -> root node + postOrder(node.left); + postOrder(node.right); + list.add(node.val); + } ``` === "Rust" ```rust title="binary_tree_dfs.rs" - [class]{}-[func]{pre_order} + /* Preorder traversal */ + fn pre_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; - [class]{}-[func]{in_order} + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // Visit priority: root node -> left subtree -> right subtree + let node = node.borrow(); + res.push(node.val); + dfs(node.left.as_ref(), res); + dfs(node.right.as_ref(), res); + } + } + dfs(root, &mut result); - [class]{}-[func]{post_order} + result + } + + /* Inorder traversal */ + fn in_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // Visit priority: left subtree -> root node -> right subtree + let node = node.borrow(); + dfs(node.left.as_ref(), res); + res.push(node.val); + dfs(node.right.as_ref(), res); + } + } + dfs(root, &mut result); + + result + } + + /* Postorder traversal */ + fn post_order(root: Option<&Rc>>) -> Vec { + let mut result = vec![]; + + fn dfs(root: Option<&Rc>>, res: &mut Vec) { + if let Some(node) = root { + // Visit priority: left subtree -> right subtree -> root node + let node = node.borrow(); + dfs(node.left.as_ref(), res); + dfs(node.right.as_ref(), res); + res.push(node.val); + } + } + + dfs(root, &mut result); + + result + } ``` === "C" ```c title="binary_tree_dfs.c" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + void preOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // Visit priority: root node -> left subtree -> right subtree + arr[(*size)++] = root->val; + preOrder(root->left, size); + preOrder(root->right, size); + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + void inOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // Visit priority: left subtree -> root node -> right subtree + inOrder(root->left, size); + arr[(*size)++] = root->val; + inOrder(root->right, size); + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + void postOrder(TreeNode *root, int *size) { + if (root == NULL) + return; + // Visit priority: left subtree -> right subtree -> root node + postOrder(root->left, size); + postOrder(root->right, size); + arr[(*size)++] = root->val; + } ``` === "Kotlin" ```kotlin title="binary_tree_dfs.kt" - [class]{}-[func]{preOrder} + /* Preorder traversal */ + fun preOrder(root: TreeNode?) { + if (root == null) return + // Visit priority: root node -> left subtree -> right subtree + list.add(root._val) + preOrder(root.left) + preOrder(root.right) + } - [class]{}-[func]{inOrder} + /* Inorder traversal */ + fun inOrder(root: TreeNode?) { + if (root == null) return + // Visit priority: left subtree -> root node -> right subtree + inOrder(root.left) + list.add(root._val) + inOrder(root.right) + } - [class]{}-[func]{postOrder} + /* Postorder traversal */ + fun postOrder(root: TreeNode?) { + if (root == null) return + // Visit priority: left subtree -> right subtree -> root node + postOrder(root.left) + postOrder(root.right) + list.add(root._val) + } ``` === "Ruby" ```ruby title="binary_tree_dfs.rb" - [class]{}-[func]{pre_order} + ### Pre-order traversal ### + def pre_order(root) + return if root.nil? - [class]{}-[func]{in_order} + # Visit priority: root node -> left subtree -> right subtree + $res << root.val + pre_order(root.left) + pre_order(root.right) + end - [class]{}-[func]{post_order} - ``` + ### In-order traversal ### + def in_order(root) + return if root.nil? -=== "Zig" + # Visit priority: left subtree -> root node -> right subtree + in_order(root.left) + $res << root.val + in_order(root.right) + end - ```zig title="binary_tree_dfs.zig" - [class]{}-[func]{preOrder} + ### Post-order traversal ### + def post_order(root) + return if root.nil? - [class]{}-[func]{inOrder} - - [class]{}-[func]{postOrder} + # Visit priority: left subtree -> right subtree -> root node + post_order(root.left) + post_order(root.right) + $res << root.val + end ``` !!! tip Depth-first search can also be implemented based on iteration, interested readers can study this on their own. -Figure 7-11 shows the recursive process of pre-order traversal of a binary tree, which can be divided into two opposite parts: "recursion" and "return". +Figure 7-11 shows the recursive process of preorder traversal of a binary tree, which can be divided into two opposite parts: "recursion" and "return". -1. "Recursion" means starting a new method, the program accesses the next node in this process. -2. "Return" means the function returns, indicating the current node has been fully accessed. +1. "Recursion" means opening a new method, where the program accesses the next node in this process. +2. "Return" means the function returns, indicating that the current node has been fully visited. === "<1>" - ![The recursive process of pre-order traversal](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } + ![The recursive process of preorder traversal](binary_tree_traversal.assets/preorder_step1.png){ class="animation-figure" } === "<2>" ![preorder_step2](binary_tree_traversal.assets/preorder_step2.png){ class="animation-figure" } @@ -423,9 +856,9 @@ Figure 7-11 shows the recursive process of pre-order traversal of a binary tree, === "<11>" ![preorder_step11](binary_tree_traversal.assets/preorder_step11.png){ class="animation-figure" } -

Figure 7-11   The recursive process of pre-order traversal

+

Figure 7-11   The recursive process of preorder traversal

-### 2.   Complexity analysis +### 2.   Complexity Analysis - **Time complexity is $O(n)$**: All nodes are visited once, using $O(n)$ time. -- **Space complexity is $O(n)$**: In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches $n$, the system occupies $O(n)$ stack frame space. +- **Space complexity is $O(n)$**: In the worst case, i.e., the tree degenerates into a linked list, the recursion depth reaches $n$, and the system occupies $O(n)$ stack frame space. diff --git a/en/docs/chapter_tree/index.md b/en/docs/chapter_tree/index.md index 4e22d1d78..e7f6b8cda 100644 --- a/en/docs/chapter_tree/index.md +++ b/en/docs/chapter_tree/index.md @@ -9,14 +9,14 @@ icon: material/graph-outline !!! abstract - The towering tree exudes a vibrant essence, boasting profound roots and abundant foliage, yet its branches are sparsely scattered, creating an ethereal aura. - - It shows us the vivid form of divide-and-conquer in data. + Towering trees are full of vitality, with deep roots and lush leaves, spreading branches and flourishing. + + They show us the vivid form of divide and conquer in data. ## Chapter contents -- [7.1   Binary tree](binary_tree.md) -- [7.2   Binary tree traversal](binary_tree_traversal.md) -- [7.3   Array Representation of tree](array_representation_of_tree.md) -- [7.4   Binary Search tree](binary_search_tree.md) -- [7.5   AVL tree *](avl_tree.md) +- [7.1   Binary Tree](binary_tree.md) +- [7.2   Binary Tree Traversal](binary_tree_traversal.md) +- [7.3   Array Representation of Tree](array_representation_of_tree.md) +- [7.4   Binary Search Tree](binary_search_tree.md) +- [7.5   AVL Tree *](avl_tree.md) - [7.6   Summary](summary.md) diff --git a/en/docs/chapter_tree/summary.md b/en/docs/chapter_tree/summary.md index 0b776427c..d1e55dd8b 100644 --- a/en/docs/chapter_tree/summary.md +++ b/en/docs/chapter_tree/summary.md @@ -4,19 +4,19 @@ comments: true # 7.6   Summary -### 1.   Key review +### 1.   Key Review -- A binary tree is a non-linear data structure that reflects the "divide and conquer" logic of splitting one into two. Each binary tree node contains a value and two pointers, which point to its left and right child nodes, respectively. -- For a node in a binary tree, its left (right) child node and the tree formed below it are collectively called the node's left (right) subtree. -- Terms related to binary trees include root node, leaf node, level, degree, edge, height, and depth. -- The operations of initializing a binary tree, inserting nodes, and removing nodes are similar to those of linked list operations. -- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree represents the ideal state, while the linked list is the worst state after degradation. -- A binary tree can be represented using an array by arranging the node values and empty slots in a level-order traversal sequence and implementing pointers based on the index mapping relationship between parent nodes and child nodes. -- The level-order traversal of a binary tree is a breadth-first search method, which reflects a layer-by-layer traversal manner of "expanding circle by circle." It is usually implemented using a queue. -- Pre-order, in-order, and post-order traversals are all depth-first search methods, reflecting the traversal manner of "going to the end first, then backtracking to continue." They are usually implemented using recursion. -- A binary search tree is an efficient data structure for element searching, with the time complexity of search, insert, and remove operations all being $O(\log n)$. When a binary search tree degrades into a linked list, these time complexities deteriorate to $O(n)$. -- An AVL tree, also known as a balanced binary search tree, ensures that the tree remains balanced after continuous node insertions and removals through rotation operations. -- Rotation operations in an AVL tree include right rotation, left rotation, right-left rotation, and left-right rotation. After node insertion or removal, the AVL tree rebalances itself by performing these rotations in a bottom-up manner. +- A binary tree is a non-linear data structure that embodies the divide-and-conquer logic of "one divides into two". Each binary tree node contains a value and two pointers, which respectively point to its left and right child nodes. +- For a certain node in a binary tree, the tree formed by its left (right) child node and all nodes below is called the left (right) subtree of that node. +- Related terminology of binary trees includes root node, leaf node, level, degree, edge, height, and depth. +- The initialization, node insertion, and node removal operations of binary trees are similar to those of linked lists. +- Common types of binary trees include perfect binary trees, complete binary trees, full binary trees, and balanced binary trees. The perfect binary tree is the ideal state, while the linked list is the worst state after degradation. +- A binary tree can be represented using an array by arranging node values and empty slots in level-order traversal sequence, and implementing pointers based on the index mapping relationship between parent and child nodes. +- Level-order traversal of a binary tree is a breadth-first search method, embodying a layer-by-layer traversal approach of "expanding outward circle by circle", typically implemented using a queue. +- Preorder, inorder, and postorder traversals all belong to depth-first search, embodying a traversal approach of "first go to the end, then backtrack and continue", typically implemented using recursion. +- A binary search tree is an efficient data structure for element searching, with search, insertion, and removal operations all having time complexity of $O(\log n)$. When a binary search tree degenerates into a linked list, all time complexities degrade to $O(n)$. +- An AVL tree, also known as a balanced binary search tree, ensures the tree remains balanced after continuous node insertions and removals through rotation operations. +- Rotation operations in AVL trees include right rotation, left rotation, left rotation then right rotation, and right rotation then left rotation. After inserting or removing nodes, AVL trees perform rotation operations from bottom to top to restore the tree to balance. ### 2.   Q & A @@ -28,21 +28,21 @@ Yes, because height and depth are typically defined as "the number of edges pass Taking the binary search tree as an example, the operation of removing a node needs to be handled in three different scenarios, each requiring multiple steps of node operations. -**Q**: Why are there three sequences: pre-order, in-order, and post-order for DFS traversal of a binary tree, and what are their uses? +**Q**: Why does DFS traversal of binary trees have three orders: preorder, inorder, and postorder, and what are their uses? -Similar to sequential and reverse traversal of arrays, pre-order, in-order, and post-order traversals are three methods of traversing a binary tree, allowing us to obtain a traversal result in a specific order. For example, in a binary search tree, since the node sizes satisfy `left child node value < root node value < right child node value`, we can obtain an ordered node sequence by traversing the tree in the "left $\rightarrow$ root $\rightarrow$ right" priority. +Similar to forward and reverse traversal of arrays, preorder, inorder, and postorder traversals are three methods of binary tree traversal that allow us to obtain a traversal result in a specific order. For example, in a binary search tree, since nodes satisfy the relationship `left child node value < root node value < right child node value`, we only need to traverse the tree with the priority of "left $\rightarrow$ root $\rightarrow$ right" to obtain an ordered node sequence. -**Q**: In a right rotation operation that deals with the relationship between the imbalance nodes `node`, `child`, `grand_child`, isn't the connection between `node` and its parent node and the original link of `node` lost after the right rotation? +**Q**: In a right rotation operation handling the relationship between unbalanced nodes `node`, `child`, and `grand_child`, doesn't the connection between `node` and its parent node get lost after the right rotation? -We need to view this problem from a recursive perspective. The `right_rotate(root)` operation passes the root node of the subtree and eventually returns the root node of the rotated subtree with `return child`. The connection between the subtree's root node and its parent node is established after this function returns, which is outside the scope of the right rotation operation's maintenance. +We need to view this problem from a recursive perspective. The right rotation operation `right_rotate(root)` passes in the root node of the subtree and eventually returns the root node of the subtree after rotation with `return child`. The connection between the subtree's root node and its parent node is completed after the function returns, which is not within the maintenance scope of the right rotation operation. **Q**: In C++, functions are divided into `private` and `public` sections. What considerations are there for this? Why are the `height()` function and the `updateHeight()` function placed in `public` and `private`, respectively? -It depends on the scope of the method's use. If a method is only used within the class, then it is designed to be `private`. For example, it makes no sense for users to call `updateHeight()` on their own, as it is just a step in the insertion or removal operations. However, `height()` is for accessing node height, similar to `vector.size()`, thus it is set to `public` for use. +It mainly depends on the method's usage scope. If a method is only used within the class, then it is designed as `private`. For example, calling `updateHeight()` alone by the user makes no sense, as it is only a step in insertion or removal operations. However, `height()` is used to access node height, similar to `vector.size()`, so it is set to `public` for ease of use. **Q**: How do you build a binary search tree from a set of input data? Is the choice of root node very important? -Yes, the method for building the tree is provided in the `build_tree()` method in the binary search tree code. As for the choice of the root node, we usually sort the input data and then select the middle element as the root node, recursively building the left and right subtrees. This approach maximizes the balance of the tree. +Yes, the method for building a tree is provided in the `build_tree()` method in the binary search tree code. As for the choice of root node, we typically sort the input data, then select the middle element as the root node, and recursively build the left and right subtrees. This approach maximizes the tree's balance. **Q**: In Java, do you always have to use the `equals()` method for string comparison? @@ -51,7 +51,7 @@ In Java, for primitive data types, `==` is used to compare whether the values of - `==`: Used to compare whether two variables point to the same object, i.e., whether their positions in memory are the same. - `equals()`: Used to compare whether the values of two objects are equal. -Therefore, to compare values, we should use `equals()`. However, strings initialized with `String a = "hi"; String b = "hi";` are stored in the string constant pool and point to the same object, so `a == b` can also be used to compare the contents of two strings. +Therefore, if we want to compare values, we should use `equals()`. However, strings initialized via `String a = "hi"; String b = "hi";` are stored in the string constant pool and point to the same object, so `a == b` can also be used to compare the contents of the two strings. **Q**: Before reaching the bottom level, is the number of nodes in the queue $2^h$ in breadth-first traversal? diff --git a/en/docs/index.html b/en/docs/index.html index 0a18da586..181fa0d27 100644 --- a/en/docs/index.html +++ b/en/docs/index.html @@ -26,7 +26,7 @@ - Hash table + Hashing diff --git a/ja/docs/chapter_array_and_linkedlist/array.md b/ja/docs/chapter_array_and_linkedlist/array.md index 8e8b0b886..ad3563da8 100644 --- a/ja/docs/chapter_array_and_linkedlist/array.md +++ b/ja/docs/chapter_array_and_linkedlist/array.md @@ -122,14 +122,6 @@ comments: true ``` -=== "Zig" - - ```zig title="array.zig" - // 配列を初期化 - var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } - var nums = [_]i32{ 1, 3, 2, 5, 4 }; - ``` - ### 2.   要素へのアクセス 配列内の要素は連続したメモリ空間に格納されるため、各要素のメモリアドレスを計算することが簡単になります。以下の図に示されている公式は、配列のメモリアドレス(特に、最初の要素のアドレス)と要素のインデックスを利用して、要素のメモリアドレスを決定するのに役立ちます。この計算により、目的の要素への直接アクセスが合理化されます。 @@ -240,12 +232,6 @@ comments: true [class]{}-[func]{random_access} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{randomAccess} - ``` - ### 3.   要素の挿入 配列要素はメモリ内で密に詰まっており、それらの間に追加データを収容するための空間はありません。以下の図に示すように、配列の中央に要素を挿入するには、後続のすべての要素を1つずつ後ろにシフトして、新しい要素のための空間を作る必要があります。 @@ -356,12 +342,6 @@ comments: true [class]{}-[func]{insert} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{insert} - ``` - ### 4.   要素の削除 同様に、以下の図に示すように、インデックス$i$の要素を削除するには、インデックス$i$に続くすべての要素を1つずつ前に移動する必要があります。 @@ -466,12 +446,6 @@ comments: true [class]{}-[func]{remove} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{remove} - ``` - 要約すると、配列の挿入と削除操作には以下の欠点があります: - **高い時間計算量**:配列の挿入と削除の両方の平均時間計算量は$O(n)$で、ここで$n$は配列の長さです。 @@ -590,12 +564,6 @@ comments: true [class]{}-[func]{traverse} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{traverse} - ``` - ### 6.   要素の検索 配列内の特定の要素を見つけることは、配列を反復し、各要素をチェックして目的の値と一致するかどうかを決定することを含みます。 @@ -699,12 +667,6 @@ comments: true [class]{}-[func]{find} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{find} - ``` - ### 7.   配列の拡張 複雑なシステム環境では、安全な容量拡張のために配列の後にメモリ空間の可用性を確保することが困難になります。その結果、ほとんどのプログラミング言語では、**配列の長さは不変**です。 @@ -819,12 +781,6 @@ comments: true [class]{}-[func]{extend} ``` -=== "Zig" - - ```zig title="array.zig" - [class]{}-[func]{extend} - ``` - ## 4.1.2   配列の利点と制限 配列は連続したメモリ空間に格納され、同じ型の要素で構成されます。このアプローチは、システムがデータ構造操作の効率を最適化するために活用できる実質的な事前情報を提供します。 diff --git a/ja/docs/chapter_array_and_linkedlist/linked_list.md b/ja/docs/chapter_array_and_linkedlist/linked_list.md index c81da9c36..78d9b5785 100644 --- a/ja/docs/chapter_array_and_linkedlist/linked_list.md +++ b/ja/docs/chapter_array_and_linkedlist/linked_list.md @@ -171,26 +171,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - // 連結リストノードクラス - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // ノード値 - next: ?*Self = null, // 次のノードへのポインタ - - // コンストラクタ - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - } - }; - } - ``` - ## 4.2.1   連結リストの一般的な操作 ### 1.   連結リストの初期化 @@ -391,23 +371,6 @@ comments: true ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 連結リストを初期化 - // 各ノードを初期化 - var n0 = inc.ListNode(i32){.val = 1}; - var n1 = inc.ListNode(i32){.val = 3}; - var n2 = inc.ListNode(i32){.val = 2}; - var n3 = inc.ListNode(i32){.val = 5}; - var n4 = inc.ListNode(i32){.val = 4}; - // ノード間の参照を構築 - n0.next = &n1; - n1.next = &n2; - n2.next = &n3; - n3.next = &n4; - ``` - 配列全体は1つの変数です。例えば、配列`nums`には`nums[0]`、`nums[1]`などの要素が含まれますが、連結リストは複数の異なるノードオブジェクトで構成されています。**通常、連結リストはそのヘッドノードで参照されます**。例えば、前のコードスニペットの連結リストは`n0`として参照されます。 ### 2.   ノードの挿入 @@ -512,12 +475,6 @@ comments: true [class]{}-[func]{insert} ``` -=== "Zig" - - ```zig title="linked_list.zig" - [class]{}-[func]{insert} - ``` - ### 3.   ノードの削除 下図に示すように、連結リストからノードを削除することも非常に簡単で、**1つのノードの参照(ポインタ)を変更するだけです**。 @@ -631,12 +588,6 @@ comments: true [class]{}-[func]{remove} ``` -=== "Zig" - - ```zig title="linked_list.zig" - [class]{}-[func]{remove} - ``` - ### 4.   ノードへのアクセス **連結リストでのノードへのアクセスは効率が悪いです**。前述したように、配列の任意の要素には$O(1)$時間でアクセスできます。対照的に、連結リストでは、プログラムはヘッドノードから開始して目的のノードが見つかるまで順次ノードを巡回する必要があります。つまり、連結リストの$i$番目のノードにアクセスするには、プログラムは$i - 1$個のノードを反復処理する必要があり、時間計算量は$O(n)$になります。 @@ -741,12 +692,6 @@ comments: true [class]{}-[func]{access} ``` -=== "Zig" - - ```zig title="linked_list.zig" - [class]{}-[func]{access} - ``` - ### 5.   ノードの検索 連結リストを巡回して、値が`target`に一致するノードを見つけ、連結リスト内でのそのノードのインデックスを出力します。この手順も線形検索の例です。対応するコードは以下のとおりです: @@ -857,12 +802,6 @@ comments: true [class]{}-[func]{find} ``` -=== "Zig" - - ```zig title="linked_list.zig" - [class]{}-[func]{find} - ``` - ## 4.2.2   配列 vs. 連結リスト 下表は配列と連結リストの特性をまとめ、様々な操作における効率も比較しています。それぞれが対照的な格納戦略を使用するため、それぞれの特性と操作効率は明確に対比されています。 @@ -1065,28 +1004,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - // 双方向連結リストノードクラス - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // ノード値 - next: ?*Self = null, // 後続ノードへのポインタ - prev: ?*Self = null, // 前任ノードへのポインタ - - // コンストラクタ - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - self.prev = null; - } - }; - } - ``` - ![連結リストの一般的な種類](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" }

図 4-8   連結リストの一般的な種類

diff --git a/ja/docs/chapter_array_and_linkedlist/list.md b/ja/docs/chapter_array_and_linkedlist/list.md index a3ea7e1ee..df293613d 100644 --- a/ja/docs/chapter_array_and_linkedlist/list.md +++ b/ja/docs/chapter_array_and_linkedlist/list.md @@ -136,15 +136,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // リストを初期化 - var nums = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums.deinit(); - try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); - ``` - ### 2.   要素へのアクセス リストは本質的に配列であるため、$O(1)$時間で要素にアクセスし更新することができ、非常に効率的です。 @@ -260,16 +251,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // 要素にアクセス - var num = nums.items[1]; // インデックス1の要素にアクセス - - // 要素を更新 - nums.items[1] = 0; // インデックス1の要素を0に更新 - ``` - ### 3.   要素の挿入と削除 配列と比較して、リストは要素の追加と削除においてより柔軟性を提供します。リストの末尾への要素追加は$O(1)$操作ですが、リストの他の場所での要素の挿入と削除の効率は配列と同じままで、時間計算量は$O(n)$です。 @@ -486,26 +467,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // リストをクリア - nums.clearRetainingCapacity(); - - // 末尾に要素を追加 - try nums.append(1); - try nums.append(3); - try nums.append(2); - try nums.append(5); - try nums.append(4); - - // 中間に要素を挿入 - try nums.insert(3, 6); // インデックス3に数値6を挿入 - - // 要素を削除 - _ = nums.orderedRemove(3); // インデックス3の要素を削除 - ``` - ### 4.   リストの反復 配列と同様に、リストはインデックスを使用して反復することも、各要素を直接反復することもできます。 @@ -678,23 +639,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // インデックスでリストを反復 - var count: i32 = 0; - var i: i32 = 0; - while (i < nums.items.len) : (i += 1) { - count += nums[i]; - } - - // リスト要素を直接反復 - count = 0; - for (nums.items) |num| { - count += num; - } - ``` - ### 5.   リストの連結 新しいリスト`nums1`が与えられたとき、それを元のリストの末尾に追加できます。 @@ -792,16 +736,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // 2つのリストを連結 - var nums1 = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums1.deinit(); - try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); - try nums.insertSlice(nums.items.len, nums1.items); // nums1をnumsの末尾に連結 - ``` - ### 6.   リストのソート リストがソートされると、「二分探索」や「双ポインタ」アルゴリズムなど、配列関連のアルゴリズム問題でよく使用されるアルゴリズムを使用できます。 @@ -888,13 +822,6 @@ comments: true ``` -=== "Zig" - - ```zig title="list.zig" - // リストをソート - std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32)); - ``` - ## 4.3.2   リストの実装 多くのプログラミング言語には、Java、C++、Pythonなどを含む組み込みリストが付属しています。それらの実装は、初期容量や拡張係数などの様々なパラメータを慎重に考慮した設定で、複雑になりがちです。興味のある読者は、さらなる学習のためにソースコードを調べることができます。 @@ -1262,9 +1189,3 @@ comments: true ```ruby title="my_list.rb" [class]{MyList}-[func]{} ``` - -=== "Zig" - - ```zig title="my_list.zig" - [class]{MyList}-[func]{} - ``` diff --git a/ja/docs/chapter_backtracking/backtracking_algorithm.md b/ja/docs/chapter_backtracking/backtracking_algorithm.md index d055b0ddf..8dd9848ef 100644 --- a/ja/docs/chapter_backtracking/backtracking_algorithm.md +++ b/ja/docs/chapter_backtracking/backtracking_algorithm.md @@ -122,12 +122,6 @@ comments: true [class]{}-[func]{pre_order} ``` -=== "Zig" - - ```zig title="preorder_traversal_i_compact.zig" - [class]{}-[func]{preOrder} - ``` - ![前順走査でのノード検索](backtracking_algorithm.assets/preorder_find_nodes.png){ class="animation-figure" }

図 13-1   前順走査でのノード検索

@@ -266,12 +260,6 @@ comments: true [class]{}-[func]{pre_order} ``` -=== "Zig" - - ```zig title="preorder_traversal_ii_compact.zig" - [class]{}-[func]{preOrder} - ``` - 各「試行」で、現在のノードを `path` に追加することでパスを記録します。「後退」が必要なときはいつでも、`path` からノードをポップして**この失敗した試行前の状態を復元します**。 以下の図に示すプロセスを観察することで、**試行は「前進」のようで、後退は「元に戻す」のようです**。後者のペアは、対応するものに対する逆操作と見なすことができます。 @@ -444,12 +432,6 @@ comments: true [class]{}-[func]{pre_order} ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_compact.zig" - [class]{}-[func]{preOrder} - ``` - 「剪定」は非常に生き生きとした名詞です。以下の図に示すように、検索プロセスで、**制約を満たさない検索分岐を「切り取り」ます**。さらなる不要な試行を避け、検索効率を向上させます。 ![制約に基づく剪定](backtracking_algorithm.assets/preorder_find_constrained_paths.png){ class="animation-figure" } @@ -796,12 +778,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 次に、フレームワークコードに基づいて例題 3 を解きます。状態 `state` はノードの走査経路を表し、選択肢 `choices` は現在ノードの左子ノードと右子ノード、結果 `res` は経路リストです: === "Python" @@ -1108,22 +1084,6 @@ comments: true [class]{}-[func]{backtrack} ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_template.zig" - [class]{}-[func]{isSolution} - - [class]{}-[func]{recordSolution} - - [class]{}-[func]{isValid} - - [class]{}-[func]{makeChoice} - - [class]{}-[func]{undoChoice} - - [class]{}-[func]{backtrack} - ``` - 問題文の意味に従い、値が $7$ のノードを見つけた後も探索を続ける必要があります。**したがって、解を記録した後の `return` 文を削除する必要があります**。次の図は、`return` 文を保持する場合と削除する場合の探索過程の比較です。 ![returnを保持する場合と削除する場合の探索過程の比較](backtracking_algorithm.assets/backtrack_remove_return_or_not.png){ class="animation-figure" } diff --git a/ja/docs/chapter_backtracking/n_queens_problem.md b/ja/docs/chapter_backtracking/n_queens_problem.md index e37b97d39..a1131d198 100644 --- a/ja/docs/chapter_backtracking/n_queens_problem.md +++ b/ja/docs/chapter_backtracking/n_queens_problem.md @@ -283,14 +283,6 @@ $n$ 次元の正方行列では、$row - col$ の範囲は $[-n + 1, n - 1]$ で [class]{}-[func]{n_queens} ``` -=== "Zig" - - ```zig title="n_queens.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - $n$ 個のクイーンを行ごとに配置し、列の制約を考慮して、最初の行から最後の行まで、$n$、$n-1$、$\dots$、$2$、$1$ の選択肢があり、$O(n!)$ 時間を使用します。解を記録する際、行列 `state` をコピーして `res` に追加する必要があり、コピー操作は $O(n^2)$ 時間を使用します。したがって、**全体の時間計算量は $O(n! \cdot n^2)$ です**。実際には、対角線制約に基づく剪定により検索空間を大幅に削減できるため、多くの場合、検索効率は上記の時間計算量よりも優れています。 配列 `state` は $O(n^2)$ 空間を使用し、配列 `cols`、`diags1`、`diags2` はそれぞれ $O(n)$ 空間を使用します。最大再帰深度は $n$ で、$O(n)$ のスタックフレーム空間を使用します。したがって、**空間計算量は $O(n^2)$ です**。 diff --git a/ja/docs/chapter_backtracking/permutations_problem.md b/ja/docs/chapter_backtracking/permutations_problem.md index f2757dbd1..c30c45fcc 100644 --- a/ja/docs/chapter_backtracking/permutations_problem.md +++ b/ja/docs/chapter_backtracking/permutations_problem.md @@ -238,14 +238,6 @@ comments: true [class]{}-[func]{permutations_i} ``` -=== "Zig" - - ```zig title="permutations_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsI} - ``` - ## 13.2.2   重複要素を考慮する場合 !!! question @@ -467,14 +459,6 @@ comments: true [class]{}-[func]{permutations_ii} ``` -=== "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)$ です。** diff --git a/ja/docs/chapter_backtracking/subset_sum_problem.md b/ja/docs/chapter_backtracking/subset_sum_problem.md index de3bc430b..ca30de042 100644 --- a/ja/docs/chapter_backtracking/subset_sum_problem.md +++ b/ja/docs/chapter_backtracking/subset_sum_problem.md @@ -207,14 +207,6 @@ comments: true [class]{}-[func]{subset_sum_i_naive} ``` -=== "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]$ が含まれています**。 これは、検索プロセスが選択の順序を区別するためですが、部分集合は選択順序を区別しません。以下の図に示すように、$5$ の前に $4$ を選択することと $4$ の前に $5$ を選択することは異なる分岐ですが、同じ部分集合に対応します。 @@ -447,14 +439,6 @@ comments: true [class]{}-[func]{subset_sum_i} ``` -=== "Zig" - - ```zig title="subset_sum_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumI} - ``` - 以下の図は、配列 $[3, 4, 5]$ とターゲット要素 $9$ を上記のコードに入力した後の全体的なバックトラッキングプロセスを示しています。 ![部分集合和 I のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_i.png){ class="animation-figure" } @@ -688,14 +672,6 @@ comments: true [class]{}-[func]{subset_sum_ii} ``` -=== "Zig" - - ```zig title="subset_sum_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumII} - ``` - 以下の図は、配列 $[4, 4, 5]$ とターゲット要素 $9$ のバックトラッキングプロセスを示し、4種類の剪定操作が含まれています。図とコードのコメントを組み合わせて、検索プロセス全体と各種類の剪定操作の動作を理解してください。 ![部分集合和 II のバックトラッキングプロセス](subset_sum_problem.assets/subset_sum_ii.png){ class="animation-figure" } diff --git a/ja/docs/chapter_computational_complexity/iteration_and_recursion.md b/ja/docs/chapter_computational_complexity/iteration_and_recursion.md index b617d63df..2792a36d4 100644 --- a/ja/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/ja/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -116,12 +116,6 @@ comments: true [class]{}-[func]{for_loop} ``` -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{forLoop} - ``` - 以下の図はこの合計関数を表しています。 ![Flowchart of the sum function](iteration_and_recursion.assets/iteration.png){ class="animation-figure" } @@ -242,12 +236,6 @@ comments: true [class]{}-[func]{while_loop} ``` -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{whileLoop} - ``` - **`while`ループは`for`ループよりも柔軟性を提供します**。特に、条件変数のカスタム初期化と各ステップでの変更が可能です。 例えば、以下のコードでは、条件変数$i$が各ラウンドで2回更新されますが、これは`for`ループでは実装が不便です。 @@ -364,12 +352,6 @@ comments: true [class]{}-[func]{while_loop_ii} ``` -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{whileLoopII} - ``` - 全体的に、**`for`ループはより簡潔で、`while`ループはより柔軟です**。どちらも反復構造を実装できます。どちらを使用するかは、問題の具体的な要件に基づいて決定する必要があります。 ### 3.   ネストしたループ @@ -484,12 +466,6 @@ comments: true [class]{}-[func]{nested_for_loop} ``` -=== "Zig" - - ```zig title="iteration.zig" - [class]{}-[func]{nestedForLoop} - ``` - 以下の図はこのネストしたループを表しています。 ![Flowchart of the nested loop](iteration_and_recursion.assets/nested_iteration.png){ class="animation-figure" } @@ -619,12 +595,6 @@ comments: true [class]{}-[func]{recur} ``` -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{recur} - ``` - 以下の図はこの関数の再帰プロセスを示しています。 ![Recursive process of the sum function](iteration_and_recursion.assets/recursion_sum.png){ class="animation-figure" } @@ -763,12 +733,6 @@ comments: true [class]{}-[func]{tail_recur} ``` -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{tailRecur} - ``` - 末尾再帰の実行プロセスは以下の図に示されています。通常の再帰と末尾再帰を比較すると、合計操作のポイントが異なります。 - **通常の再帰**: 合計操作は「返却」フェーズで発生し、各レイヤーが返った後にもう一度合計が必要です。 @@ -901,12 +865,6 @@ comments: true [class]{}-[func]{fib} ``` -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{fib} - ``` - 上記のコードを観察すると、それ自体の中で2つの関数を再帰的に呼び出していることがわかります。**つまり、1回の呼び出しで2つの分岐呼び出しが生成されます**。以下の図に示されているように、この継続的な再帰呼び出しは最終的に深さ$n$の再帰木を作成します。 ![Fibonacci sequence recursion tree](iteration_and_recursion.assets/recursion_tree.png){ class="animation-figure" } @@ -1075,12 +1033,6 @@ comments: true [class]{}-[func]{for_loop_recur} ``` -=== "Zig" - - ```zig title="recursion.zig" - [class]{}-[func]{forLoopRecur} - ``` - 上記のコードを観察すると、再帰が反復に変換されたとき、コードはより複雑になります。反復と再帰はしばしば相互に変換できますが、2つの理由でそうすることが常に推奨されるわけではありません: - 変換されたコードは理解がより困難になり、読みにくくなる可能性があります。 diff --git a/ja/docs/chapter_computational_complexity/space_complexity.md b/ja/docs/chapter_computational_complexity/space_complexity.md index ef9178a2a..1db205645 100644 --- a/ja/docs/chapter_computational_complexity/space_complexity.md +++ b/ja/docs/chapter_computational_complexity/space_complexity.md @@ -322,12 +322,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - - ``` - ## 2.4.2   計算方法 空間計算量を計算する方法は時間計算量とほぼ同様で、統計対象を「操作数」から「使用空間のサイズ」に変更するだけです。 @@ -474,12 +468,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - - ``` - **再帰関数では、スタックフレーム空間を考慮に入れる必要があります**。以下のコードを考えてみましょう: === "Python" @@ -718,12 +706,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - - ``` - `loop()`関数と`recur()`関数の時間計算量は両方とも$O(n)$ですが、それらの空間計算量は異なります。 - `loop()`関数はループ内で`function()`を$n$回呼び出し、各反復の`function()`は返ってそのスタックフレーム空間を解放するため、空間計算量は$O(1)$のままです。 @@ -906,14 +888,6 @@ $$ [class]{}-[func]{constant} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{function} - - [class]{}-[func]{constant} - ``` - ### 2.   線形オーダー $O(n)$ {data-toc-label="2.   線形オーダー"} 線形オーダーは配列、連結リスト、スタック、キューなどで一般的で、要素数は$n$に比例します: @@ -1033,12 +1007,6 @@ $$ [class]{}-[func]{linear} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{linear} - ``` - 下図に示されているように、この関数の再帰深度は$n$で、$n$個の未返却の`linear_recur()`関数インスタンスがあり、$O(n)$サイズのスタックフレーム空間を使用します: === "Python" @@ -1136,12 +1104,6 @@ $$ [class]{}-[func]{linear_recur} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{linearRecur} - ``` - ![Recursive function generating linear order space complexity](space_complexity.assets/space_complexity_recursive_linear.png){ class="animation-figure" }

図 2-17   Recursive function generating linear order space complexity

@@ -1255,12 +1217,6 @@ $$ [class]{}-[func]{quadratic} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{quadratic} - ``` - 下図に示されているように、この関数の再帰深度は$n$で、各再帰呼び出しで長さ$n$、$n-1$、$\dots$、$2$、$1$の配列が初期化され、平均$n/2$となり、全体として$O(n^2)$の空間を占有します: === "Python" @@ -1362,12 +1318,6 @@ $$ [class]{}-[func]{quadratic_recur} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{quadraticRecur} - ``` - ![Recursive function generating quadratic order space complexity](space_complexity.assets/space_complexity_recursive_quadratic.png){ class="animation-figure" }

図 2-18   Recursive function generating quadratic order space complexity

@@ -1477,12 +1427,6 @@ $$ [class]{}-[func]{build_tree} ``` -=== "Zig" - - ```zig title="space_complexity.zig" - [class]{}-[func]{buildTree} - ``` - ![Full binary tree generating exponential order space complexity](space_complexity.assets/space_complexity_exponential.png){ class="animation-figure" }

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

diff --git a/ja/docs/chapter_computational_complexity/time_complexity.md b/ja/docs/chapter_computational_complexity/time_complexity.md index 533a1ac05..442f4d8de 100644 --- a/ja/docs/chapter_computational_complexity/time_complexity.md +++ b/ja/docs/chapter_computational_complexity/time_complexity.md @@ -181,21 +181,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - // 特定の操作プラットフォーム下で - fn algorithm(n: usize) void { - var a: i32 = 2; // 1 ns - a += 1; // 1 ns - a *= 2; // 10 ns - // n回ループ - for (0..n) |_| { // 1 ns - std.debug.print("{}\n", .{0}); // 5 ns - } - } - ``` - 上記の方法を使用すると、アルゴリズムの実行時間は$(6n + 12)$ nsとして計算できます: $$ @@ -445,29 +430,6 @@ $$ ``` -=== "Zig" - - ```zig title="" - // アルゴリズムAの時間計算量:定数オーダー - fn algorithm_A(n: usize) void { - _ = n; - std.debug.print("{}\n", .{0}); - } - // アルゴリズムBの時間計算量:線形オーダー - fn algorithm_B(n: i32) void { - for (0..n) |_| { - std.debug.print("{}\n", .{0}); - } - } - // アルゴリズムCの時間計算量:定数オーダー - fn algorithm_C(n: i32) void { - _ = n; - for (0..1000000) |_| { - std.debug.print("{}\n", .{0}); - } - } - ``` - 下図はこれら3つのアルゴリズムの時間計算量を示しています。 - アルゴリズム`A`には1つの印刷操作のみがあり、その実行時間は$n$とともに増加しません。その時間計算量は「定数オーダー」と考えられます。 @@ -647,20 +609,6 @@ $$ ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +1 - a += 1; // +1 - a *= 2; // +1 - // n回ループ - for (0..n) |_| { // +1 (毎回i++が実行される) - std.debug.print("{}\n", .{0}); // +1 - } - } - ``` - アルゴリズムの操作数を入力サイズ$n$の関数として表す関数を$T(n)$とすると、以下の例を考えてみましょう: $$ @@ -910,27 +858,6 @@ $f(n)$が決まれば、時間計算量$O(f(n))$が得られます。しかし ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +0 (技法1) - a = a + @as(i32, @intCast(n)); // +0 (技法1) - - // +n (技法2) - for(0..(5 * n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - - // +n*n (技法3) - for(0..(2 * n)) |_| { - for(0..(n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - } - } - ``` - 以下の式は、簡略化前後のカウント結果を示しており、どちらも$O(n^2)$の時間計算量に導きます: $$ @@ -1078,12 +1005,6 @@ $$ [class]{}-[func]{constant} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{constant} - ``` - ### 2.   線形オーダー $O(n)$ {data-toc-label="2.   線形オーダー"} 線形オーダーは、操作数が入力データサイズ$n$と線形に増加することを示します。線形オーダーは一般的に単一ループ構造で現れます: @@ -1183,12 +1104,6 @@ $$ [class]{}-[func]{linear} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{linear} - ``` - 配列の走査や連結リストの走査などの操作は時間計算量が$O(n)$で、$n$は配列またはリストの長さです: === "Python" @@ -1291,12 +1206,6 @@ $$ [class]{}-[func]{array_traversal} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{arrayTraversal} - ``` - **入力データサイズ$n$は入力データの種類に基づいて決定する必要があります**。例えば、最初の例では、$n$は入力データサイズを表し、2番目の例では、配列の長さ$n$がデータサイズです。 ### 3.   二次オーダー $O(n^2)$ {data-toc-label="3.   二次オーダー"} @@ -1408,12 +1317,6 @@ $$ [class]{}-[func]{quadratic} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{quadratic} - ``` - 下図は定数オーダー、線形オーダー、二次オーダーの時間計算量を比較しています。 ![Constant, linear, and quadratic order time complexities](time_complexity.assets/time_complexity_constant_linear_quadratic.png){ class="animation-figure" } @@ -1547,12 +1450,6 @@ $$ [class]{}-[func]{bubble_sort} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{bubbleSort} - ``` - ### 4.   指数オーダー $O(2^n)$ {data-toc-label="4.   指数オーダー"} 生物学的「細胞分裂」は指数オーダー増加の典型例です:1つの細胞から始まり、1回の分裂後に2つ、2回の分裂後に4つとなり、$n$回の分裂後に$2^n$個の細胞になります。 @@ -1671,12 +1568,6 @@ $$ [class]{}-[func]{exponential} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{exponential} - ``` - ![Exponential order time complexity](time_complexity.assets/time_complexity_exponential.png){ class="animation-figure" }

図 2-11   Exponential order time complexity

@@ -1775,12 +1666,6 @@ $$ [class]{}-[func]{exp_recur} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{expRecur} - ``` - 指数オーダーの増加は極めて急速で、全数探索法(ブルートフォース、バックトラッキングなど)でよく見られます。大規模問題では、指数オーダーは受け入れられず、しばしば動的プログラミングや貪欲アルゴリズムが解決策として必要になります。 ### 5.   対数オーダー $O(\log n)$ {data-toc-label="5.   対数オーダー"} @@ -1889,12 +1774,6 @@ $$ [class]{}-[func]{logarithmic} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{logarithmic} - ``` - ![Logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic.png){ class="animation-figure" }

図 2-12   Logarithmic order time complexity

@@ -1993,12 +1872,6 @@ $$ [class]{}-[func]{log_recur} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{logRecur} - ``` - 対数オーダーは分割統治戦略に基づくアルゴリズムの典型で、「多くに分割」と「複雑な問題を単純化」するアプローチを体現しています。増加が遅く、定数オーダーの次に最も理想的な時間計算量です。 !!! tip "$O(\log n)$の底は何ですか?" @@ -2118,12 +1991,6 @@ $$ [class]{}-[func]{linear_log_recur} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{linearLogRecur} - ``` - 下図は線形対数オーダーがどのように生成されるかを示しています。二分木の各レベルには$n$個の操作があり、木には$\log_2 n + 1$レベルがあり、時間計算量は$O(n \log n)$になります。 ![Linear-logarithmic order time complexity](time_complexity.assets/time_complexity_logarithmic_linear.png){ class="animation-figure" } @@ -2248,12 +2115,6 @@ $$ [class]{}-[func]{factorial_recur} ``` -=== "Zig" - - ```zig title="time_complexity.zig" - [class]{}-[func]{factorialRecur} - ``` - ![Factorial order time complexity](time_complexity.assets/time_complexity_factorial.png){ class="animation-figure" }

図 2-14   Factorial order time complexity

@@ -2431,14 +2292,6 @@ $$ [class]{}-[func]{find_one} ``` -=== "Zig" - - ```zig title="worst_best_time_complexity.zig" - [class]{}-[func]{randomNumbers} - - [class]{}-[func]{findOne} - ``` - 最良ケース時間計算量は実際にはほとんど使用されないことに注意してください。通常は非常に低い確率でのみ達成可能で、誤解を招く可能性があるからです。**最悪ケース時間計算量はより実用的で、効率の安全値を提供し**、アルゴリズムを自信を持って使用できるようにします。 上記の例から、最悪ケースと最良ケースの時間計算量は両方とも「特殊なデータ分布」下でのみ発生し、発生確率が小さく、アルゴリズムの実行効率を正確に反映しない可能性があることが明らかです。対照的に、**平均時間計算量はランダム入力データ下でのアルゴリズムの効率を反映でき**、$\Theta$記法で表されます。 diff --git a/ja/docs/chapter_data_structure/basic_data_types.md b/ja/docs/chapter_data_structure/basic_data_types.md index 49073d0b8..4d4b9ac32 100644 --- a/ja/docs/chapter_data_structure/basic_data_types.md +++ b/ja/docs/chapter_data_structure/basic_data_types.md @@ -167,12 +167,3 @@ comments: true ``` -=== "Zig" - - ```zig title="" - // 様々な基本データ型を使用して配列を初期化 - var numbers: [5]i32 = undefined; - var decimals: [5]f32 = undefined; - var characters: [5]u8 = undefined; - var bools: [5]bool = undefined; - ``` diff --git a/ja/docs/chapter_divide_and_conquer/binary_search_recur.md b/ja/docs/chapter_divide_and_conquer/binary_search_recur.md index 0b77d869c..9e5ab280a 100644 --- a/ja/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/ja/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -214,11 +214,3 @@ comments: true [class]{}-[func]{binary_search} ``` - -=== "Zig" - - ```zig title="binary_search_recur.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} - ``` diff --git a/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index 3316bc4e6..4a9768784 100644 --- a/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/ja/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -248,14 +248,6 @@ $m$ の問い合わせの効率を向上させるために、ハッシュテー [class]{}-[func]{build_tree} ``` -=== "Zig" - - ```zig title="build_tree.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - 以下の図は、二分木を構築する再帰過程を示しています。各ノードは再帰の「下降」段階で作成され、各エッジ(参照)は「上昇」段階で形成されます。 === "<1>" diff --git a/ja/docs/chapter_divide_and_conquer/hanota_problem.md b/ja/docs/chapter_divide_and_conquer/hanota_problem.md index 95680bedd..5612ec755 100644 --- a/ja/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/ja/docs/chapter_divide_and_conquer/hanota_problem.md @@ -295,16 +295,6 @@ $f(2)$ を解決する過程は次のように要約できます:**`B` の助 [class]{}-[func]{solve_hanota} ``` -=== "Zig" - - ```zig title="hanota.zig" - [class]{}-[func]{move} - - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - 以下の図に示すように、ハノイの塔問題は高さ $n$ の再帰木として視覚化できます。各ノードは部分問題を表し、`dfs()` の呼び出しに対応します。**したがって、時間計算量は $O(2^n)$、空間計算量は $O(n)$ です。** ![ハノイの塔の再帰木](hanota_problem.assets/hanota_recursive_tree.png){ class="animation-figure" } diff --git a/ja/docs/chapter_dynamic_programming/dp_problem_features.md b/ja/docs/chapter_dynamic_programming/dp_problem_features.md index 990a7b5e5..fc7c867f2 100644 --- a/ja/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/ja/docs/chapter_dynamic_programming/dp_problem_features.md @@ -160,12 +160,6 @@ $$ [class]{}-[func]{min_cost_climbing_stairs_dp} ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - [class]{}-[func]{minCostClimbingStairsDP} - ``` - 下の図は上記コードの動的プログラミングプロセスを示しています。 ![階段登りの最小コストの動的プログラミングプロセス](dp_problem_features.assets/min_cost_cs_dp.png){ class="animation-figure" } @@ -284,12 +278,6 @@ $$ [class]{}-[func]{min_cost_climbing_stairs_dp_comp} ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - [class]{}-[func]{minCostClimbingStairsDPComp} - ``` - ## 14.2.2   無記憶性 無記憶性は動的プログラミングが問題解決に効果的であることを可能にする重要な特徴の1つです。その定義は:**特定の状態が与えられたとき、その将来の発展は現在の状態のみに関連し、過去に経験したすべての状態とは無関係である**。 @@ -459,12 +447,6 @@ $$ [class]{}-[func]{climbing_stairs_constraint_dp} ``` -=== "Zig" - - ```zig title="climbing_stairs_constraint_dp.zig" - [class]{}-[func]{climbingStairsConstraintDP} - ``` - 上記のケースでは、前の状態のみを考慮すればよいため、状態定義を拡張することで依然として無記憶性を満たすことができます。しかし、一部の問題では非常に深刻な「状態効果」があります。 !!! question "障害物生成付き階段登り" diff --git a/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 5926557e3..c795300a0 100644 --- a/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/ja/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -232,12 +232,6 @@ $$ [class]{}-[func]{min_path_sum_dfs} ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDFS} - ``` - 下の図は $dp[2, 1]$ を根とする再帰木を示しており、いくつかの重複する部分問題を含み、その数はグリッド `grid` のサイズが増加すると急激に増加します。 本質的に、重複する部分問題の理由は:**左上隅から特定のセルに到達する複数のパスが存在する**ことです。 @@ -368,12 +362,6 @@ $$ [class]{}-[func]{min_path_sum_dfs_mem} ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDFSMem} - ``` - 下の図に示すように、メモ化を導入した後、すべての部分問題の解は一度だけ計算される必要があるため、時間計算量は状態の総数、つまりグリッドサイズ $O(nm)$ に依存します。 ![メモ化探索の再帰木](dp_solution_pipeline.assets/min_path_sum_dfs_mem.png){ class="animation-figure" } @@ -520,12 +508,6 @@ $$ [class]{}-[func]{min_path_sum_dp} ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDP} - ``` - 下の図は最小経路和の状態遷移プロセスを示し、グリッド全体を走査するため、**時間計算量は $O(nm)$** です。 配列 `dp` のサイズは $n \times m$ であるため、**空間計算量は $O(nm)$** です。 @@ -687,9 +669,3 @@ $$ ```ruby title="min_path_sum.rb" [class]{}-[func]{min_path_sum_dp_comp} ``` - -=== "Zig" - - ```zig title="min_path_sum.zig" - [class]{}-[func]{minPathSumDPComp} - ``` diff --git a/ja/docs/chapter_dynamic_programming/edit_distance_problem.md b/ja/docs/chapter_dynamic_programming/edit_distance_problem.md index bb1b59c28..a19e17c67 100644 --- a/ja/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/ja/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -221,12 +221,6 @@ $$ [class]{}-[func]{edit_distance_dp} ``` -=== "Zig" - - ```zig title="edit_distance.zig" - [class]{}-[func]{editDistanceDP} - ``` - 下の図に示すように、編集距離問題の状態遷移プロセスはナップサック問題と非常に似ており、二次元グリッドを埋めることと見なすことができます。 === "<1>" @@ -408,9 +402,3 @@ $dp[i, j]$ は上の $dp[i-1, j]$、左の $dp[i, j-1]$、左上の $dp[i-1, j-1 ```ruby title="edit_distance.rb" [class]{}-[func]{edit_distance_dp_comp} ``` - -=== "Zig" - - ```zig title="edit_distance.zig" - [class]{}-[func]{editDistanceDPComp} - ``` diff --git a/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index ff4bdbe18..cd1e47668 100644 --- a/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/ja/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -185,14 +185,6 @@ comments: true [class]{}-[func]{climbing_stairs_backtrack} ``` -=== "Zig" - - ```zig title="climbing_stairs_backtrack.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{climbingStairsBacktrack} - ``` - ## 14.1.1   方法1:力任せ探索 バックトラッキングアルゴリズムは問題を明示的に部分問題に分解しません。代わりに、問題を一連の決定ステップとして扱い、試行と枝刈りを通じてすべての可能性を探索します。 @@ -356,14 +348,6 @@ $$ [class]{}-[func]{climbing_stairs_dfs} ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{climbingStairsDFS} - ``` - 下の図は力任せ探索によって形成される再帰木を示しています。問題 $dp[n]$ について、その再帰木の深さは $n$ で、時間計算量は $O(2^n)$ です。この指数的増加により、$n$ が大きいとプログラムの実行がはるかに遅くなり、長い待機時間が生じます。 ![階段登りの再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_tree.png){ class="animation-figure" } @@ -540,14 +524,6 @@ $$ [class]{}-[func]{climbing_stairs_dfs_mem} ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs_mem.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{climbingStairsDFSMem} - ``` - 下の図を観察すると、**メモ化後、すべての重複する部分問題は一度だけ計算される必要があり、時間計算量を $O(n)$ に最適化**します。これは大幅な改善です。 ![メモ化探索による再帰木](intro_to_dynamic_programming.assets/climbing_stairs_dfs_memo_tree.png){ class="animation-figure" } @@ -679,12 +655,6 @@ $$ [class]{}-[func]{climbing_stairs_dp} ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - [class]{}-[func]{climbingStairsDP} - ``` - 下の図は上記コードの実行プロセスをシミュレートしています。 ![階段登りの動的プログラミングプロセス](intro_to_dynamic_programming.assets/climbing_stairs_dp.png){ class="animation-figure" } @@ -810,12 +780,6 @@ $$ [class]{}-[func]{climbing_stairs_dp_comp} ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - [class]{}-[func]{climbingStairsDPComp} - ``` - 上記のコードを観察すると、配列 `dp` が占有していた空間が削除されるため、空間計算量は $O(n)$ から $O(1)$ に削減されます。 多くの動的プログラミング問題では、現在の状態は限られた数の前の状態のみに依存するため、必要な状態のみを保持し、「次元削減」によってメモリ空間を節約できます。**この空間最適化技術は「ローリング変数」または「ローリング配列」として知られています**。 diff --git a/ja/docs/chapter_dynamic_programming/knapsack_problem.md b/ja/docs/chapter_dynamic_programming/knapsack_problem.md index e1d05370b..9286f8a59 100644 --- a/ja/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/ja/docs/chapter_dynamic_programming/knapsack_problem.md @@ -182,12 +182,6 @@ $$ [class]{}-[func]{knapsack_dfs} ``` -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDFS} - ``` - 下の図に示すように、各アイテムは選択しないと選択するという2つの探索分岐を生成するため、時間計算量は $O(2^n)$ です。 再帰木を観察すると、$dp[1, 10]$ などの重複する部分問題があることが容易にわかります。アイテムが多く、ナップサック容量が大きい場合、特に同じ重量のアイテムが多い場合、重複する部分問題の数は大幅に増加します。 @@ -318,12 +312,6 @@ $$ [class]{}-[func]{knapsack_dfs_mem} ``` -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDFSMem} - ``` - 下の図はメモ化探索で枝刈りされる探索分岐を示しています。 ![0-1ナップサック問題のメモ化探索再帰木](knapsack_problem.assets/knapsack_dfs_mem.png){ class="animation-figure" } @@ -462,12 +450,6 @@ $$ [class]{}-[func]{knapsack_dp} ``` -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDP} - ``` - 下の図に示すように、時間計算量と空間計算量の両方が配列 `dp` のサイズ、つまり $O(n \times cap)$ によって決定されます。 === "<1>" @@ -671,9 +653,3 @@ $$ ```ruby title="knapsack.rb" [class]{}-[func]{knapsack_dp_comp} ``` - -=== "Zig" - - ```zig title="knapsack.zig" - [class]{}-[func]{knapsackDPComp} - ``` diff --git a/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index 83d768c85..e930c688d 100644 --- a/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/ja/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -166,12 +166,6 @@ $$ [class]{}-[func]{unbounded_knapsack_dp} ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - [class]{}-[func]{unboundedKnapsackDP} - ``` - ### 3.   空間最適化 現在の状態は左と上の状態から来るため、**空間最適化解法は $dp$ テーブルの各行に対して前方走査を実行する必要があります**。 @@ -329,12 +323,6 @@ $$ [class]{}-[func]{unbounded_knapsack_dp_comp} ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - [class]{}-[func]{unboundedKnapsackDPComp} - ``` - ## 14.5.2   コイン交換問題 ナップサック問題は動的プログラミング問題の大きなクラスの代表であり、コイン交換問題など多くの変種があります。 @@ -526,12 +514,6 @@ $$ [class]{}-[func]{coin_change_dp} ``` -=== "Zig" - - ```zig title="coin_change.zig" - [class]{}-[func]{coinChangeDP} - ``` - 下の図はコイン交換問題の動的プログラミングプロセスを示しており、無制限ナップサック問題と非常に似ています。 === "<1>" @@ -721,12 +703,6 @@ $$ [class]{}-[func]{coin_change_dp_comp} ``` -=== "Zig" - - ```zig title="coin_change.zig" - [class]{}-[func]{coinChangeDPComp} - ``` - ## 14.5.3   コイン交換問題II !!! question @@ -890,12 +866,6 @@ $$ [class]{}-[func]{coin_change_ii_dp} ``` -=== "Zig" - - ```zig title="coin_change_ii.zig" - [class]{}-[func]{coinChangeIIDP} - ``` - ### 3.   空間最適化 空間最適化アプローチは同じで、コインの次元を削除するだけです: @@ -1031,9 +1001,3 @@ $$ ```ruby title="coin_change_ii.rb" [class]{}-[func]{coin_change_ii_dp_comp} ``` - -=== "Zig" - - ```zig title="coin_change_ii.zig" - [class]{}-[func]{coinChangeIIDPComp} - ``` diff --git a/ja/docs/chapter_graph/graph_operations.md b/ja/docs/chapter_graph/graph_operations.md index 4328c1aaa..76c14da1e 100644 --- a/ja/docs/chapter_graph/graph_operations.md +++ b/ja/docs/chapter_graph/graph_operations.md @@ -346,12 +346,6 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま [class]{GraphAdjMat}-[func]{} ``` -=== "Zig" - - ```zig title="graph_adjacency_matrix.zig" - [class]{GraphAdjMat}-[func]{} - ``` - ## 9.2.2   隣接リストに基づく実装 総計$n$個の頂点と$m$個の辺を持つ無向グラフが与えられた場合、さまざまな操作は下図のように実装できます。 @@ -670,12 +664,6 @@ $n$個の頂点を持つ無向グラフが与えられた場合、さまざま [class]{GraphAdjList}-[func]{} ``` -=== "Zig" - - ```zig title="graph_adjacency_list.zig" - [class]{GraphAdjList}-[func]{} - ``` - ## 9.2.3   効率の比較 グラフに$n$個の頂点と$m$個の辺があると仮定すると、下表は隣接行列と隣接リストの時間効率と空間効率を比較しています。 diff --git a/ja/docs/chapter_graph/graph_traversal.md b/ja/docs/chapter_graph/graph_traversal.md index d708b1525..845c33a0e 100644 --- a/ja/docs/chapter_graph/graph_traversal.md +++ b/ja/docs/chapter_graph/graph_traversal.md @@ -178,12 +178,6 @@ BFSは通常キューの助けを借りて実装されます(下記のコー [class]{}-[func]{graph_bfs} ``` -=== "Zig" - - ```zig title="graph_bfs.zig" - [class]{}-[func]{graphBFS} - ``` - コードは比較的抽象的ですが、下図と比較することでより良く理解できます。 === "<1>" @@ -406,14 +400,6 @@ BFSは通常キューの助けを借りて実装されます(下記のコー [class]{}-[func]{graph_dfs} ``` -=== "Zig" - - ```zig title="graph_dfs.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{graphDFS} - ``` - 深さ優先探索のアルゴリズムプロセスを下図に示します。 - **破線は下向きの再帰を表し**、新しい頂点を訪問するために新しい再帰メソッドが開始されたことを示します。 diff --git a/ja/docs/chapter_greedy/fractional_knapsack_problem.md b/ja/docs/chapter_greedy/fractional_knapsack_problem.md index ada082f8c..27d909940 100644 --- a/ja/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/ja/docs/chapter_greedy/fractional_knapsack_problem.md @@ -231,14 +231,6 @@ comments: true [class]{}-[func]{fractional_knapsack} ``` -=== "Zig" - - ```zig title="fractional_knapsack.zig" - [class]{Item}-[func]{} - - [class]{}-[func]{fractionalKnapsack} - ``` - ソート以外に、最悪の場合、アイテムのリスト全体を走査する必要があるため、**時間計算量は $O(n)$** です。ここで $n$ はアイテムの数です。 `Item` オブジェクトリストが初期化されるため、**空間計算量は $O(n)$** です。 diff --git a/ja/docs/chapter_greedy/greedy_algorithm.md b/ja/docs/chapter_greedy/greedy_algorithm.md index 80d48cbc1..5cbbd7b15 100644 --- a/ja/docs/chapter_greedy/greedy_algorithm.md +++ b/ja/docs/chapter_greedy/greedy_algorithm.md @@ -151,12 +151,6 @@ comments: true [class]{}-[func]{coin_change_greedy} ``` -=== "Zig" - - ```zig title="coin_change_greedy.zig" - [class]{}-[func]{coinChangeGreedy} - ``` - 感嘆するかもしれません:なんて簡潔なんだ!貪欲アルゴリズムは約10行のコードでコイン交換問題を解決します。 ## 15.1.1   貪欲アルゴリズムの利点と制限 diff --git a/ja/docs/chapter_greedy/max_capacity_problem.md b/ja/docs/chapter_greedy/max_capacity_problem.md index 422682698..deeb265ab 100644 --- a/ja/docs/chapter_greedy/max_capacity_problem.md +++ b/ja/docs/chapter_greedy/max_capacity_problem.md @@ -224,12 +224,6 @@ $$ [class]{}-[func]{max_capacity} ``` -=== "Zig" - - ```zig title="max_capacity.zig" - [class]{}-[func]{maxCapacity} - ``` - ### 3.   正しさの証明 貪欲法が列挙よりも高速である理由は、各ラウンドの貪欲選択が一部の状態を「スキップ」するからです。 diff --git a/ja/docs/chapter_greedy/max_product_cutting_problem.md b/ja/docs/chapter_greedy/max_product_cutting_problem.md index 110715863..928e5766b 100644 --- a/ja/docs/chapter_greedy/max_product_cutting_problem.md +++ b/ja/docs/chapter_greedy/max_product_cutting_problem.md @@ -203,12 +203,6 @@ $n \leq 3$ の境界ケースでは、$1$ を分割する必要があり、積 [class]{}-[func]{max_product_cutting} ``` -=== "Zig" - - ```zig title="max_product_cutting.zig" - [class]{}-[func]{maxProductCutting} - ``` - ![切断後の最大積の計算方法](max_product_cutting_problem.assets/max_product_cutting_greedy_calculation.png){ class="animation-figure" }

図 15-16   切断後の最大積の計算方法

diff --git a/ja/docs/chapter_hashing/hash_algorithm.md b/ja/docs/chapter_hashing/hash_algorithm.md index 19e98c39b..cb5e1f994 100644 --- a/ja/docs/chapter_hashing/hash_algorithm.md +++ b/ja/docs/chapter_hashing/hash_algorithm.md @@ -296,18 +296,6 @@ index = hash(key) % capacity [class]{}-[func]{rot_hash} ``` -=== "Zig" - - ```zig title="simple_hash.zig" - [class]{}-[func]{addHash} - - [class]{}-[func]{mulHash} - - [class]{}-[func]{xorHash} - - [class]{}-[func]{rotHash} - ``` - 各ハッシュアルゴリズムの最後のステップが大きな素数$1000000007$の剰余を取ることで、ハッシュ値が適切な範囲内にあることを保証していることが観察されます。なぜ素数の剰余を取ることが強調されるのか、または合成数の剰余を取ることの欠点は何かを考える価値があります。これは興味深い質問です。 結論として:**大きな素数を剰余として使用することで、ハッシュ値の均等分散を最大化できます**。素数は他の数と共通因子を持たないため、剰余演算によって引き起こされる周期的パターンを減らし、ハッシュ衝突を回避できます。 @@ -611,12 +599,6 @@ $$ ``` -=== "Zig" - - ```zig title="built_in_hash.zig" - - ``` - 多くのプログラミング言語では、**不変オブジェクトのみがハッシュ表の`key`として機能できます**。リスト(動的配列)を`key`として使用する場合、リストの内容が変更されると、そのハッシュ値も変更され、ハッシュ表で元の`value`を見つけることができなくなります。 カスタムオブジェクト(連結リストノードなど)のメンバー変数は可変ですが、ハッシュ可能です。**これは、オブジェクトのハッシュ値が通常そのメモリアドレスに基づいて生成されるためです**。オブジェクトの内容が変更されても、メモリアドレスは同じままなので、ハッシュ値は変更されません。 diff --git a/ja/docs/chapter_hashing/hash_collision.md b/ja/docs/chapter_hashing/hash_collision.md index 1d701fb2a..8e0c4fc97 100644 --- a/ja/docs/chapter_hashing/hash_collision.md +++ b/ja/docs/chapter_hashing/hash_collision.md @@ -414,12 +414,6 @@ comments: true [class]{HashMapChaining}-[func]{} ``` -=== "Zig" - - ```zig title="hash_map_chaining.zig" - [class]{HashMapChaining}-[func]{} - ``` - 連結リストが非常に長い場合、クエリ効率$O(n)$が悪いことは注目に値します。**この場合、リストを「AVL木」または「赤黒木」に変換して**、クエリ操作の時間計算量を$O(\log n)$に最適化できます。 ## 6.2.2   オープンアドレス法 @@ -886,12 +880,6 @@ comments: true [class]{HashMapOpenAddressing}-[func]{} ``` -=== "Zig" - - ```zig title="hash_map_open_addressing.zig" - [class]{HashMapOpenAddressing}-[func]{} - ``` - ### 2.   二次プローブ 二次プローブは線形プローブに似ており、オープンアドレス法の一般的な戦略の1つです。衝突が発生した場合、二次プローブは単純に固定ステップ数をスキップするのではなく、「プローブ回数の二乗」に等しいステップ数、つまり$1, 4, 9, \dots$ステップをスキップします。 diff --git a/ja/docs/chapter_hashing/hash_map.md b/ja/docs/chapter_hashing/hash_map.md index ff6f84323..023b82fb7 100644 --- a/ja/docs/chapter_hashing/hash_map.md +++ b/ja/docs/chapter_hashing/hash_map.md @@ -283,12 +283,6 @@ comments: true ``` -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - ハッシュ表を走査する一般的な方法は3つあります:キー値ペアの走査、キーの走査、値の走査。以下はコード例です: === "Python" @@ -480,12 +474,6 @@ comments: true ``` -=== "Zig" - - ```zig title="hash_map.zig" - // Zigの例は提供されていません - ``` - ## 6.1.2   ハッシュ表の簡単な実装 まず、最も簡単なケースを考えてみましょう:**配列のみを使ってハッシュ表を実装すること**。ハッシュ表において、配列の各空きスロットはバケットと呼ばれ、各バケットはキー値ペアを格納できます。したがって、クエリ操作は`key`に対応するバケットを見つけ、そこから`value`を取得することになります。 @@ -864,14 +852,6 @@ index = hash(key) % capacity [class]{ArrayHashMap}-[func]{} ``` -=== "Zig" - - ```zig title="array_hash_map.zig" - [class]{Pair}-[func]{} - - [class]{ArrayHashMap}-[func]{} - ``` - ## 6.1.3   ハッシュ衝突とリサイズ 本質的に、ハッシュ関数の役割は、すべてのキーの入力空間全体を、すべての配列インデックスの出力空間にマッピングすることです。しかし、入力空間は出力空間よりもはるかに大きいことがよくあります。したがって、**理論的には、「複数の入力が同じ出力に対応する」ケースが常に存在します**。 diff --git a/ja/docs/chapter_heap/build_heap.md b/ja/docs/chapter_heap/build_heap.md index 0591fe94c..0ecb2efcf 100644 --- a/ja/docs/chapter_heap/build_heap.md +++ b/ja/docs/chapter_heap/build_heap.md @@ -127,12 +127,6 @@ comments: true [class]{MaxHeap}-[func]{initialize} ``` -=== "Zig" - - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{init} - ``` - ## 8.2.3   計算量分析 次に、この第2のヒープ構築方法の時間計算量を計算してみましょう。 diff --git a/ja/docs/chapter_heap/heap.md b/ja/docs/chapter_heap/heap.md index cb871198e..78de77039 100644 --- a/ja/docs/chapter_heap/heap.md +++ b/ja/docs/chapter_heap/heap.md @@ -418,12 +418,6 @@ comments: true ``` -=== "Zig" - - ```zig title="heap.zig" - - ``` - ## 8.1.2   ヒープの実装 以下の実装は最大ヒープです。最小ヒープに変換するには、すべてのサイズ論理比較を反転させるだけです(例えば、$\geq$を$\leq$に置き換える)。興味のある読者は自分で実装することをお勧めします。 @@ -596,16 +590,6 @@ comments: true [class]{MaxHeap}-[func]{parent} ``` -=== "Zig" - - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{left} - - [class]{MaxHeap}-[func]{right} - - [class]{MaxHeap}-[func]{parent} - ``` - ### 2.   ヒープの先頭要素へのアクセス ヒープの先頭要素は二分木の根ノードで、リストの最初の要素でもあります: @@ -696,12 +680,6 @@ comments: true [class]{MaxHeap}-[func]{peek} ``` -=== "Zig" - - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{peek} - ``` - ### 3.   ヒープへの要素挿入 要素`val`が与えられた場合、まずそれをヒープの底に追加します。追加後、`val`がヒープ内の他の要素より大きい可能性があるため、ヒープの完全性が損なわれる可能性があります。**したがって、挿入されたノードから根ノードまでのパスを修復する必要があります**。この操作はヒープ化と呼ばれます。 @@ -897,14 +875,6 @@ comments: true [class]{MaxHeap}-[func]{sift_up} ``` -=== "Zig" - - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{push} - - [class]{MaxHeap}-[func]{siftUp} - ``` - ### 4.   ヒープからの先頭要素削除 ヒープの先頭要素は二分木の根ノード、つまりリストの最初の要素です。リストから最初の要素を直接削除すると、二分木内のすべてのノードインデックスが変更され、後続の修復にヒープ化を使用することが困難になります。要素インデックスの変更を最小限に抑えるため、次の手順を使用します。 @@ -1138,14 +1108,6 @@ comments: true [class]{MaxHeap}-[func]{sift_down} ``` -=== "Zig" - - ```zig title="my_heap.zig" - [class]{MaxHeap}-[func]{pop} - - [class]{MaxHeap}-[func]{siftDown} - ``` - ## 8.1.3   ヒープの一般的な応用 - **優先度キュー**:ヒープは優先度キューを実装するための好ましいデータ構造で、エンキュー操作とデキュー操作の両方の時間計算量が$O(\log n)$、キュー構築の時間計算量が$O(n)$で、すべて非常に効率的です。 diff --git a/ja/docs/chapter_heap/top_k.md b/ja/docs/chapter_heap/top_k.md index c1b0b579c..3f3b8de0e 100644 --- a/ja/docs/chapter_heap/top_k.md +++ b/ja/docs/chapter_heap/top_k.md @@ -223,12 +223,6 @@ comments: true [class]{}-[func]{top_k_heap} ``` -=== "Zig" - - ```zig title="top_k.zig" - [class]{}-[func]{topKHeap} - ``` - 合計$n$回のヒープ挿入と削除が実行され、最大ヒープサイズが$k$であるため、時間計算量は$O(n \log k)$です。この方法は非常に効率的で、$k$が小さい場合、時間計算量は$O(n)$に近づき、$k$が大きい場合でも、時間計算量は$O(n \log n)$を超えません。 さらに、この方法は動的データストリームのシナリオに適しています。データを継続的に追加することで、ヒープ内の要素を維持し、最大$k$個の要素の動的更新を実現できます。 diff --git a/ja/docs/chapter_preface/suggestions.md b/ja/docs/chapter_preface/suggestions.md index cc29714e6..c5c10aab0 100644 --- a/ja/docs/chapter_preface/suggestions.md +++ b/ja/docs/chapter_preface/suggestions.md @@ -173,17 +173,6 @@ comments: true */ ``` -=== "Zig" - - ```zig title="" - // 関数、クラス、テストサンプルなどをラベル付けするためのヘッダーコメント - - // 詳細を説明するためのコメント - - // 複数行 - // コメント - ``` - ## 0.2.2   アニメーション図解による効率的学習 テキストと比較して、動画や画像は情報密度が高く、より構造化されており、理解しやすくなっています。この本では、**重要で難しい概念は主にアニメーションと図解を通じて提示され**、テキストは説明と補足として機能します。 diff --git a/ja/docs/chapter_searching/binary_search.md b/ja/docs/chapter_searching/binary_search.md index 62cfdce99..84c906bd2 100644 --- a/ja/docs/chapter_searching/binary_search.md +++ b/ja/docs/chapter_searching/binary_search.md @@ -177,12 +177,6 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 [class]{}-[func]{binary_search} ``` -=== "Zig" - - ```zig title="binary_search.zig" - [class]{}-[func]{binarySearch} - ``` - **時間計算量は$O(\log n)$です**:二分ループにおいて、区間は各ラウンドで半分に減少するため、反復回数は$\log_2 n$となります。 **空間計算量は$O(1)$です**:ポインタ$i$と$j$は定数サイズの空間を占有します。 @@ -316,12 +310,6 @@ $i$と$j$が両方とも`int`型であるため、**$i + j$は`int`型の範囲 [class]{}-[func]{binary_search_lcro} ``` -=== "Zig" - - ```zig title="binary_search.zig" - [class]{}-[func]{binarySearchLCRO} - ``` - 下図に示すように、2つの区間表現タイプにおいて、二分探索アルゴリズムの初期化、ループ条件、区間縮小操作が異なります。 「閉区間」表現では両方の境界が包含的であるため、ポインタ$i$と$j$による区間縮小操作も対称的です。これによりエラーが発生しにくくなるため、**一般的に「閉区間」アプローチの使用が推奨されます**。 diff --git a/ja/docs/chapter_searching/binary_search_edge.md b/ja/docs/chapter_searching/binary_search_edge.md index 4c05c8ead..026b3f08b 100644 --- a/ja/docs/chapter_searching/binary_search_edge.md +++ b/ja/docs/chapter_searching/binary_search_edge.md @@ -125,12 +125,6 @@ comments: true [class]{}-[func]{binary_search_left_edge} ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchLeftEdge} - ``` - ## 10.3.2   右境界を見つける `target`の最も右の出現をどのように見つけるでしょうか?最も直接的な方法は、`nums[m] == target`の場合に探索境界を調整する方法を変更して、従来の二分探索ロジックを修正することです。コードはここでは省略されています。興味がある場合は、自分でコードを実装してみてください。 @@ -261,12 +255,6 @@ comments: true [class]{}-[func]{binary_search_right_edge} ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchRightEdge} - ``` - ### 2.   要素探索に変換する 配列に`target`が含まれていない場合、$i$と$j$は最終的に`target`より大きい最初の要素と小さい最初の要素をそれぞれ指します。 diff --git a/ja/docs/chapter_searching/binary_search_insertion.md b/ja/docs/chapter_searching/binary_search_insertion.md index b7c65d197..d659466b9 100644 --- a/ja/docs/chapter_searching/binary_search_insertion.md +++ b/ja/docs/chapter_searching/binary_search_insertion.md @@ -148,12 +148,6 @@ comments: true [class]{}-[func]{binary_search_insertion_simple} ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertionSimple} - ``` - ## 10.2.2   重複要素がある場合 !!! question @@ -330,12 +324,6 @@ comments: true [class]{}-[func]{binary_search_insertion} ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertion} - ``` - !!! tip このセクションのコードは「閉区間」を使用しています。「左閉右開」に興味がある場合は、自分でコードを実装してみてください。 diff --git a/ja/docs/chapter_searching/replace_linear_by_hashing.md b/ja/docs/chapter_searching/replace_linear_by_hashing.md index 877680568..7756a3969 100644 --- a/ja/docs/chapter_searching/replace_linear_by_hashing.md +++ b/ja/docs/chapter_searching/replace_linear_by_hashing.md @@ -127,12 +127,6 @@ comments: true [class]{}-[func]{two_sum_brute_force} ``` -=== "Zig" - - ```zig title="two_sum.zig" - [class]{}-[func]{twoSumBruteForce} - ``` - この方法の時間計算量は$O(n^2)$、空間計算量は$O(1)$で、大容量データでは非常に時間がかかる可能性があります。 ## 10.4.2   ハッシュ探索:空間を時間と交換 @@ -270,12 +264,6 @@ comments: true [class]{}-[func]{two_sum_hash_table} ``` -=== "Zig" - - ```zig title="two_sum.zig" - [class]{}-[func]{twoSumHashTable} - ``` - この方法は、ハッシュ探索を使用することで時間計算量を$O(n^2)$から$O(n)$に削減し、実行時効率を大幅に向上させます。 追加のハッシュテーブルを維持する必要があるため、空間計算量は$O(n)$です。**それにもかかわらず、この方法は全体的により均衡のとれた時空間効率を持ち、この問題の最適解となります**。 diff --git a/ja/docs/chapter_sorting/bubble_sort.md b/ja/docs/chapter_sorting/bubble_sort.md index 7b45d88cf..75cffe648 100644 --- a/ja/docs/chapter_sorting/bubble_sort.md +++ b/ja/docs/chapter_sorting/bubble_sort.md @@ -160,12 +160,6 @@ comments: true [class]{}-[func]{bubble_sort} ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - [class]{}-[func]{bubbleSort} - ``` - ## 11.3.2   効率の最適化 「バブリング」のラウンド中に交換が発生しない場合、配列はすでにソートされているため、すぐに戻ることができます。これを検出するために、`flag`変数を追加できます;パスで交換が行われない場合は、フラグを設定して早期に戻ります。 @@ -298,12 +292,6 @@ comments: true [class]{}-[func]{bubble_sort_with_flag} ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - [class]{}-[func]{bubbleSortWithFlag} - ``` - ## 11.3.3   アルゴリズムの特性 - **$O(n^2)$の時間計算量、適応ソート。** 各「バブリング」ラウンドは長さ$n - 1$、$n - 2$、$\dots$、$2$、$1$の配列セグメントを横断し、合計は$(n - 1) n / 2$となります。`flag`最適化により、配列がすでにソートされている場合、最良ケース時間計算量は$O(n)$に達する可能性があります。 diff --git a/ja/docs/chapter_sorting/bucket_sort.md b/ja/docs/chapter_sorting/bucket_sort.md index d243937a2..723b9ad9e 100644 --- a/ja/docs/chapter_sorting/bucket_sort.md +++ b/ja/docs/chapter_sorting/bucket_sort.md @@ -171,12 +171,6 @@ comments: true [class]{}-[func]{bucket_sort} ``` -=== "Zig" - - ```zig title="bucket_sort.zig" - [class]{}-[func]{bucketSort} - ``` - ## 11.8.2   アルゴリズムの特徴 バケットソートは非常に大きなデータセットの処理に適しています。例えば、入力データに100万個の要素が含まれ、システムメモリの制限によりすべてのデータを同時にロードできない場合、データを1,000個のバケットに分割し、各バケットを個別にソートしてから結果をマージできます。 diff --git a/ja/docs/chapter_sorting/counting_sort.md b/ja/docs/chapter_sorting/counting_sort.md index bdd2867b4..5a19c9376 100644 --- a/ja/docs/chapter_sorting/counting_sort.md +++ b/ja/docs/chapter_sorting/counting_sort.md @@ -157,12 +157,6 @@ comments: true [class]{}-[func]{counting_sort_naive} ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSortNaive} - ``` - !!! note "計数ソートとバケットソートの関係" バケットソートの観点から、計数ソートにおける計数配列 `counter` の各インデックスをバケットと考え、カウントの過程を要素を対応するバケットに分散させることと考えることができます。本質的に、計数ソートは整数データのためのバケットソートの特別なケースです。 @@ -376,12 +370,6 @@ $$ [class]{}-[func]{counting_sort} ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSort} - ``` - ## 11.9.3   アルゴリズムの特徴 - **時間計算量は $O(n + m)$、非適応ソート**:`nums` と `counter` の走査が含まれ、どちらも線形時間を使用します。一般的に、$n \gg m$ であり、時間計算量は $O(n)$ に近づきます。 diff --git a/ja/docs/chapter_sorting/heap_sort.md b/ja/docs/chapter_sorting/heap_sort.md index 89e2a6da1..f4e7b5ee8 100644 --- a/ja/docs/chapter_sorting/heap_sort.md +++ b/ja/docs/chapter_sorting/heap_sort.md @@ -268,14 +268,6 @@ comments: true [class]{}-[func]{heap_sort} ``` -=== "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$ ラウンドループします。 diff --git a/ja/docs/chapter_sorting/insertion_sort.md b/ja/docs/chapter_sorting/insertion_sort.md index 2481e0b96..854f1d31c 100644 --- a/ja/docs/chapter_sorting/insertion_sort.md +++ b/ja/docs/chapter_sorting/insertion_sort.md @@ -141,12 +141,6 @@ comments: true [class]{}-[func]{insertion_sort} ``` -=== "Zig" - - ```zig title="insertion_sort.zig" - [class]{}-[func]{insertionSort} - ``` - ## 11.4.2   アルゴリズムの特性 - **時間計算量は$O(n^2)$、適応ソート**:最悪の場合、各挿入操作には$n - 1$、$n-2$、...、$2$、$1$のループが必要で、合計は$(n - 1) n / 2$となり、時間計算量は$O(n^2)$です。順序付きデータの場合、挿入操作は早期に終了します。入力配列が完全に順序付けられている場合、挿入ソートは最良時間計算量$O(n)$を実現します。 diff --git a/ja/docs/chapter_sorting/merge_sort.md b/ja/docs/chapter_sorting/merge_sort.md index 34289993b..2b787a3e7 100644 --- a/ja/docs/chapter_sorting/merge_sort.md +++ b/ja/docs/chapter_sorting/merge_sort.md @@ -274,14 +274,6 @@ comments: true [class]{}-[func]{merge_sort} ``` -=== "Zig" - - ```zig title="merge_sort.zig" - [class]{}-[func]{merge} - - [class]{}-[func]{mergeSort} - ``` - ## 11.6.2   アルゴリズムの特性 - **$O(n \log n)$の時間計算量、非適応ソート**:分割により高さ$\log n$の再帰ツリーが作成され、各層で合計$n$回の操作をマージし、全体的な時間計算量は$O(n \log n)$となります。 diff --git a/ja/docs/chapter_sorting/quick_sort.md b/ja/docs/chapter_sorting/quick_sort.md index 147c1bb5d..2b393a070 100644 --- a/ja/docs/chapter_sorting/quick_sort.md +++ b/ja/docs/chapter_sorting/quick_sort.md @@ -183,14 +183,6 @@ comments: true [class]{QuickSort}-[func]{partition} ``` -=== "Zig" - - ```zig title="quick_sort.zig" - [class]{QuickSort}-[func]{swap} - - [class]{QuickSort}-[func]{partition} - ``` - ## 11.5.1   アルゴリズムプロセス クイックソートの全体的なプロセスは下図に示されます。 @@ -310,12 +302,6 @@ comments: true [class]{QuickSort}-[func]{quick_sort} ``` -=== "Zig" - - ```zig title="quick_sort.zig" - [class]{QuickSort}-[func]{quickSort} - ``` - ## 11.5.2   アルゴリズムの特徴 - **$O(n \log n)$の時間計算量、非適応ソート**:平均的なケースでは、ピボット分割の再帰レベルは$\log n$で、レベルあたりのループの総数は$n$であり、全体で$O(n \log n)$の時間を使用します。最悪の場合、各ラウンドのピボット分割は長さ$n$の配列を長さ$0$と$n - 1$の2つのサブ配列に分割し、再帰レベル数が$n$に達すると、各レベルのループ数は$n$で、使用される総時間は$O(n^2)$です。 @@ -520,14 +506,6 @@ comments: true [class]{QuickSortMedian}-[func]{partition} ``` -=== "Zig" - - ```zig title="quick_sort.zig" - [class]{QuickSortMedian}-[func]{medianThree} - - [class]{QuickSortMedian}-[func]{partition} - ``` - ## 11.5.5   末尾再帰最適化 **特定の入力では、クイックソートはより多くの空間を占有する可能性があります**。例えば、完全に順序付けられた入力配列を考えてみましょう。再帰でのサブ配列の長さを$m$とします。各ラウンドのピボット分割で、長さ$0$の左サブ配列と長さ$m - 1$の右サブ配列が生成されます。これは、再帰呼び出しごとに問題サイズが1つの要素のみ減少することを意味し、各レベルの再帰での削減が非常に小さくなります。 @@ -654,9 +632,3 @@ comments: true ```ruby title="quick_sort.rb" [class]{QuickSortTailCall}-[func]{quick_sort} ``` - -=== "Zig" - - ```zig title="quick_sort.zig" - [class]{QuickSortTailCall}-[func]{quickSort} - ``` diff --git a/ja/docs/chapter_sorting/radix_sort.md b/ja/docs/chapter_sorting/radix_sort.md index 00707343a..f397caf78 100644 --- a/ja/docs/chapter_sorting/radix_sort.md +++ b/ja/docs/chapter_sorting/radix_sort.md @@ -280,16 +280,6 @@ $$ [class]{}-[func]{radix_sort} ``` -=== "Zig" - - ```zig title="radix_sort.zig" - [class]{}-[func]{digit} - - [class]{}-[func]{countingSortDigit} - - [class]{}-[func]{radixSort} - ``` - !!! question "なぜ最下位桁から開始するのか?" 連続するソートラウンドでは、後のラウンドの結果が前のラウンドの結果を上書きします。例えば、最初のラウンドの結果が $a < b$ で、2番目のラウンドが $a > b$ の場合、2番目のラウンドの結果が最初のラウンドの結果を置き換えます。上位桁は下位桁より優先されるため、上位桁の前に下位桁をソートすることが理にかなっています。 diff --git a/ja/docs/chapter_sorting/selection_sort.md b/ja/docs/chapter_sorting/selection_sort.md index e59b3916e..198a42e95 100644 --- a/ja/docs/chapter_sorting/selection_sort.md +++ b/ja/docs/chapter_sorting/selection_sort.md @@ -170,12 +170,6 @@ comments: true [class]{}-[func]{selection_sort} ``` -=== "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}$となります。 diff --git a/ja/docs/chapter_stack_and_queue/deque.md b/ja/docs/chapter_stack_and_queue/deque.md index 4f0224b42..c7919f1b0 100644 --- a/ja/docs/chapter_stack_and_queue/deque.md +++ b/ja/docs/chapter_stack_and_queue/deque.md @@ -340,12 +340,6 @@ comments: true ``` -=== "Zig" - - ```zig title="deque.zig" - - ``` - ## 5.3.2   両端キューの実装 * 両端キューの実装は通常のキューの実装と似ており、連結リストまたは配列を基盤となるデータ構造として使用できます。 @@ -845,14 +839,6 @@ comments: true [class]{LinkedListDeque}-[func]{} ``` -=== "Zig" - - ```zig title="linkedlist_deque.zig" - [class]{ListNode}-[func]{} - - [class]{LinkedListDeque}-[func]{} - ``` - ### 2.   配列に基づく実装 下図に示すように、配列でキューを実装するのと同様に、循環配列を使って両端キューを実装することもできます。 @@ -1245,12 +1231,6 @@ comments: true [class]{ArrayDeque}-[func]{} ``` -=== "Zig" - - ```zig title="array_deque.zig" - [class]{ArrayDeque}-[func]{} - ``` - ## 5.3.3   両端キューの応用 両端キューはスタックとキューの両方のロジックを組み合わせているため、**それぞれのすべてのユースケースを実装でき、より大きな柔軟性を提供します**。 diff --git a/ja/docs/chapter_stack_and_queue/queue.md b/ja/docs/chapter_stack_and_queue/queue.md index ca1cd8d52..3f8def737 100644 --- a/ja/docs/chapter_stack_and_queue/queue.md +++ b/ja/docs/chapter_stack_and_queue/queue.md @@ -318,12 +318,6 @@ comments: true ``` -=== "Zig" - - ```zig title="queue.zig" - - ``` - ## 5.2.2   キューの実装 キューを実装するには、一方の端で要素を追加し、もう一方の端で要素を削除できるデータ構造が必要です。連結リストと配列の両方がこの要件を満たします。 @@ -611,12 +605,6 @@ comments: true [class]{LinkedListQueue}-[func]{} ``` -=== "Zig" - - ```zig title="linkedlist_queue.zig" - [class]{LinkedListQueue}-[func]{} - ``` - ### 2.   配列ベースの実装 配列の最初の要素を削除する時間計算量は$O(n)$で、デキュー操作が非効率になります。しかし、この問題は以下のように巧妙に回避できます。 @@ -915,12 +903,6 @@ comments: true [class]{ArrayQueue}-[func]{} ``` -=== "Zig" - - ```zig title="array_queue.zig" - [class]{ArrayQueue}-[func]{} - ``` - 上記のキュー実装にはまだ制限があります:長さが固定されています。しかし、この問題は解決が困難ではありません。配列を必要に応じて自動拡張できる動的配列に置き換えることができます。興味のある読者は自分で実装してみてください。 2つの実装の比較はスタックの場合と一貫しており、ここでは繰り返しません。 diff --git a/ja/docs/chapter_stack_and_queue/stack.md b/ja/docs/chapter_stack_and_queue/stack.md index 60bdf94c6..ba10a32bc 100644 --- a/ja/docs/chapter_stack_and_queue/stack.md +++ b/ja/docs/chapter_stack_and_queue/stack.md @@ -312,12 +312,6 @@ comments: true ``` -=== "Zig" - - ```zig title="stack.zig" - - ``` - ## 5.1.2   スタックの実装 スタックがどのように動作するかをより深く理解するために、自分でスタッククラスを実装してみましょう。 @@ -580,12 +574,6 @@ comments: true [class]{LinkedListStack}-[func]{} ``` -=== "Zig" - - ```zig title="linkedlist_stack.zig" - [class]{LinkedListStack}-[func]{} - ``` - ### 2.   配列ベースの実装 配列を使用してスタックを実装する場合、配列の末尾をスタックのトップと考えることができます。下図に示すように、プッシュとポップ操作は、それぞれ配列の末尾での要素の追加と削除に対応し、どちらも時間計算量$O(1)$です。 @@ -795,12 +783,6 @@ comments: true [class]{ArrayStack}-[func]{} ``` -=== "Zig" - - ```zig title="array_stack.zig" - [class]{ArrayStack}-[func]{} - ``` - ## 5.1.3   2つの実装の比較 **サポートされる操作** diff --git a/ja/docs/chapter_tree/array_representation_of_tree.md b/ja/docs/chapter_tree/array_representation_of_tree.md index daf8ba543..2a4b005d8 100644 --- a/ja/docs/chapter_tree/array_representation_of_tree.md +++ b/ja/docs/chapter_tree/array_representation_of_tree.md @@ -134,12 +134,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - - ``` - ![任意の種類の二分木の配列表現](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }

図 7-14   任意の種類の二分木の配列表現

@@ -480,12 +474,6 @@ comments: true [class]{ArrayBinaryTree}-[func]{} ``` -=== "Zig" - - ```zig title="array_binary_tree.zig" - [class]{ArrayBinaryTree}-[func]{} - ``` - ## 7.3.3   利点と制限 二分木の配列表現には以下の利点があります: diff --git a/ja/docs/chapter_tree/avl_tree.md b/ja/docs/chapter_tree/avl_tree.md index 3cf03b5b7..56ba2766f 100644 --- a/ja/docs/chapter_tree/avl_tree.md +++ b/ja/docs/chapter_tree/avl_tree.md @@ -225,12 +225,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ ``` -=== "Zig" - - ```zig title="" - - ``` - 「ノードの高さ」とは、そのノードから最も遠い葉ノードまでの距離、つまり通過する「辺」の数を指します。重要なのは、葉ノードの高さは$0$で、nullノードの高さは$-1$であることです。ノードの高さを取得し、更新するための2つのユーティリティ関数を作成します: === "Python" @@ -361,14 +355,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ [class]{AVLTree}-[func]{update_height} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{height} - - [class]{AVLTree}-[func]{updateHeight} - ``` - ### 2.   ノードの平衡因子 ノードの平衡因子は、そのノードの左部分木の高さから右部分木の高さを引いた値として定義され、nullノードの平衡因子は$0$として定義されます。後で使いやすくするため、ノードの平衡因子を取得する機能も関数にカプセル化します: @@ -471,12 +457,6 @@ AVL木に関連する操作ではノードの高さを取得する必要があ [class]{AVLTree}-[func]{balance_factor} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{balanceFactor} - ``` - !!! tip 平衡因子を$f$とすると、AVL木の任意のノードの平衡因子は$-1 \le f \le 1$を満たします。 @@ -626,12 +606,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 [class]{AVLTree}-[func]{right_rotate} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{rightRotate} - ``` - ### 2.   左回転 対応して、上記の不平衡二分木の「鏡像」を考慮すると、下図に示す「左回転」操作を実行する必要があります。 @@ -761,12 +735,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 [class]{AVLTree}-[func]{left_rotate} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{leftRotate} - ``` - ### 3.   左右回転 下図に示す不平衡ノード3の場合、左回転または右回転のいずれかだけでは部分木のバランスを回復できません。この場合、まず`child`に対して「左回転」を実行し、次に`node`に対して「右回転」を実行する必要があります。 @@ -965,12 +933,6 @@ AVL木の特徴的な機能は「回転」操作で、これは二分木の中 [class]{AVLTree}-[func]{rotate} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{rotate} - ``` - ## 7.5.3   AVL木の一般的な操作 ### 1.   ノードの挿入 @@ -1136,14 +1098,6 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 [class]{AVLTree}-[func]{insert_helper} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{insert} - - [class]{AVLTree}-[func]{insertHelper} - ``` - ### 2.   ノードの削除 同様に、二分探索木でのノード削除方法に基づいて、下から上へ回転操作を実行してすべての不平衡ノードのバランスを回復する必要があります。コードは以下の通りです: @@ -1359,14 +1313,6 @@ AVL木のノード挿入操作は二分探索木のそれと似ています。 [class]{AVLTree}-[func]{remove_helper} ``` -=== "Zig" - - ```zig title="avl_tree.zig" - [class]{AVLTree}-[func]{remove} - - [class]{AVLTree}-[func]{removeHelper} - ``` - ### 3.   ノードの検索 AVL木でのノード検索操作は二分探索木のそれと一致しており、ここでは詳述しません。 diff --git a/ja/docs/chapter_tree/binary_search_tree.md b/ja/docs/chapter_tree/binary_search_tree.md index d7ad3acf7..5e46323fe 100644 --- a/ja/docs/chapter_tree/binary_search_tree.md +++ b/ja/docs/chapter_tree/binary_search_tree.md @@ -167,12 +167,6 @@ comments: true [class]{BinarySearchTree}-[func]{search} ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{search} - ``` - ### 2.   ノードの挿入 挿入する要素`num`が与えられた場合、二分探索木の性質「左部分木 < 根ノード < 右部分木」を維持するため、挿入操作は下図に示すように進行します。 @@ -345,12 +339,6 @@ comments: true [class]{BinarySearchTree}-[func]{insert} ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{insert} - ``` - ノードの検索と同様に、ノードの挿入には$O(\log n)$の時間を使用します。 ### 3.   ノードの削除 @@ -615,12 +603,6 @@ comments: true [class]{BinarySearchTree}-[func]{remove} ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - [class]{BinarySearchTree}-[func]{remove} - ``` - ### 4.   中順走査は順序付けされている 下図に示すように、二分木の中順走査は「左 $\rightarrow$ 根 $\rightarrow$ 右」の走査順序に従い、二分探索木は「左子ノード $<$ 根ノード $<$ 右子ノード」のサイズ関係を満たします。 diff --git a/ja/docs/chapter_tree/binary_tree.md b/ja/docs/chapter_tree/binary_tree.md index add1638f8..0eb6777da 100644 --- a/ja/docs/chapter_tree/binary_tree.md +++ b/ja/docs/chapter_tree/binary_tree.md @@ -196,12 +196,6 @@ comments: true ``` -=== "Zig" - - ```zig title="" - - ``` - 各ノードは2つの参照(ポインタ)を持ち、それぞれ左の子ノード右の子ノードを指しています。このノードは、これら2つの子ノードの親ノードと呼ばれます。二分木のノードが与えられたとき、このノードの左の子とその下にあるすべてのノードで形成される木を、このノードの左部分木と呼びます。同様に、右部分木も定義できます。 **二分木では、葉ノードを除いて、他のすべてのノードは子ノードと空でない部分木を含みます。** 下図に示すように、「ノード2」を親ノードとして見ると、その左と右の子ノードはそれぞれ「ノード4」と「ノード5」です。左部分木は「ノード4」とその下にあるすべてのノードで形成され、右部分木は「ノード5」とその下にあるすべてのノードで形成されます。 @@ -443,12 +437,6 @@ comments: true ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - ### 2.   ノードの挿入と削除 連結リストと同様に、二分木でのノードの挿入と削除はポインタを変更することで実現できます。下図に例を示します。 @@ -603,12 +591,6 @@ comments: true ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - !!! tip ノードの挿入は二分木の元の論理構造を変更する可能性があり、ノードの削除は通常そのノードとそのすべての部分木を削除することになることに注意してください。したがって、二分木では、挿入と削除は通常一連の操作を通じて実行され、意味のある結果を得ます。 diff --git a/ja/docs/chapter_tree/binary_tree_traversal.md b/ja/docs/chapter_tree/binary_tree_traversal.md index 408daf1ec..f35084e85 100644 --- a/ja/docs/chapter_tree/binary_tree_traversal.md +++ b/ja/docs/chapter_tree/binary_tree_traversal.md @@ -147,12 +147,6 @@ comments: true [class]{}-[func]{level_order} ``` -=== "Zig" - - ```zig title="binary_tree_bfs.zig" - [class]{}-[func]{levelOrder} - ``` - ### 2.   計算量分析 - **時間計算量は$O(n)$**: すべてのノードが一度ずつ訪問され、$O(n)$の時間がかかります。ここで$n$はノード数です。 @@ -371,16 +365,6 @@ comments: true [class]{}-[func]{post_order} ``` -=== "Zig" - - ```zig title="binary_tree_dfs.zig" - [class]{}-[func]{preOrder} - - [class]{}-[func]{inOrder} - - [class]{}-[func]{postOrder} - ``` - !!! tip 深度優先探索は反復に基づいても実装できます。興味のある読者は自分で学習してください。 diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/array.md b/zh-Hant/docs/chapter_array_and_linkedlist/array.md index b75dafcfd..14eec21ce 100755 --- a/zh-Hant/docs/chapter_array_and_linkedlist/array.md +++ b/zh-Hant/docs/chapter_array_and_linkedlist/array.md @@ -132,14 +132,6 @@ comments: true nums = [1, 3, 2, 5, 4] ``` -=== "Zig" - - ```zig title="array.zig" - // 初始化陣列 - const arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 } - const nums = [_]i32{ 1, 3, 2, 5, 4 }; - ``` - ??? pythontutor "視覺化執行"
@@ -326,19 +318,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 隨機訪問元素 - fn randomAccess(nums: []const i32) i32 { - // 在區間 [0, nums.len) 中隨機抽取一個整數 - const random_index = std.crypto.random.intRangeLessThan(usize, 0, nums.len); - // 獲取並返回隨機元素 - const randomNum = nums[random_index]; - return randomNum; - } - ``` - ??? pythontutor "視覺化執行"
@@ -535,21 +514,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 在陣列的索引 index 處插入元素 num - fn insert(nums: []i32, num: i32, index: usize) void { - // 把索引 index 以及之後的所有元素向後移動一位 - var i = nums.len - 1; - while (i > index) : (i -= 1) { - nums[i] = nums[i - 1]; - } - // 將 num 賦給 index 處的元素 - nums[index] = num; - } - ``` - ??? pythontutor "視覺化執行"
@@ -720,19 +684,6 @@ comments: true end ``` -=== "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]; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -980,33 +931,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 走訪陣列 - fn traverse(nums: []const i32) void { - var count: i32 = 0; - - // 透過索引走訪陣列 - var i: usize = 0; - while (i < nums.len) : (i += 1) { - count += nums[i]; - } - - // 直接走訪陣列元素 - count = 0; - for (nums) |num| { - count += num; - } - - // 同時走訪資料索引和元素 - for (nums, 0..) |num, index| { - count += nums[index]; - count += num; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -1189,18 +1113,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 在陣列中查詢指定元素 - fn find(nums: []i32, target: i32) i32 { - for (nums, 0..) |num, i| { - if (num == target) return @intCast(i); - } - return -1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1431,23 +1343,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array.zig" - // 擴展陣列長度 - fn extend(allocator: std.mem.Allocator, nums: []const i32, enlarge: usize) ![]i32 { - // 初始化一個擴展長度後的陣列 - const res = try allocator.alloc(i32, nums.len + enlarge); - @memset(res, 0); - - // 將原陣列中的所有元素複製到新陣列 - std.mem.copyForwards(i32, res, nums); - - // 返回擴展後的新陣列 - return res; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md b/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md index 739198b3e..1737fc988 100755 --- a/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md +++ b/zh-Hant/docs/chapter_array_and_linkedlist/linked_list.md @@ -191,26 +191,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 鏈結串列節點類別 - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // 節點值 - next: ?*Self = null, // 指向下一節點的指標 - - // 建構子 - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - } - }; - } - ``` - ## 4.2.1   鏈結串列常用操作 ### 1.   初始化鏈結串列 @@ -439,23 +419,6 @@ comments: true n3.next = n4 ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 初始化鏈結串列 - // 初始化各個節點 - var n0 = inc.ListNode(i32){.val = 1}; - var n1 = inc.ListNode(i32){.val = 3}; - var n2 = inc.ListNode(i32){.val = 2}; - var n3 = inc.ListNode(i32){.val = 5}; - var n4 = inc.ListNode(i32){.val = 4}; - // 構建節點之間的引用 - n0.next = &n1; - n1.next = &n2; - n2.next = &n3; - n3.next = &n4; - ``` - ??? pythontutor "視覺化執行"
@@ -617,17 +580,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 在鏈結串列的節點 n0 之後插入節點 P - fn insert(comptime T: type, n0: *ListNode(T), P: *ListNode(T)) void { - const n1 = n0.next; - P.next = n1; - n0.next = P; - } - ``` - ??? pythontutor "視覺化執行"
@@ -831,18 +783,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 刪除鏈結串列的節點 n0 之後的首個節點 - fn remove(comptime T: type, n0: *ListNode(T)) void { - // n0 -> P -> n1 => n0 -> n1 - const P = n0.next; - const n1 = P.?.next; - n0.next = n1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1047,24 +987,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 訪問鏈結串列中索引為 index 的節點 - fn access(comptime T: type, node: *ListNode(T), index: i32) ?*ListNode(T) { - var head: ?*ListNode(T) = node; - var i: i32 = 0; - while (i < index) : (i += 1) { - if (head) |cur| { - head = cur.next; - } else { - return null; - } - } - return head; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1291,22 +1213,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linked_list.zig" - // 在鏈結串列中查詢值為 target 的首個節點 - fn find(comptime T: type, node: *ListNode(T), target: T) i32 { - var head: ?*ListNode(T) = node; - var index: i32 = 0; - while (head) |cur| { - if (cur.val == target) return index; - head = cur.next; - index += 1; - } - return -1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1537,28 +1443,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 雙向鏈結串列節點類別 - pub fn ListNode(comptime T: type) type { - return struct { - const Self = @This(); - - val: T = 0, // 節點值 - next: ?*Self = null, // 指向後繼節點的指標 - prev: ?*Self = null, // 指向前驅節點的指標 - - // 建構子 - pub fn init(self: *Self, x: i32) void { - self.val = x; - self.next = null; - self.prev = null; - } - }; - } - ``` - ![常見鏈結串列種類](linked_list.assets/linkedlist_common_types.png){ class="animation-figure" }

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

diff --git a/zh-Hant/docs/chapter_array_and_linkedlist/list.md b/zh-Hant/docs/chapter_array_and_linkedlist/list.md index e50e5e62e..fa27449b2 100755 --- a/zh-Hant/docs/chapter_array_and_linkedlist/list.md +++ b/zh-Hant/docs/chapter_array_and_linkedlist/list.md @@ -151,15 +151,6 @@ comments: true nums = [1, 3, 2, 5, 4] ``` -=== "Zig" - - ```zig title="list.zig" - // 初始化串列 - var nums = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums.deinit(); - try nums.appendSlice(&[_]i32{ 1, 3, 2, 5, 4 }); - ``` - ??? pythontutor "視覺化執行"
@@ -292,16 +283,6 @@ comments: true nums[1] = 0 # 將索引 1 處的元素更新為 0 ``` -=== "Zig" - - ```zig title="list.zig" - // 訪問元素 - var num = nums.items[1]; // 訪問索引 1 處的元素 - - // 更新元素 - nums.items[1] = 0; // 將索引 1 處的元素更新為 0 - ``` - ??? pythontutor "視覺化執行"
@@ -557,26 +538,6 @@ comments: true nums.delete_at(3) # 刪除索引 3 處的元素 ``` -=== "Zig" - - ```zig title="list.zig" - // 清空串列 - nums.clearRetainingCapacity(); - - // 在尾部新增元素 - try nums.append(1); - try nums.append(3); - try nums.append(2); - try nums.append(5); - try nums.append(4); - - // 在中間插入元素 - try nums.insert(3, 6); // 在索引 3 處插入數字 6 - - // 刪除元素 - _ = nums.orderedRemove(3); // 刪除索引 3 處的元素 - ``` - ??? pythontutor "視覺化執行"
@@ -779,23 +740,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="list.zig" - // 透過索引走訪串列 - var count: i32 = 0; - var i: i32 = 0; - while (i < nums.items.len) : (i += 1) { - count += nums[i]; - } - - // 直接走訪串列元素 - count = 0; - for (nums.items) |num| { - count += num; - } - ``` - ??? pythontutor "視覺化執行"
@@ -908,16 +852,6 @@ comments: true nums += nums1 ``` -=== "Zig" - - ```zig title="list.zig" - // 拼接兩個串列 - var nums1 = std.ArrayList(i32).init(std.heap.page_allocator); - defer nums1.deinit(); - try nums1.appendSlice(&[_]i32{ 6, 8, 7, 10, 9 }); - try nums.insertSlice(nums.items.len, nums1.items); // 將串列 nums1 拼接到 nums 之後 - ``` - ??? pythontutor "視覺化執行"
@@ -1017,13 +951,6 @@ comments: true nums = nums.sort { |a, b| a <=> b } # 排序後,串列元素從小到大排列 ``` -=== "Zig" - - ```zig title="list.zig" - // 排序串列 - std.sort.sort(i32, nums.items, {}, comptime std.sort.asc(i32)); - ``` - ??? pythontutor "視覺化執行"
@@ -2368,163 +2295,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_list.zig" - // 串列類別 - const MyList = struct { - const Self = @This(); - - items: []i32, // 陣列(儲存串列元素) - capacity: usize, // 串列容量 - allocator: std.mem.Allocator, // 記憶體分配器 - - extend_ratio: usize = 2, // 每次串列擴容的倍數 - - // 建構子(分配記憶體+初始化串列) - pub fn init(allocator: std.mem.Allocator) Self { - return Self{ - .items = &[_]i32{}, - .capacity = 0, - .allocator = allocator, - }; - } - - // 析構函式(釋放記憶體) - pub fn deinit(self: Self) void { - self.allocator.free(self.allocatedSlice()); - } - - // 在尾部新增元素 - pub fn add(self: *Self, item: i32) !void { - // 元素數量超出容量時,觸發擴容機制 - const newlen = self.items.len + 1; - try self.ensureTotalCapacity(newlen); - - // 更新元素 - self.items.len += 1; - const new_item_ptr = &self.items[self.items.len - 1]; - new_item_ptr.* = item; - } - - // 獲取串列長度(當前元素數量) - pub fn getSize(self: *Self) usize { - return self.items.len; - } - - // 獲取串列容量 - pub fn getCapacity(self: *Self) usize { - return self.capacity; - } - - // 訪問元素 - pub fn get(self: *Self, index: usize) i32 { - // 索引如果越界,則丟擲異常,下同 - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - return self.items[index]; - } - - // 更新元素 - pub fn set(self: *Self, index: usize, num: i32) void { - // 索引如果越界,則丟擲異常,下同 - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - self.items[index] = num; - } - - // 在中間插入元素 - pub fn insert(self: *Self, index: usize, item: i32) !void { - if (index < 0 or index >= self.items.len) { - @panic("索引越界"); - } - - // 元素數量超出容量時,觸發擴容機制 - const newlen = self.items.len + 1; - try self.ensureTotalCapacity(newlen); - - // 將索引 index 以及之後的元素都向後移動一位 - self.items.len += 1; - var i = self.items.len - 1; - while (i >= index) : (i -= 1) { - self.items[i] = self.items[i - 1]; - } - self.items[index] = item; - } - - // 刪除元素 - pub fn remove(self: *Self, index: usize) i32 { - if (index < 0 or index >= self.getSize()) { - @panic("索引越界"); - } - // 將索引 index 之後的元素都向前移動一位 - const item = self.items[index]; - var i = index; - while (i < self.items.len - 1) : (i += 1) { - self.items[i] = self.items[i + 1]; - } - self.items.len -= 1; - // 返回被刪除的元素 - return item; - } - - // 將串列轉換為陣列 - pub fn toArraySlice(self: *Self) ![]i32 { - return self.toOwnedSlice(false); - } - - // 返回新的切片並設定是否要重置或清空串列容器 - pub fn toOwnedSlice(self: *Self, clear: bool) ![]i32 { - const allocator = self.allocator; - const old_memory = self.allocatedSlice(); - if (allocator.remap(old_memory, self.items.len)) |new_items| { - if (clear) { - self.* = init(allocator); - } - return new_items; - } - - const new_memory = try allocator.alloc(i32, self.items.len); - @memcpy(new_memory, self.items); - if (clear) { - self.clearAndFree(); - } - return new_memory; - } - - // 串列擴容 - fn ensureTotalCapacity(self: *Self, new_capacity: usize) !void { - if (self.capacity >= new_capacity) return; - const capcacity = if (self.capacity == 0) 10 else self.capacity; - const better_capacity = capcacity * self.extend_ratio; - - const old_memory = self.allocatedSlice(); - if (self.allocator.remap(old_memory, better_capacity)) |new_memory| { - self.items.ptr = new_memory.ptr; - self.capacity = new_memory.len; - } else { - const new_memory = try self.allocator.alloc(i32, better_capacity); - @memcpy(new_memory[0..self.items.len], self.items); - self.allocator.free(old_memory); - self.items.ptr = new_memory.ptr; - self.capacity = new_memory.len; - } - } - - fn clearAndFree(self: *Self, allocator: std.mem.Allocator) void { - allocator.free(self.allocatedSlice()); - self.items.len = 0; - self.capacity = 0; - } - - fn allocatedSlice(self: Self) []i32 { - return self.items.ptr[0..self.capacity]; - } - }; - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md b/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md index c8f973b66..e488062a8 100644 --- a/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md +++ b/zh-Hant/docs/chapter_backtracking/backtracking_algorithm.md @@ -232,12 +232,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_i_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "視覺化執行"
@@ -549,12 +543,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_ii_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "視覺化執行"
@@ -909,12 +897,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_compact.zig" - [class]{}-[func]{preOrder} - ``` - ??? pythontutor "視覺化執行"
@@ -1266,12 +1248,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 接下來,我們基於框架程式碼來解決例題三。狀態 `state` 為節點走訪路徑,選擇 `choices` 為當前節點的左子節點和右子節點,結果 `res` 是路徑串列: === "Python" @@ -1948,22 +1924,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="preorder_traversal_iii_template.zig" - [class]{}-[func]{isSolution} - - [class]{}-[func]{recordSolution} - - [class]{}-[func]{isValid} - - [class]{}-[func]{makeChoice} - - [class]{}-[func]{undoChoice} - - [class]{}-[func]{backtrack} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_backtracking/n_queens_problem.md b/zh-Hant/docs/chapter_backtracking/n_queens_problem.md index 66937e0bc..17f214611 100644 --- a/zh-Hant/docs/chapter_backtracking/n_queens_problem.md +++ b/zh-Hant/docs/chapter_backtracking/n_queens_problem.md @@ -744,14 +744,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="n_queens.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{nQueens} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_backtracking/permutations_problem.md b/zh-Hant/docs/chapter_backtracking/permutations_problem.md index d2a2bb77d..b8d15b516 100644 --- a/zh-Hant/docs/chapter_backtracking/permutations_problem.md +++ b/zh-Hant/docs/chapter_backtracking/permutations_problem.md @@ -538,14 +538,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="permutations_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsI} - ``` - ??? pythontutor "視覺化執行"
@@ -1093,14 +1085,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="permutations_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{permutationsII} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md b/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md index 50f712adc..e1e0a00cd 100644 --- a/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md +++ b/zh-Hant/docs/chapter_backtracking/subset_sum_problem.md @@ -501,14 +501,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_i_naive.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumINaive} - ``` - ??? pythontutor "視覺化執行"
@@ -1070,14 +1062,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_i.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumI} - ``` - ??? pythontutor "視覺化執行"
@@ -1689,14 +1673,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="subset_sum_ii.zig" - [class]{}-[func]{backtrack} - - [class]{}-[func]{subsetSumII} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md b/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md index 23961fe71..bf5d4ecb2 100644 --- a/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md +++ b/zh-Hant/docs/chapter_computational_complexity/iteration_and_recursion.md @@ -198,20 +198,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // for 迴圈 - fn forLoop(n: usize) i32 { - var res: i32 = 0; - // 迴圈求和 1, 2, ..., n-1, n - for (1..n + 1) |i| { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "視覺化執行"
@@ -442,21 +428,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // while 迴圈 - fn whileLoop(n: i32) i32 { - var res: i32 = 0; - var i: i32 = 1; // 初始化條件變數 - // 迴圈求和 1, 2, ..., n-1, n - while (i <= n) : (i += 1) { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "視覺化執行"
@@ -702,25 +673,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // while 迴圈(兩次更新) - fn whileLoopII(n: i32) i32 { - var res: i32 = 0; - var i: i32 = 1; // 初始化條件變數 - // 迴圈求和 1, 4, 10, ... - while (i <= n) : ({ - // 更新條件變數 - i += 1; - i *= 2; - }) { - res += @intCast(i); - } - return res; - } - ``` - ??? pythontutor "視覺化執行"
@@ -956,26 +908,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="iteration.zig" - // 雙層 for 迴圈 - fn nestedForLoop(allocator: Allocator, n: usize) ![]const u8 { - var res = std.ArrayList(u8).init(allocator); - defer res.deinit(); - var buffer: [20]u8 = undefined; - // 迴圈 i = 1, 2, ..., n-1, n - for (1..n + 1) |i| { - // 迴圈 j = 1, 2, ..., n-1, n - for (1..n + 1) |j| { - const str = try std.fmt.bufPrint(&buffer, "({d}, {d}), ", .{ i, j }); - try res.appendSlice(str); - } - } - return res.toOwnedSlice(); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1199,22 +1131,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 遞迴函式 - fn recur(n: i32) i32 { - // 終止條件 - if (n == 1) { - return 1; - } - // 遞:遞迴呼叫 - const res = recur(n - 1); - // 迴:返回結果 - return n + res; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1428,20 +1344,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 尾遞迴函式 - fn tailRecur(n: i32, res: i32) i32 { - // 終止條件 - if (n == 0) { - return res; - } - // 尾遞迴呼叫 - return tailRecur(n - 1, res + n); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1668,22 +1570,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 費波那契數列 - fn fib(n: i32) i32 { - // 終止條件 f(1) = 0, f(2) = 1 - if (n == 1 or n == 2) { - return n - 1; - } - // 遞迴呼叫 f(n) = f(n-1) + f(n-2) - const res: i32 = fib(n - 1) + fib(n - 2); - // 返回結果 f(n) - return res; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2029,31 +1915,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="recursion.zig" - // 使用迭代模擬遞迴 - fn forLoopRecur(comptime n: i32) i32 { - // 使用一個顯式的堆疊來模擬系統呼叫堆疊 - var stack: [n]i32 = undefined; - var res: i32 = 0; - // 遞:遞迴呼叫 - var i: usize = n; - while (i > 0) { - stack[i - 1] = @intCast(i); - i -= 1; - } - // 迴:返回結果 - var index: usize = n; - while (index > 0) { - index -= 1; - res += stack[index]; - } - // res = 1+2+3+...+n - return res; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_computational_complexity/space_complexity.md b/zh-Hant/docs/chapter_computational_complexity/space_complexity.md index 5052dfed5..cc7ffae81 100755 --- a/zh-Hant/docs/chapter_computational_complexity/space_complexity.md +++ b/zh-Hant/docs/chapter_computational_complexity/space_complexity.md @@ -367,12 +367,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - ## 2.4.2   推算方法 空間複雜度的推算方法與時間複雜度大致相同,只需將統計物件從“操作數量”轉為“使用空間大小”。 @@ -535,12 +529,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - **在遞迴函式中,需要注意統計堆疊幀空間**。觀察以下程式碼: === "Python" @@ -813,12 +801,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 函式 `loop()` 和 `recur()` 的時間複雜度都為 $O(n)$ ,但空間複雜度不同。 - 函式 `loop()` 在迴圈中呼叫了 $n$ 次 `function()` ,每輪中的 `function()` 都返回並釋放了堆疊幀空間,因此空間複雜度仍為 $O(1)$ 。 @@ -1195,40 +1177,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 函式 - fn function() i32 { - // 執行某些操作 - return 0; - } - - // 常數階 - fn constant(n: i32) void { - // 常數、變數、物件佔用 O(1) 空間 - const a: i32 = 0; - const b: i32 = 0; - const nums = [_]i32{0} ** 10000; - const node = ListNode(i32){ .val = 0 }; - var i: i32 = 0; - // 迴圈中的變數佔用 O(1) 空間 - while (i < n) : (i += 1) { - const c: i32 = 0; - _ = c; - } - // 迴圈中的函式佔用 O(1) 空間 - i = 0; - while (i < n) : (i += 1) { - _ = function(); - } - _ = a; - _ = b; - _ = nums; - _ = node; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1507,33 +1455,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 線性階 - fn linear(comptime n: i32) !void { - // 長度為 n 的陣列佔用 O(n) 空間 - const 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; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1694,17 +1615,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 線性階(遞迴實現) - fn linearRecur(comptime n: i32) void { - std.debug.print("遞迴 n = {}\n", .{n}); - if (n == 1) return; - linearRecur(n - 1); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1938,27 +1848,6 @@ $$ end ``` -=== "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); - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -2140,18 +2029,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 平方階(遞迴實現) - fn quadraticRecur(comptime n: i32) i32 { - if (n <= 0) return 0; - const nums = [_]i32{0} ** n; - std.debug.print("遞迴 n = {} 中的 nums 長度 = {}\n", .{ n, nums.len }); - return quadraticRecur(n - 1); - } - ``` - ??? pythontutor "視覺化執行"
@@ -2346,20 +2223,6 @@ $$ end ``` -=== "Zig" - - ```zig title="space_complexity.zig" - // 指數階(建立滿二元樹) - fn buildTree(allocator: std.mem.Allocator, n: i32) !?*TreeNode(i32) { - if (n == 0) return null; - const root = try allocator.create(TreeNode(i32)); - root.init(0); - root.left = try buildTree(allocator, n - 1); - root.right = try buildTree(allocator, n - 1); - return root; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_computational_complexity/time_complexity.md b/zh-Hant/docs/chapter_computational_complexity/time_complexity.md index bcd24ed7e..cd1fdc679 100755 --- a/zh-Hant/docs/chapter_computational_complexity/time_complexity.md +++ b/zh-Hant/docs/chapter_computational_complexity/time_complexity.md @@ -205,21 +205,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - // 在某執行平臺下 - fn algorithm(n: usize) void { - var a: i32 = 2; // 1 ns - a += 1; // 1 ns - a *= 2; // 10 ns - // 迴圈 n 次 - for (0..n) |_| { // 1 ns - std.debug.print("{}\n", .{0}); // 5 ns - } - } - ``` - 根據以上方法,可以得到演算法的執行時間為 $(6n + 12)$ ns : $$ @@ -503,29 +488,6 @@ $$ end ``` -=== "Zig" - - ```zig title="" - // 演算法 A 的時間複雜度:常數階 - fn algorithm_A(n: usize) void { - _ = n; - std.debug.print("{}\n", .{0}); - } - // 演算法 B 的時間複雜度:線性階 - fn algorithm_B(n: i32) void { - for (0..n) |_| { - std.debug.print("{}\n", .{0}); - } - } - // 演算法 C 的時間複雜度:常數階 - fn algorithm_C(n: i32) void { - _ = n; - for (0..1000000) |_| { - std.debug.print("{}\n", .{0}); - } - } - ``` - 圖 2-7 展示了以上三個演算法函式的時間複雜度。 - 演算法 `A` 只有 $1$ 個列印操作,演算法執行時間不隨著 $n$ 增大而增長。我們稱此演算法的時間複雜度為“常數階”。 @@ -727,20 +689,6 @@ $$ end ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +1 - a += 1; // +1 - a *= 2; // +1 - // 迴圈 n 次 - for (0..n) |_| { // +1(每輪都執行 i ++) - std.debug.print("{}\n", .{0}); // +1 - } - } - ``` - 設演算法的操作數量是一個關於輸入資料大小 $n$ 的函式,記為 $T(n)$ ,則以上函式的操作數量為: $$ @@ -1020,27 +968,6 @@ $T(n)$ 是一次函式,說明其執行時間的增長趨勢是線性的,因 end ``` -=== "Zig" - - ```zig title="" - fn algorithm(n: usize) void { - var a: i32 = 1; // +0(技巧 1) - a = a + @as(i32, @intCast(n)); // +0(技巧 1) - - // +n(技巧 2) - for(0..(5 * n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - - // +n*n(技巧 3) - for(0..(2 * n)) |_| { - for(0..(n + 1)) |_| { - std.debug.print("{}\n", .{0}); - } - } - } - ``` - 以下公式展示了使用上述技巧前後的統計結果,兩者推算出的時間複雜度都為 $O(n^2)$ 。 $$ @@ -1266,22 +1193,6 @@ $$ end ``` -=== "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 < size) : (i += 1) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1448,20 +1359,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1651,20 +1548,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 線性階(走訪陣列) - fn arrayTraversal(nums: []i32) i32 { - var count: i32 = 0; - // 迴圈次數與陣列長度成正比 - for (nums) |_| { - count += 1; - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1883,24 +1766,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 平方階 - fn quadratic(n: i32) i32 { - var count: i32 = 0; - var i: i32 = 0; - // 迴圈次數與資料大小 n 成平方關係 - while (i < n) : (i += 1) { - var j: i32 = 0; - while (j < n) : (j += 1) { - count += 1; - } - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2210,31 +2075,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 平方階(泡沫排序) - fn bubbleSort(nums: []i32) i32 { - var count: i32 = 0; // 計數器 - // 外迴圈:未排序區間為 [0, i] - var i: i32 = @as(i32, @intCast(nums.len)) - 1; - while (i > 0) : (i -= 1) { - var j: usize = 0; - // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交換 nums[j] 與 nums[j + 1] - const tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - count += 3; // 元素交換包含 3 個單元操作 - } - } - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2484,27 +2324,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2657,16 +2476,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 指數階(遞迴實現) - fn expRecur(n: i32) i32 { - if (n == 1) return 1; - return expRecur(n - 1) + expRecur(n - 1) + 1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2864,20 +2673,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 對數階(迴圈實現) - fn logarithmic(n: i32) i32 { - var count: i32 = 0; - var n_var: i32 = n; - while (n_var > 1) : (n_var = @divTrunc(n_var, 2)) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -3029,16 +2824,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 對數階(遞迴實現) - fn logRecur(n: i32) i32 { - if (n <= 1) return 0; - return logRecur(@divTrunc(n, 2)) + 1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -3253,21 +3038,6 @@ $$ end ``` -=== "Zig" - - ```zig title="time_complexity.zig" - // 線性對數階 - fn linearLogRecur(n: i32) i32 { - if (n <= 1) return 1; - var count: i32 = linearLogRecur(@divTrunc(n, 2)) + linearLogRecur(@divTrunc(n, 2)); - var i: i32 = 0; - while (i < n) : (i += 1) { - count += 1; - } - return count; - } - ``` - ??? pythontutor "視覺化執行"
@@ -3494,22 +3264,6 @@ $$ end ``` -=== "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; - } - ``` - ??? pythontutor "視覺化執行"
@@ -3904,33 +3658,6 @@ $$ end ``` -=== "Zig" - - ```zig title="worst_best_time_complexity.zig" - // 生成一個陣列,元素為 { 1, 2, ..., n },順序被打亂 - fn randomNumbers(comptime n: usize) [n]i32 { - var nums: [n]i32 = undefined; - // 生成陣列 nums = { 1, 2, 3, ..., n } - for (&nums, 0..) |*num, i| { - num.* = @as(i32, @intCast(i)) + 1; - } - // 隨機打亂陣列元素 - const rand = std.crypto.random; - rand.shuffle(i32, &nums); - return nums; - } - - // 查詢陣列 nums 中數字 1 所在索引 - fn findOne(nums: []i32) i32 { - for (nums, 0..) |num, i| { - // 當元素 1 在陣列頭部時,達到最佳時間複雜度 O(1) - // 當元素 1 在陣列尾部時,達到最差時間複雜度 O(n) - if (num == 1) return @intCast(i); - } - return -1; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_data_structure/basic_data_types.md b/zh-Hant/docs/chapter_data_structure/basic_data_types.md index f21d7e535..032e2eefd 100644 --- a/zh-Hant/docs/chapter_data_structure/basic_data_types.md +++ b/zh-Hant/docs/chapter_data_structure/basic_data_types.md @@ -178,24 +178,6 @@ comments: true data = [0, 0.0, 'a', false, ListNode(0)] ``` -=== "Zig" - - ```zig title="" - const hello = [5]u8{ 'h', 'e', 'l', 'l', 'o' }; - // 以上程式碼展示了定義一個字面量陣列的方式,其中你可以選擇指明陣列的大小或者使用 _ 代替。使用 _ 時,Zig 會嘗試自動計算陣列的長度 - - const matrix_4x4 = [4][4]f32{ - [_]f32{ 1.0, 0.0, 0.0, 0.0 }, - [_]f32{ 0.0, 1.0, 0.0, 1.0 }, - [_]f32{ 0.0, 0.0, 1.0, 0.0 }, - [_]f32{ 0.0, 0.0, 0.0, 1.0 }, - }; - // 多維陣列(矩陣)實際上就是巢狀陣列,我們很容易就可以建立一個多維陣列出來 - - const array = [_:0]u8{ 1, 2, 3, 4 }; - // 定義一個哨兵終止陣列,本質上來說,這是為了相容 C 中的規定的字串結尾字元\0。我們使用語法 [N:x]T 來描述一個元素為型別 T,長度為 N 的陣列,在它對應 N 的索引處的值應該是 x - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md b/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md index d26c901af..aa39a291d 100644 --- a/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md +++ b/zh-Hant/docs/chapter_divide_and_conquer/binary_search_recur.md @@ -450,14 +450,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_recur.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{binarySearch} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md b/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md index a66b40d99..7fdf55899 100644 --- a/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md +++ b/zh-Hant/docs/chapter_divide_and_conquer/build_binary_tree_problem.md @@ -512,14 +512,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="build_tree.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{buildTree} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md b/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md index 99f0e0df4..66969873b 100644 --- a/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md +++ b/zh-Hant/docs/chapter_divide_and_conquer/hanota_problem.md @@ -542,16 +542,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hanota.zig" - [class]{}-[func]{move} - - [class]{}-[func]{dfs} - - [class]{}-[func]{solveHanota} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_divide_and_conquer/summary.md b/zh-Hant/docs/chapter_divide_and_conquer/summary.md index 8e2afdad6..6fb85ea5b 100644 --- a/zh-Hant/docs/chapter_divide_and_conquer/summary.md +++ b/zh-Hant/docs/chapter_divide_and_conquer/summary.md @@ -4,6 +4,8 @@ comments: true # 12.5   小結 +### 1.   重點回顧 + - 分治是一種常見的演算法設計策略,包括分(劃分)和治(合併)兩個階段,通常基於遞迴實現。 - 判斷是否是分治演算法問題的依據包括:問題能否分解、子問題是否獨立、子問題能否合併。 - 合併排序是分治策略的典型應用,其遞迴地將陣列劃分為等長的兩個子陣列,直到只剩一個元素時開始逐層合併,從而完成排序。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md b/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md index 0f3cc3d4e..fafbb9ab9 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md +++ b/zh-Hant/docs/chapter_dynamic_programming/dp_problem_features.md @@ -318,28 +318,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - // 爬樓梯最小代價:動態規劃 - fn minCostClimbingStairsDP(comptime cost: []i32) i32 { - comptime var n = cost.len - 1; - if (n == 1 or n == 2) { - return cost[n]; - } - // 初始化 dp 表,用於儲存子問題的解 - var dp = [_]i32{-1} ** (n + 1); - // 初始狀態:預設最小子問題的解 - dp[1] = cost[1]; - dp[2] = cost[2]; - // 狀態轉移:從較小子問題逐步求解較大子問題 - for (3..n + 1) |i| { - dp[i] = @min(dp[i - 1], dp[i - 2]) + cost[i]; - } - return dp[n]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -603,27 +581,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_cost_climbing_stairs_dp.zig" - // 爬樓梯最小代價:空間最佳化後的動態規劃 - fn minCostClimbingStairsDPComp(cost: []i32) i32 { - var n = cost.len - 1; - if (n == 1 or n == 2) { - return cost[n]; - } - var a = cost[1]; - var b = cost[2]; - // 狀態轉移:從較小子問題逐步求解較大子問題 - for (3..n + 1) |i| { - var tmp = b; - b = @min(a, tmp) + cost[i]; - a = tmp; - } - return b; - } - ``` - ??? pythontutor "視覺化執行"
@@ -985,30 +942,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_constraint_dp.zig" - // 帶約束爬樓梯:動態規劃 - fn climbingStairsConstraintDP(comptime n: usize) i32 { - if (n == 1 or n == 2) { - return 1; - } - // 初始化 dp 表,用於儲存子問題的解 - var dp = [_][3]i32{ [_]i32{ -1, -1, -1 } } ** (n + 1); - // 初始狀態:預設最小子問題的解 - dp[1][1] = 1; - dp[1][2] = 0; - dp[2][1] = 0; - dp[2][2] = 1; - // 狀態轉移:從較小子問題逐步求解較大子問題 - for (3..n + 1) |i| { - dp[i][1] = dp[i - 1][2]; - dp[i][2] = dp[i - 2][1] + dp[i - 2][2]; - } - return dp[n][1] + dp[n][2]; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md b/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md index 832f02b44..36970cbca 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md +++ b/zh-Hant/docs/chapter_dynamic_programming/dp_solution_pipeline.md @@ -383,27 +383,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路徑和:暴力搜尋 - fn minPathSumDFS(grid: anytype, i: i32, j: i32) i32 { - // 若為左上角單元格,則終止搜尋 - if (i == 0 and j == 0) { - return grid[0][0]; - } - // 若行列索引越界,則返回 +∞ 代價 - if (i < 0 or j < 0) { - return std.math.maxInt(i32); - } - // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 - var up = minPathSumDFS(grid, i - 1, j); - var left = minPathSumDFS(grid, i, j - 1); - // 返回從左上角到 (i, j) 的最小路徑代價 - return @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -763,33 +742,6 @@ $$ end ``` -=== "Zig" - - ```zig title="min_path_sum.zig" - // 最小路徑和:記憶化搜尋 - fn minPathSumDFSMem(grid: anytype, mem: anytype, i: i32, j: i32) i32 { - // 若為左上角單元格,則終止搜尋 - if (i == 0 and j == 0) { - return grid[0][0]; - } - // 若行列索引越界,則返回 +∞ 代價 - if (i < 0 or j < 0) { - return std.math.maxInt(i32); - } - // 若已有記錄,則直接返回 - if (mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] != -1) { - return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - // 計算從左上角到 (i-1, j) 和 (i, j-1) 的最小路徑代價 - var up = minPathSumDFSMem(grid, mem, i - 1, j); - var left = minPathSumDFSMem(grid, mem, i, j - 1); - // 返回從左上角到 (i, j) 的最小路徑代價 - // 記錄並返回左上角到 (i, j) 的最小路徑代價 - mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))] = @min(left, up) + grid[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - return mem[@as(usize, @intCast(i))][@as(usize, @intCast(j))]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1165,34 +1117,6 @@ $$ end ``` -=== "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]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1581,32 +1505,6 @@ $$ end ``` -=== "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]; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md b/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md index 66f60d0bf..9561efa93 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md +++ b/zh-Hant/docs/chapter_dynamic_programming/edit_distance_problem.md @@ -477,37 +477,6 @@ $$ end ``` -=== "Zig" - - ```zig title="edit_distance.zig" - // 編輯距離:動態規劃 - fn editDistanceDP(comptime s: []const u8, comptime t: []const u8) i32 { - comptime var n = s.len; - comptime var m = t.len; - var dp = [_][m + 1]i32{[_]i32{0} ** (m + 1)} ** (n + 1); - // 狀態轉移:首行首列 - for (1..n + 1) |i| { - dp[i][0] = @intCast(i); - } - for (1..m + 1) |j| { - dp[0][j] = @intCast(j); - } - // 狀態轉移:其餘行和列 - for (1..n + 1) |i| { - for (1..m + 1) |j| { - if (s[i - 1] == t[j - 1]) { - // 若兩字元相等,則直接跳過此兩字元 - dp[i][j] = dp[i - 1][j - 1]; - } else { - // 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1 - dp[i][j] = @min(@min(dp[i][j - 1], dp[i - 1][j]), dp[i - 1][j - 1]) + 1; - } - } - } - return dp[n][m]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -997,40 +966,6 @@ $$ end ``` -=== "Zig" - - ```zig title="edit_distance.zig" - // 編輯距離:空間最佳化後的動態規劃 - fn editDistanceDPComp(comptime s: []const u8, comptime t: []const u8) i32 { - comptime var n = s.len; - comptime var m = t.len; - var dp = [_]i32{0} ** (m + 1); - // 狀態轉移:首行 - for (1..m + 1) |j| { - dp[j] = @intCast(j); - } - // 狀態轉移:其餘行 - for (1..n + 1) |i| { - // 狀態轉移:首列 - var leftup = dp[0]; // 暫存 dp[i-1, j-1] - dp[0] = @intCast(i); - // 狀態轉移:其餘列 - for (1..m + 1) |j| { - var temp = dp[j]; - if (s[i - 1] == t[j - 1]) { - // 若兩字元相等,則直接跳過此兩字元 - dp[j] = leftup; - } else { - // 最少編輯步數 = 插入、刪除、替換這三種操作的最少編輯步數 + 1 - dp[j] = @min(@min(dp[j - 1], dp[j]), leftup) + 1; - } - leftup = temp; // 更新為下一輪的 dp[i-1, j-1] - } - } - return dp[m]; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md b/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md index 53deed7fa..91bc94b68 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md +++ b/zh-Hant/docs/chapter_dynamic_programming/intro_to_dynamic_programming.md @@ -420,39 +420,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="climbing_stairs_backtrack.zig" - // 回溯 - fn backtrack(choices: []i32, state: i32, n: i32, res: std.ArrayList(i32)) void { - // 當爬到第 n 階時,方案數量加 1 - if (state == n) { - res.items[0] = res.items[0] + 1; - } - // 走訪所有選擇 - for (choices) |choice| { - // 剪枝:不允許越過第 n 階 - if (state + choice > n) { - continue; - } - // 嘗試:做出選擇,更新狀態 - backtrack(choices, state + choice, n, res); - // 回退 - } - } - - // 爬樓梯:回溯 - fn climbingStairsBacktrack(n: usize) !i32 { - var choices = [_]i32{ 1, 2 }; // 可選擇向上爬 1 階或 2 階 - var state: i32 = 0; // 從第 0 階開始爬 - var res = std.ArrayList(i32).init(std.heap.page_allocator); - defer res.deinit(); - try res.append(0); // 使用 res[0] 記錄方案數量 - backtrack(&choices, state, @intCast(n), res); - return res.items[0]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -728,26 +695,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs.zig" - // 搜尋 - fn dfs(i: usize) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (i == 1 or i == 2) { - return @intCast(i); - } - // dp[i] = dp[i-1] + dp[i-2] - var count = dfs(i - 1) + dfs(i - 2); - return count; - } - - // 爬樓梯:搜尋 - fn climbingStairsDFS(comptime n: usize) i32 { - return dfs(n); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1115,34 +1062,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dfs_mem.zig" - // 記憶化搜尋 - fn dfs(i: usize, mem: []i32) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (i == 1 or i == 2) { - return @intCast(i); - } - // 若存在記錄 dp[i] ,則直接返回之 - if (mem[i] != -1) { - return mem[i]; - } - // dp[i] = dp[i-1] + dp[i-2] - var count = dfs(i - 1, mem) + dfs(i - 2, mem); - // 記錄 dp[i] - mem[i] = count; - return count; - } - - // 爬樓梯:記憶化搜尋 - fn climbingStairsDFSMem(comptime n: usize) i32 { - // mem[i] 記錄爬到第 i 階的方案總數,-1 代表無記錄 - var mem = [_]i32{ -1 } ** (n + 1); - return dfs(n, &mem); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1419,28 +1338,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - // 爬樓梯:動態規劃 - fn climbingStairsDP(comptime n: usize) i32 { - // 已知 dp[1] 和 dp[2] ,返回之 - if (n == 1 or n == 2) { - return @intCast(n); - } - // 初始化 dp 表,用於儲存子問題的解 - var dp = [_]i32{-1} ** (n + 1); - // 初始狀態:預設最小子問題的解 - dp[1] = 1; - dp[2] = 2; - // 狀態轉移:從較小子問題逐步求解較大子問題 - for (3..n + 1) |i| { - dp[i] = dp[i - 1] + dp[i - 2]; - } - return dp[n]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1678,25 +1575,6 @@ $$ end ``` -=== "Zig" - - ```zig title="climbing_stairs_dp.zig" - // 爬樓梯:空間最佳化後的動態規劃 - fn climbingStairsDPComp(comptime n: usize) i32 { - if (n == 1 or n == 2) { - return @intCast(n); - } - var a: i32 = 1; - var b: i32 = 2; - for (3..n + 1) |_| { - var tmp = b; - b = a + b; - a = tmp; - } - return b; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md b/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md index b20271fae..b5a79f876 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md +++ b/zh-Hant/docs/chapter_dynamic_programming/knapsack_problem.md @@ -338,27 +338,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:暴力搜尋 - fn knapsackDFS(wgt: []i32, val: []i32, i: usize, c: usize) i32 { - // 若已選完所有物品或背包無剩餘容量,則返回價值 0 - if (i == 0 or c == 0) { - return 0; - } - // 若超過背包容量,則只能選擇不放入背包 - if (wgt[i - 1] > c) { - return knapsackDFS(wgt, val, i - 1, c); - } - // 計算不放入和放入物品 i 的最大價值 - var no = knapsackDFS(wgt, val, i - 1, c); - var yes = knapsackDFS(wgt, val, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; - // 返回兩種方案中價值更大的那一個 - return @max(no, yes); - } - ``` - ??? pythontutor "視覺化執行"
@@ -727,32 +706,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:記憶化搜尋 - fn knapsackDFSMem(wgt: []i32, val: []i32, mem: anytype, i: usize, c: usize) i32 { - // 若已選完所有物品或背包無剩餘容量,則返回價值 0 - if (i == 0 or c == 0) { - return 0; - } - // 若已有記錄,則直接返回 - if (mem[i][c] != -1) { - return mem[i][c]; - } - // 若超過背包容量,則只能選擇不放入背包 - if (wgt[i - 1] > c) { - return knapsackDFSMem(wgt, val, mem, i - 1, c); - } - // 計算不放入和放入物品 i 的最大價值 - var no = knapsackDFSMem(wgt, val, mem, i - 1, c); - var yes = knapsackDFSMem(wgt, val, mem, i - 1, c - @as(usize, @intCast(wgt[i - 1]))) + val[i - 1]; - // 記錄並返回兩種方案中價值更大的那一個 - mem[i][c] = @max(no, yes); - return mem[i][c]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1104,30 +1057,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:動態規劃 - fn knapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); - // 狀態轉移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超過背包容量,則不選物品 i - dp[i][c] = dp[i - 1][c]; - } else { - // 不選和選物品 i 這兩種方案的較大值 - dp[i][c] = @max(dp[i - 1][c], dp[i - 1][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[n][cap]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1510,29 +1439,6 @@ $$ end ``` -=== "Zig" - - ```zig title="knapsack.zig" - // 0-1 背包:空間最佳化後的動態規劃 - fn knapsackDPComp(wgt: []i32, val: []i32, comptime cap: usize) i32 { - var n = wgt.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (cap + 1); - // 狀態轉移 - for (1..n + 1) |i| { - // 倒序走訪 - var c = cap; - while (c > 0) : (c -= 1) { - if (wgt[i - 1] < c) { - // 不選和選物品 i 這兩種方案的較大值 - dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[cap]; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_dynamic_programming/summary.md b/zh-Hant/docs/chapter_dynamic_programming/summary.md index f158d187a..341ef09c4 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/summary.md +++ b/zh-Hant/docs/chapter_dynamic_programming/summary.md @@ -4,6 +4,8 @@ comments: true # 14.7   小結 +### 1.   重點回顧 + - 動態規劃對問題進行分解,並透過儲存子問題的解來規避重複計算,提高計算效率。 - 不考慮時間的前提下,所有動態規劃問題都可以用回溯(暴力搜尋)進行求解,但遞迴樹中存在大量的重疊子問題,效率極低。透過引入記憶化串列,可以儲存所有計算過的子問題的解,從而保證重疊子問題只被計算一次。 - 記憶化搜尋是一種從頂至底的遞迴式解法,而與之對應的動態規劃是一種從底至頂的遞推式解法,其如同“填寫表格”一樣。由於當前狀態僅依賴某些區域性狀態,因此我們可以消除 $dp$ 表的一個維度,從而降低空間複雜度。 diff --git a/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md b/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md index c807fec71..f7e21f2ae 100644 --- a/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md +++ b/zh-Hant/docs/chapter_dynamic_programming/unbounded_knapsack_problem.md @@ -371,30 +371,6 @@ $$ end ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - // 完全背包:動態規劃 - fn unboundedKnapsackDP(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_][cap + 1]i32{[_]i32{0} ** (cap + 1)} ** (n + 1); - // 狀態轉移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超過背包容量,則不選物品 i - dp[i][c] = dp[i - 1][c]; - } else { - // 不選和選物品 i 這兩種方案的較大值 - dp[i][c] = @max(dp[i - 1][c], dp[i][c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[n][cap]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -769,30 +745,6 @@ $$ end ``` -=== "Zig" - - ```zig title="unbounded_knapsack.zig" - // 完全背包:空間最佳化後的動態規劃 - fn unboundedKnapsackDPComp(comptime wgt: []i32, val: []i32, comptime cap: usize) i32 { - comptime var n = wgt.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (cap + 1); - // 狀態轉移 - for (1..n + 1) |i| { - for (1..cap + 1) |c| { - if (wgt[i - 1] > c) { - // 若超過背包容量,則不選物品 i - dp[c] = dp[c]; - } else { - // 不選和選物品 i 這兩種方案的較大值 - dp[c] = @max(dp[c], dp[c - @as(usize, @intCast(wgt[i - 1]))] + val[i - 1]); - } - } - } - return dp[cap]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1240,39 +1192,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change.zig" - // 零錢兌換:動態規劃 - fn coinChangeDP(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - comptime var max = amt + 1; - // 初始化 dp 表 - var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); - // 狀態轉移:首行首列 - for (1..amt + 1) |a| { - dp[0][a] = max; - } - // 狀態轉移:其餘行和列 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超過目標金額,則不選硬幣 i - dp[i][a] = dp[i - 1][a]; - } else { - // 不選和選硬幣 i 這兩種方案的較小值 - dp[i][a] = @min(dp[i - 1][a], dp[i][a - @as(usize, @intCast(coins[i - 1]))] + 1); - } - } - } - if (dp[n][amt] != max) { - return @intCast(dp[n][amt]); - } else { - return -1; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -1688,37 +1607,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change.zig" - // 零錢兌換:空間最佳化後的動態規劃 - fn coinChangeDPComp(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - comptime var max = amt + 1; - // 初始化 dp 表 - var dp = [_]i32{0} ** (amt + 1); - @memset(&dp, max); - dp[0] = 0; - // 狀態轉移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超過目標金額,則不選硬幣 i - dp[a] = dp[a]; - } else { - // 不選和選硬幣 i 這兩種方案的較小值 - dp[a] = @min(dp[a], dp[a - @as(usize, @intCast(coins[i - 1]))] + 1); - } - } - } - if (dp[amt] != max) { - return @intCast(dp[amt]); - } else { - return -1; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -2121,34 +2009,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change_ii.zig" - // 零錢兌換 II:動態規劃 - fn coinChangeIIDP(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - // 初始化 dp 表 - var dp = [_][amt + 1]i32{[_]i32{0} ** (amt + 1)} ** (n + 1); - // 初始化首列 - for (0..n + 1) |i| { - dp[i][0] = 1; - } - // 狀態轉移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超過目標金額,則不選硬幣 i - dp[i][a] = dp[i - 1][a]; - } else { - // 不選和選硬幣 i 這兩種方案的較小值 - dp[i][a] = dp[i - 1][a] + dp[i][a - @as(usize, @intCast(coins[i - 1]))]; - } - } - } - return dp[n][amt]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2485,31 +2345,6 @@ $$ end ``` -=== "Zig" - - ```zig title="coin_change_ii.zig" - // 零錢兌換 II:空間最佳化後的動態規劃 - fn coinChangeIIDPComp(comptime coins: []i32, comptime amt: usize) i32 { - comptime var n = coins.len; - // 初始化 dp 表 - var dp = [_]i32{0} ** (amt + 1); - dp[0] = 1; - // 狀態轉移 - for (1..n + 1) |i| { - for (1..amt + 1) |a| { - if (coins[i - 1] > @as(i32, @intCast(a))) { - // 若超過目標金額,則不選硬幣 i - dp[a] = dp[a]; - } else { - // 不選和選硬幣 i 這兩種方案的較小值 - dp[a] = dp[a] + dp[a - @as(usize, @intCast(coins[i - 1]))]; - } - } - } - return dp[amt]; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_graph/graph_operations.md b/zh-Hant/docs/chapter_graph/graph_operations.md index 00b7ed23b..6c2f1270a 100644 --- a/zh-Hant/docs/chapter_graph/graph_operations.md +++ b/zh-Hant/docs/chapter_graph/graph_operations.md @@ -1206,12 +1206,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="graph_adjacency_matrix.zig" - [class]{GraphAdjMat}-[func]{} - ``` - ??? pythontutor "視覺化執行"
@@ -2370,12 +2364,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="graph_adjacency_list.zig" - [class]{GraphAdjList}-[func]{} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_graph/graph_traversal.md b/zh-Hant/docs/chapter_graph/graph_traversal.md index 9e4c7f2fe..2e2c8f3da 100644 --- a/zh-Hant/docs/chapter_graph/graph_traversal.md +++ b/zh-Hant/docs/chapter_graph/graph_traversal.md @@ -475,12 +475,6 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入 end ``` -=== "Zig" - - ```zig title="graph_bfs.zig" - [class]{}-[func]{graphBFS} - ``` - ??? pythontutor "視覺化執行"
@@ -941,14 +935,6 @@ BFS 通常藉助佇列來實現,程式碼如下所示。佇列具有“先入 end ``` -=== "Zig" - - ```zig title="graph_dfs.zig" - [class]{}-[func]{dfs} - - [class]{}-[func]{graphDFS} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md b/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md index 181026fa7..991a517db 100644 --- a/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md +++ b/zh-Hant/docs/chapter_greedy/fractional_knapsack_problem.md @@ -531,14 +531,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="fractional_knapsack.zig" - [class]{Item}-[func]{} - - [class]{}-[func]{fractionalKnapsack} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_greedy/greedy_algorithm.md b/zh-Hant/docs/chapter_greedy/greedy_algorithm.md index 7ba687ab6..39eab3407 100644 --- a/zh-Hant/docs/chapter_greedy/greedy_algorithm.md +++ b/zh-Hant/docs/chapter_greedy/greedy_algorithm.md @@ -330,12 +330,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="coin_change_greedy.zig" - [class]{}-[func]{coinChangeGreedy} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_greedy/max_capacity_problem.md b/zh-Hant/docs/chapter_greedy/max_capacity_problem.md index 8409b3e71..0981b8f9f 100644 --- a/zh-Hant/docs/chapter_greedy/max_capacity_problem.md +++ b/zh-Hant/docs/chapter_greedy/max_capacity_problem.md @@ -421,12 +421,6 @@ $$ end ``` -=== "Zig" - - ```zig title="max_capacity.zig" - [class]{}-[func]{maxCapacity} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md b/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md index fb12f8854..22f1fdefe 100644 --- a/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md +++ b/zh-Hant/docs/chapter_greedy/max_product_cutting_problem.md @@ -386,12 +386,6 @@ $$ end ``` -=== "Zig" - - ```zig title="max_product_cutting.zig" - [class]{}-[func]{maxProductCutting} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_greedy/summary.md b/zh-Hant/docs/chapter_greedy/summary.md index 3d1f3b71c..04785ed6a 100644 --- a/zh-Hant/docs/chapter_greedy/summary.md +++ b/zh-Hant/docs/chapter_greedy/summary.md @@ -4,6 +4,8 @@ comments: true # 15.5   小結 +### 1.   重點回顧 + - 貪婪演算法通常用於解決最最佳化問題,其原理是在每個決策階段都做出區域性最優的決策,以期獲得全域性最優解。 - 貪婪演算法會迭代地做出一個又一個的貪婪選擇,每輪都將問題轉化成一個規模更小的子問題,直到問題被解決。 - 貪婪演算法不僅實現簡單,還具有很高的解題效率。相比於動態規劃,貪婪演算法的時間複雜度通常更低。 diff --git a/zh-Hant/docs/chapter_hashing/hash_algorithm.md b/zh-Hant/docs/chapter_hashing/hash_algorithm.md index ff4dc0657..2784cc502 100644 --- a/zh-Hant/docs/chapter_hashing/hash_algorithm.md +++ b/zh-Hant/docs/chapter_hashing/hash_algorithm.md @@ -642,18 +642,6 @@ index = hash(key) % capacity end ``` -=== "Zig" - - ```zig title="simple_hash.zig" - [class]{}-[func]{addHash} - - [class]{}-[func]{mulHash} - - [class]{}-[func]{xorHash} - - [class]{}-[func]{rotHash} - ``` - ??? pythontutor "視覺化執行"
@@ -1012,12 +1000,6 @@ $$ # 節點物件 # 的雜湊值為 4302940560806366381 ``` -=== "Zig" - - ```zig title="built_in_hash.zig" - - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_hashing/hash_collision.md b/zh-Hant/docs/chapter_hashing/hash_collision.md index 48ec626ed..cc1cb898e 100644 --- a/zh-Hant/docs/chapter_hashing/hash_collision.md +++ b/zh-Hant/docs/chapter_hashing/hash_collision.md @@ -1517,12 +1517,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hash_map_chaining.zig" - [class]{HashMapChaining}-[func]{} - ``` - ??? pythontutor "視覺化執行"
@@ -3288,12 +3282,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="hash_map_open_addressing.zig" - [class]{HashMapOpenAddressing}-[func]{} - ``` - ### 2.   平方探測 平方探測與線性探查類似,都是開放定址的常見策略之一。當發生衝突時,平方探測不是簡單地跳過一個固定的步數,而是跳過“探測次數的平方”的步數,即 $1, 4, 9, \dots$ 步。 diff --git a/zh-Hant/docs/chapter_hashing/hash_map.md b/zh-Hant/docs/chapter_hashing/hash_map.md index 29d7a4a7f..f95f82e2a 100755 --- a/zh-Hant/docs/chapter_hashing/hash_map.md +++ b/zh-Hant/docs/chapter_hashing/hash_map.md @@ -323,12 +323,6 @@ comments: true hmap.delete(10583) ``` -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -551,12 +545,6 @@ comments: true hmap.values.each { |val| puts val } ``` -=== "Zig" - - ```zig title="hash_map.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -1765,115 +1753,6 @@ index = hash(key) % capacity end ``` -=== "Zig" - - ```zig title="array_hash_map.zig" - // 鍵值對 - const Pair = struct { - key: usize = undefined, - val: []const u8 = undefined, - - pub fn init(key: usize, val: []const u8) Pair { - return Pair { - .key = key, - .val = val, - }; - } - }; - - // 基於陣列實現的雜湊表 - fn ArrayHashMap(comptime T: type) type { - return struct { - bucket: ?std.ArrayList(?T) = null, - mem_allocator: std.mem.Allocator = undefined, - - const Self = @This(); - - // 建構子 - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - self.mem_allocator = allocator; - // 初始化一個長度為 100 的桶(陣列) - self.bucket = std.ArrayList(?T).init(self.mem_allocator); - var i: i32 = 0; - while (i < 100) : (i += 1) { - try self.bucket.?.append(null); - } - } - - // 析構函式 - pub fn deinit(self: *Self) void { - if (self.bucket != null) self.bucket.?.deinit(); - } - - // 雜湊函式 - fn hashFunc(key: usize) usize { - var index = key % 100; - return index; - } - - // 查詢操作 - pub fn get(self: *Self, key: usize) []const u8 { - var index = hashFunc(key); - var pair = self.bucket.?.items[index]; - return pair.?.val; - } - - // 新增操作 - pub fn put(self: *Self, key: usize, val: []const u8) !void { - var pair = Pair.init(key, val); - var index = hashFunc(key); - self.bucket.?.items[index] = pair; - } - - // 刪除操作 - pub fn remove(self: *Self, key: usize) !void { - var index = hashFunc(key); - // 置為 null ,代表刪除 - self.bucket.?.items[index] = null; - } - - // 獲取所有鍵值對 - pub fn pairSet(self: *Self) !std.ArrayList(T) { - var entry_set = std.ArrayList(T).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try entry_set.append(item.?); - } - return entry_set; - } - - // 獲取所有鍵 - pub fn keySet(self: *Self) !std.ArrayList(usize) { - var key_set = std.ArrayList(usize).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try key_set.append(item.?.key); - } - return key_set; - } - - // 獲取所有值 - pub fn valueSet(self: *Self) !std.ArrayList([]const u8) { - var value_set = std.ArrayList([]const u8).init(self.mem_allocator); - for (self.bucket.?.items) |item| { - if (item == null) continue; - try value_set.append(item.?.val); - } - return value_set; - } - - // 列印雜湊表 - pub fn print(self: *Self) !void { - var entry_set = try self.pairSet(); - defer entry_set.deinit(); - for (entry_set.items) |item| { - std.debug.print("{} -> {s}\n", .{item.key, item.val}); - } - } - }; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_heap/build_heap.md b/zh-Hant/docs/chapter_heap/build_heap.md index dd57fd946..1dd769639 100644 --- a/zh-Hant/docs/chapter_heap/build_heap.md +++ b/zh-Hant/docs/chapter_heap/build_heap.md @@ -322,23 +322,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 建構子,根據輸入串列建堆積 - fn init(self: *Self, allocator: std.mem.Allocator, nums: []const T) !void { - if (self.max_heap != null) return; - self.max_heap = std.ArrayList(T).init(allocator); - // 將串列元素原封不動新增進堆積 - try self.max_heap.?.appendSlice(nums); - // 堆積化除葉節點以外的其他所有節點 - var i: usize = parent(self.size() - 1) + 1; - while (i > 0) : (i -= 1) { - try self.siftDown(i - 1); - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_heap/heap.md b/zh-Hant/docs/chapter_heap/heap.md index 24e548dbc..b42427c5c 100644 --- a/zh-Hant/docs/chapter_heap/heap.md +++ b/zh-Hant/docs/chapter_heap/heap.md @@ -418,12 +418,6 @@ comments: true # Ruby 未提供內建 Heap 類別 ``` -=== "Zig" - - ```zig title="heap.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -692,26 +686,6 @@ comments: true end ``` -=== "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.   訪問堆積頂元素 堆積頂元素即為二元樹的根節點,也就是串列的首個元素: @@ -832,15 +806,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 訪問堆積頂元素 - fn peek(self: *Self) T { - return self.max_heap.?.items[0]; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1246,33 +1211,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 元素入堆積 - fn push(self: *Self, val: T) !void { - // 新增節點 - try self.max_heap.?.append(val); - // 從底至頂堆積化 - try self.siftUp(self.size() - 1); - } - - // 從節點 i 開始,從底至頂堆積化 - fn siftUp(self: *Self, i_: usize) !void { - var i = i_; - while (true) { - // 獲取節點 i 的父節點 - var p = parent(i); - // 當“越過根節點”或“節點無須修復”時,結束堆積化 - if (p < 0 or self.max_heap.?.items[i] <= self.max_heap.?.items[p]) break; - // 交換兩節點 - try self.swap(i, p); - // 迴圈向上堆積化 - i = p; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -1830,43 +1768,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="my_heap.zig" - // 元素出堆積 - fn pop(self: *Self) !T { - // 判斷處理 - if (self.isEmpty()) unreachable; - // 交換根節點與最右葉節點(交換首元素與尾元素) - try self.swap(0, self.size() - 1); - // 刪除節點 - var val = self.max_heap.?.pop(); - // 從頂至底堆積化 - try self.siftDown(0); - // 返回堆積頂元素 - return val; - } - - // 從節點 i 開始,從頂至底堆積化 - fn siftDown(self: *Self, i_: usize) !void { - var i = i_; - while (true) { - // 判斷節點 i, l, r 中值最大的節點,記為 ma - var l = left(i); - var r = right(i); - var ma = i; - if (l < self.size() and self.max_heap.?.items[l] > self.max_heap.?.items[ma]) ma = l; - if (r < self.size() and self.max_heap.?.items[r] > self.max_heap.?.items[ma]) ma = r; - // 若節點 i 最大或索引 l, r 越界,則無須繼續堆積化,跳出 - if (ma == i) break; - // 交換兩節點 - try self.swap(i, ma); - // 迴圈向下堆積化 - i = ma; - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_heap/top_k.md b/zh-Hant/docs/chapter_heap/top_k.md index 7e275c267..6b86e1c38 100644 --- a/zh-Hant/docs/chapter_heap/top_k.md +++ b/zh-Hant/docs/chapter_heap/top_k.md @@ -461,12 +461,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="top_k.zig" - [class]{}-[func]{topKHeap} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_introduction/summary.md b/zh-Hant/docs/chapter_introduction/summary.md index f98dd0ee4..367b9a1b6 100644 --- a/zh-Hant/docs/chapter_introduction/summary.md +++ b/zh-Hant/docs/chapter_introduction/summary.md @@ -4,6 +4,8 @@ comments: true # 1.3   小結 +### 1.   重點回顧 + - 演算法在日常生活中無處不在,並不是遙不可及的高深知識。實際上,我們已經在不知不覺中學會了許多演算法,用以解決生活中的大小問題。 - 查字典的原理與二分搜尋演算法相一致。二分搜尋演算法體現了分而治之的重要演算法思想。 - 整理撲克的過程與插入排序演算法非常類似。插入排序演算法適合排序小型資料集。 @@ -12,7 +14,7 @@ comments: true - 資料結構與演算法緊密相連。資料結構是演算法的基石,而演算法為資料結構注入生命力。 - 我們可以將資料結構與演算法類比為拼裝積木,積木代表資料,積木的形狀和連線方式等代表資料結構,拼裝積木的步驟則對應演算法。 -### 1.   Q & A +### 2.   Q & A **Q**:作為一名程式設計師,我在日常工作中從未用演算法解決過問題,常用演算法都被程式語言封裝好了,直接用就可以了;這是否意味著我們工作中的問題還沒有到達需要演算法的程度? diff --git a/zh-Hant/docs/chapter_preface/suggestions.md b/zh-Hant/docs/chapter_preface/suggestions.md index 02aee4e8d..5940a37be 100644 --- a/zh-Hant/docs/chapter_preface/suggestions.md +++ b/zh-Hant/docs/chapter_preface/suggestions.md @@ -184,17 +184,6 @@ comments: true # 註釋 ``` -=== "Zig" - - ```zig title="" - // 標題註釋,用於標註函式、類別、測試樣例等 - - // 內容註釋,用於詳解程式碼 - - // 多行 - // 註釋 - ``` - ## 0.2.2   在動畫圖解中高效學習 相較於文字,影片和圖片具有更高的資訊密度和結構化程度,更易於理解。在本書中,**重點和難點知識將主要透過動畫以圖解形式展示**,而文字則作為解釋與補充。 diff --git a/zh-Hant/docs/chapter_preface/summary.md b/zh-Hant/docs/chapter_preface/summary.md index 1d371e534..5f37f60b3 100644 --- a/zh-Hant/docs/chapter_preface/summary.md +++ b/zh-Hant/docs/chapter_preface/summary.md @@ -4,6 +4,8 @@ comments: true # 0.3   小結 +### 1.   重點回顧 + - 本書的主要受眾是演算法初學者。如果你已有一定基礎,本書能幫助你系統回顧演算法知識,書中源程式碼也可作為“刷題工具庫”使用。 - 書中內容主要包括複雜度分析、資料結構和演算法三部分,涵蓋了該領域的大部分主題。 - 對於演算法新手,在初學階段閱讀一本入門書至關重要,可以少走許多彎路。 diff --git a/zh-Hant/docs/chapter_searching/binary_search.md b/zh-Hant/docs/chapter_searching/binary_search.md index a8190c108..f20fe0d38 100755 --- a/zh-Hant/docs/chapter_searching/binary_search.md +++ b/zh-Hant/docs/chapter_searching/binary_search.md @@ -362,30 +362,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search.zig" - // 二分搜尋(雙閉區間) - fn binarySearch(comptime T: type, nums: std.ArrayList(T), target: T) T { - // 初始化雙閉區間 [0, n-1] ,即 i, j 分別指向陣列首元素、尾元素 - var i: usize = 0; - var j: usize = nums.items.len - 1; - // 迴圈,當搜尋區間為空時跳出(當 i > j 時為空) - while (i <= j) { - var m = i + (j - i) / 2; // 計算中點索引 m - if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j] 中 - i = m + 1; - } else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m-1] 中 - j = m - 1; - } else { // 找到目標元素,返回其索引 - return @intCast(m); - } - } - // 未找到目標元素,返回 -1 - return -1; - } - ``` - ??? pythontutor "視覺化執行"
@@ -710,30 +686,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search.zig" - // 二分搜尋(左閉右開區間) - fn binarySearchLCRO(comptime T: type, nums: std.ArrayList(T), target: T) T { - // 初始化左閉右開區間 [0, n) ,即 i, j 分別指向陣列首元素、尾元素+1 - var i: usize = 0; - var j: usize = nums.items.len; - // 迴圈,當搜尋區間為空時跳出(當 i = j 時為空) - while (i <= j) { - var m = i + (j - i) / 2; // 計算中點索引 m - if (nums.items[m] < target) { // 此情況說明 target 在區間 [m+1, j) 中 - i = m + 1; - } else if (nums.items[m] > target) { // 此情況說明 target 在區間 [i, m) 中 - j = m; - } else { // 找到目標元素,返回其索引 - return @intCast(m); - } - } - // 未找到目標元素,返回 -1 - return -1; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_searching/binary_search_edge.md b/zh-Hant/docs/chapter_searching/binary_search_edge.md index f29a88954..a07e81adb 100644 --- a/zh-Hant/docs/chapter_searching/binary_search_edge.md +++ b/zh-Hant/docs/chapter_searching/binary_search_edge.md @@ -224,12 +224,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchLeftEdge} - ``` - ??? pythontutor "視覺化執行"
@@ -485,12 +479,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_edge.zig" - [class]{}-[func]{binarySearchRightEdge} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_searching/binary_search_insertion.md b/zh-Hant/docs/chapter_searching/binary_search_insertion.md index 9d2f4699f..87267caf1 100644 --- a/zh-Hant/docs/chapter_searching/binary_search_insertion.md +++ b/zh-Hant/docs/chapter_searching/binary_search_insertion.md @@ -315,12 +315,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertionSimple} - ``` - ??? pythontutor "視覺化執行"
@@ -666,12 +660,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_insertion.zig" - [class]{}-[func]{binarySearchInsertion} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md b/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md index 4a42a4f1d..ed4b8cd53 100755 --- a/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md +++ b/zh-Hant/docs/chapter_searching/replace_linear_by_hashing.md @@ -241,26 +241,6 @@ comments: true end ``` -=== "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; - } - ``` - ??? pythontutor "視覺化執行"
@@ -556,27 +536,6 @@ comments: true end ``` -=== "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; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_searching/summary.md b/zh-Hant/docs/chapter_searching/summary.md index 977e5594c..f64defb19 100644 --- a/zh-Hant/docs/chapter_searching/summary.md +++ b/zh-Hant/docs/chapter_searching/summary.md @@ -4,6 +4,8 @@ comments: true # 10.6   小結 +### 1.   重點回顧 + - 二分搜尋依賴資料的有序性,透過迴圈逐步縮減一半搜尋區間來進行查詢。它要求輸入資料有序,且僅適用於陣列或基於陣列實現的資料結構。 - 暴力搜尋透過走訪資料結構來定位資料。線性搜尋適用於陣列和鏈結串列,廣度優先搜尋和深度優先搜尋適用於圖和樹。此類演算法通用性好,無須對資料進行預處理,但時間複雜度 $O(n)$ 較高。 - 雜湊查詢、樹查詢和二分搜尋屬於高效搜尋方法,可在特定資料結構中快速定位目標元素。此類演算法效率高,時間複雜度可達 $O(\log n)$ 甚至 $O(1)$ ,但通常需要藉助額外資料結構。 diff --git a/zh-Hant/docs/chapter_sorting/bubble_sort.md b/zh-Hant/docs/chapter_sorting/bubble_sort.md index b1c45eede..71a388a93 100755 --- a/zh-Hant/docs/chapter_sorting/bubble_sort.md +++ b/zh-Hant/docs/chapter_sorting/bubble_sort.md @@ -290,28 +290,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - // 泡沫排序 - fn bubbleSort(nums: []i32) void { - // 外迴圈:未排序區間為 [0, i] - var i: usize = nums.len - 1; - while (i > 0) : (i -= 1) { - var j: usize = 0; - // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交換 nums[j] 與 nums[j + 1] - var tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - } - } - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -617,31 +595,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bubble_sort.zig" - // 泡沫排序(標誌最佳化) - fn bubbleSortWithFlag(nums: []i32) void { - // 外迴圈:未排序區間為 [0, i] - var i: usize = nums.len - 1; - while (i > 0) : (i -= 1) { - var flag = false; // 初始化標誌位 - var j: usize = 0; - // 內迴圈:將未排序區間 [0, i] 中的最大元素交換至該區間的最右端 - while (j < i) : (j += 1) { - if (nums[j] > nums[j + 1]) { - // 交換 nums[j] 與 nums[j + 1] - var tmp = nums[j]; - nums[j] = nums[j + 1]; - nums[j + 1] = tmp; - flag = true; - } - } - if (!flag) break; // 此輪“冒泡”未交換任何元素,直接跳出 - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/bucket_sort.md b/zh-Hant/docs/chapter_sorting/bucket_sort.md index aeec7d222..798908d40 100644 --- a/zh-Hant/docs/chapter_sorting/bucket_sort.md +++ b/zh-Hant/docs/chapter_sorting/bucket_sort.md @@ -437,12 +437,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="bucket_sort.zig" - [class]{}-[func]{bucketSort} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/counting_sort.md b/zh-Hant/docs/chapter_sorting/counting_sort.md index 006216714..633acb948 100644 --- a/zh-Hant/docs/chapter_sorting/counting_sort.md +++ b/zh-Hant/docs/chapter_sorting/counting_sort.md @@ -361,12 +361,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSortNaive} - ``` - ??? pythontutor "視覺化執行"
@@ -883,12 +877,6 @@ $$ end ``` -=== "Zig" - - ```zig title="counting_sort.zig" - [class]{}-[func]{countingSort} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/heap_sort.md b/zh-Hant/docs/chapter_sorting/heap_sort.md index 991fcd662..2e5ddc141 100644 --- a/zh-Hant/docs/chapter_sorting/heap_sort.md +++ b/zh-Hant/docs/chapter_sorting/heap_sort.md @@ -612,14 +612,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="heap_sort.zig" - [class]{}-[func]{siftDown} - - [class]{}-[func]{heapSort} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/insertion_sort.md b/zh-Hant/docs/chapter_sorting/insertion_sort.md index c6873e386..b83d2cbad 100755 --- a/zh-Hant/docs/chapter_sorting/insertion_sort.md +++ b/zh-Hant/docs/chapter_sorting/insertion_sort.md @@ -270,25 +270,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="insertion_sort.zig" - // 插入排序 - fn insertionSort(nums: []i32) void { - // 外迴圈:已排序區間為 [0, i-1] - var i: usize = 1; - while (i < nums.len) : (i += 1) { - var base = nums[i]; - var j: usize = i; - // 內迴圈:將 base 插入到已排序區間 [0, i-1] 中的正確位置 - while (j >= 1 and nums[j - 1] > base) : (j -= 1) { - nums[j] = nums[j - 1]; // 將 nums[j] 向右移動一位 - } - nums[j] = base; // 將 base 賦值到正確位置 - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/merge_sort.md b/zh-Hant/docs/chapter_sorting/merge_sort.md index a74cbbbef..aafa41b1c 100755 --- a/zh-Hant/docs/chapter_sorting/merge_sort.md +++ b/zh-Hant/docs/chapter_sorting/merge_sort.md @@ -679,60 +679,6 @@ comments: true end ``` -=== "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 - left) / 2; // 計算中點 - try mergeSort(nums, left, mid); // 遞迴左子陣列 - try mergeSort(nums, mid + 1, right); // 遞迴右子陣列 - // 合併階段 - try merge(nums, left, mid, right); - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/quick_sort.md b/zh-Hant/docs/chapter_sorting/quick_sort.md index 97d566851..d13ba050c 100755 --- a/zh-Hant/docs/chapter_sorting/quick_sort.md +++ b/zh-Hant/docs/chapter_sorting/quick_sort.md @@ -366,31 +366,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 元素交換 - fn swap(nums: []i32, i: usize, j: usize) void { - var tmp = nums[i]; - nums[i] = nums[j]; - nums[j] = tmp; - } - - // 哨兵劃分 - fn partition(nums: []i32, left: usize, right: usize) usize { - // 以 nums[left] 為基準數 - var i = left; - var j = right; - while (i < j) { - while (i < j and nums[j] >= nums[left]) j -= 1; // 從右向左找首個小於基準數的元素 - while (i < j and nums[i] <= nums[left]) i += 1; // 從左向右找首個大於基準數的元素 - swap(nums, i, j); // 交換這兩個元素 - } - swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 - return i; // 返回基準數的索引 - } - ``` - ??? pythontutor "視覺化執行"
@@ -618,21 +593,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 快速排序 - fn quickSort(nums: []i32, left: usize, right: usize) void { - // 子陣列長度為 1 時終止遞迴 - if (left >= right) return; - // 哨兵劃分 - var pivot = partition(nums, left, right); - // 遞迴左子陣列、右子陣列 - quickSort(nums, left, pivot - 1); - quickSort(nums, pivot + 1, right); - } - ``` - ??? pythontutor "視覺化執行"
@@ -1122,40 +1082,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 選取三個候選元素的中位數 - fn medianThree(nums: []i32, left: usize, mid: usize, right: usize) usize { - var l = nums[left]; - var m = nums[mid]; - var r = nums[right]; - if ((l <= m && m <= r) || (r <= m && m <= l)) - return mid; // m 在 l 和 r 之間 - if ((m <= l && l <= r) || (r <= l && l <= m)) - return left; // l 在 m 和 r 之間 - return right; - } - - // 哨兵劃分(三數取中值) - fn partition(nums: []i32, left: usize, right: usize) usize { - // 選取三個候選元素的中位數 - var med = medianThree(nums, left, (left + right) / 2, right); - // 將中位數交換至陣列最左端 - swap(nums, left, med); - // 以 nums[left] 為基準數 - var i = left; - var j = right; - while (i < j) { - while (i < j and nums[j] >= nums[left]) j -= 1; // 從右向左找首個小於基準數的元素 - while (i < j and nums[i] <= nums[left]) i += 1; // 從左向右找首個大於基準數的元素 - swap(nums, i, j); // 交換這兩個元素 - } - swap(nums, i, left); // 將基準數交換至兩子陣列的分界線 - return i; // 返回基準數的索引 - } - ``` - ??? pythontutor "視覺化執行"
@@ -1445,29 +1371,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="quick_sort.zig" - // 快速排序(遞迴深度最佳化) - fn quickSort(nums: []i32, left_: usize, right_: usize) void { - var left = left_; - var right = right_; - // 子陣列長度為 1 時終止遞迴 - while (left < right) { - // 哨兵劃分操作 - var pivot = partition(nums, left, right); - // 對兩個子陣列中較短的那個執行快速排序 - if (pivot - left < right - pivot) { - quickSort(nums, left, pivot - 1); // 遞迴排序左子陣列 - left = pivot + 1; // 剩餘未排序區間為 [pivot + 1, right] - } else { - quickSort(nums, pivot + 1, right); // 遞迴排序右子陣列 - right = pivot - 1; // 剩餘未排序區間為 [left, pivot - 1] - } - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/radix_sort.md b/zh-Hant/docs/chapter_sorting/radix_sort.md index da9c7cc07..a5ac0bbe0 100644 --- a/zh-Hant/docs/chapter_sorting/radix_sort.md +++ b/zh-Hant/docs/chapter_sorting/radix_sort.md @@ -716,70 +716,6 @@ $$ end ``` -=== "Zig" - - ```zig title="radix_sort.zig" - // 獲取元素 num 的第 k 位,其中 exp = 10^(k-1) - fn digit(num: i32, exp: i32) i32 { - // 傳入 exp 而非 k 可以避免在此重複執行昂貴的次方計算 - return @mod(@divFloor(num, exp), 10); - } - - // 計數排序(根據 nums 第 k 位排序) - fn countingSortDigit(nums: []i32, exp: i32) !void { - // 十進位制的位範圍為 0~9 ,因此需要長度為 10 的桶陣列 - var mem_arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - // defer mem_arena.deinit(); - const mem_allocator = mem_arena.allocator(); - var counter = try mem_allocator.alloc(usize, 10); - @memset(counter, 0); - var n = nums.len; - // 統計 0~9 各數字的出現次數 - for (nums) |num| { - var d: u32 = @bitCast(digit(num, exp)); // 獲取 nums[i] 第 k 位,記為 d - counter[d] += 1; // 統計數字 d 的出現次數 - } - // 求前綴和,將“出現個數”轉換為“陣列索引” - var i: usize = 1; - while (i < 10) : (i += 1) { - counter[i] += counter[i - 1]; - } - // 倒序走訪,根據桶內統計結果,將各元素填入 res - var res = try mem_allocator.alloc(i32, n); - i = n - 1; - while (i >= 0) : (i -= 1) { - var d: u32 = @bitCast(digit(nums[i], exp)); - var j = counter[d] - 1; // 獲取 d 在陣列中的索引 j - res[j] = nums[i]; // 將當前元素填入索引 j - counter[d] -= 1; // 將 d 的數量減 1 - if (i == 0) break; - } - // 使用結果覆蓋原陣列 nums - i = 0; - while (i < n) : (i += 1) { - nums[i] = res[i]; - } - } - - // 基數排序 - fn radixSort(nums: []i32) !void { - // 獲取陣列的最大元素,用於判斷最大位數 - var m: i32 = std.math.minInt(i32); - for (nums) |num| { - if (num > m) m = num; - } - // 按照從低位到高位的順序走訪 - var exp: i32 = 1; - while (exp <= m) : (exp *= 10) { - // 對陣列元素的第 k 位執行計數排序 - // k = 1 -> exp = 1 - // k = 2 -> exp = 10 - // 即 exp = 10^(k-1) - try countingSortDigit(nums, exp); - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_sorting/selection_sort.md b/zh-Hant/docs/chapter_sorting/selection_sort.md index 6dcc79db9..7db8185c6 100644 --- a/zh-Hant/docs/chapter_sorting/selection_sort.md +++ b/zh-Hant/docs/chapter_sorting/selection_sort.md @@ -324,12 +324,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="selection_sort.zig" - [class]{}-[func]{selectionSort} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_stack_and_queue/deque.md b/zh-Hant/docs/chapter_stack_and_queue/deque.md index 4b0115f26..4bdef68b1 100644 --- a/zh-Hant/docs/chapter_stack_and_queue/deque.md +++ b/zh-Hant/docs/chapter_stack_and_queue/deque.md @@ -393,12 +393,6 @@ comments: true is_empty = size.zero? ``` -=== "Zig" - - ```zig title="deque.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -2160,166 +2154,6 @@ comments: true end ``` -=== "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 所示,與基於陣列實現佇列類似,我們也可以使用環形陣列來實現雙向佇列。 @@ -3768,12 +3602,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_deque.zig" - [class]{ArrayDeque}-[func]{} - ``` - ## 5.3.3   雙向佇列應用 雙向佇列兼具堆疊與佇列的邏輯,**因此它可以實現這兩者的所有應用場景,同時提供更高的自由度**。 diff --git a/zh-Hant/docs/chapter_stack_and_queue/queue.md b/zh-Hant/docs/chapter_stack_and_queue/queue.md index dc354d8eb..b42217782 100755 --- a/zh-Hant/docs/chapter_stack_and_queue/queue.md +++ b/zh-Hant/docs/chapter_stack_and_queue/queue.md @@ -366,12 +366,6 @@ comments: true is_empty = queue.empty? ``` -=== "Zig" - - ```zig title="queue.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -1317,95 +1311,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linkedlist_queue.zig" - // 基於鏈結串列實現的佇列 - fn LinkedListQueue(comptime T: type) type { - return struct { - const Self = @This(); - - front: ?*inc.ListNode(T) = null, // 頭節點 front - rear: ?*inc.ListNode(T) = null, // 尾節點 rear - que_size: usize = 0, // 佇列的長度 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 - - // 建構子(分配記憶體+初始化佇列) - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.front = null; - self.rear = null; - self.que_size = 0; - } - - // 析構函式(釋放記憶體) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 獲取佇列的長度 - pub fn size(self: *Self) usize { - return self.que_size; - } - - // 判斷佇列是否為空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 訪問佇列首元素 - pub fn peek(self: *Self) T { - if (self.size() == 0) @panic("佇列為空"); - return self.front.?.val; - } - - // 入列 - pub fn push(self: *Self, num: T) !void { - // 在尾節點後新增 num - var node = try self.mem_allocator.create(inc.ListNode(T)); - node.init(num); - // 如果佇列為空,則令頭、尾節點都指向該節點 - if (self.front == null) { - self.front = node; - self.rear = node; - // 如果佇列不為空,則將該節點新增到尾節點後 - } else { - self.rear.?.next = node; - self.rear = node; - } - self.que_size += 1; - } - - // 出列 - pub fn pop(self: *Self) T { - var num = self.peek(); - // 刪除頭節點 - self.front = self.front.?.next; - self.que_size -= 1; - return num; - } - - // 將鏈結串列轉換為陣列 - pub fn toArray(self: *Self) ![]T { - var node = self.front; - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - while (i < res.len) : (i += 1) { - res[i] = node.?.val; - node = node.?.next; - } - return res; - } - }; - } - ``` - ??? pythontutor "視覺化執行"
@@ -2381,98 +2286,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_queue.zig" - // 基於環形陣列實現的佇列 - fn ArrayQueue(comptime T: type) type { - return struct { - const Self = @This(); - - nums: []T = undefined, // 用於儲存佇列元素的陣列 - cap: usize = 0, // 佇列容量 - front: usize = 0, // 佇列首指標,指向佇列首元素 - queSize: usize = 0, // 尾指標,指向佇列尾 + 1 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 - - // 建構子(分配記憶體+初始化陣列) - pub fn init(self: *Self, allocator: std.mem.Allocator, cap: usize) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.cap = cap; - self.nums = try self.mem_allocator.alloc(T, self.cap); - @memset(self.nums, @as(T, 0)); - } - - // 析構函式(釋放記憶體) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 獲取佇列的容量 - pub fn capacity(self: *Self) usize { - return self.cap; - } - - // 獲取佇列的長度 - pub fn size(self: *Self) usize { - return self.queSize; - } - - // 判斷佇列是否為空 - pub fn isEmpty(self: *Self) bool { - return self.queSize == 0; - } - - // 入列 - pub fn push(self: *Self, num: T) !void { - if (self.size() == self.capacity()) { - std.debug.print("佇列已滿\n", .{}); - return; - } - // 計算佇列尾指標,指向佇列尾索引 + 1 - // 透過取餘操作實現 rear 越過陣列尾部後回到頭部 - var rear = (self.front + self.queSize) % self.capacity(); - // 在尾節點後新增 num - self.nums[rear] = num; - self.queSize += 1; - } - - // 出列 - pub fn pop(self: *Self) T { - var num = self.peek(); - // 佇列首指標向後移動一位,若越過尾部,則返回到陣列頭部 - self.front = (self.front + 1) % self.capacity(); - self.queSize -= 1; - return num; - } - - // 訪問佇列首元素 - pub fn peek(self: *Self) T { - if (self.isEmpty()) @panic("佇列為空"); - return self.nums[self.front]; - } - - // 返回陣列 - pub fn toArray(self: *Self) ![]T { - // 僅轉換有效長度範圍內的串列元素 - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - var j: usize = self.front; - while (i < self.size()) : ({ i += 1; j += 1; }) { - res[i] = self.nums[j % self.capacity()]; - } - return res; - } - }; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_stack_and_queue/stack.md b/zh-Hant/docs/chapter_stack_and_queue/stack.md index 50d515d89..b49ecf82d 100755 --- a/zh-Hant/docs/chapter_stack_and_queue/stack.md +++ b/zh-Hant/docs/chapter_stack_and_queue/stack.md @@ -359,12 +359,6 @@ comments: true is_empty = stack.empty? ``` -=== "Zig" - - ```zig title="stack.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -1166,84 +1160,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="linkedlist_stack.zig" - // 基於鏈結串列實現的堆疊 - fn LinkedListStack(comptime T: type) type { - return struct { - const Self = @This(); - - stack_top: ?*inc.ListNode(T) = null, // 將頭節點作為堆疊頂 - stk_size: usize = 0, // 堆疊的長度 - mem_arena: ?std.heap.ArenaAllocator = null, - mem_allocator: std.mem.Allocator = undefined, // 記憶體分配器 - - // 建構子(分配記憶體+初始化堆疊) - pub fn init(self: *Self, allocator: std.mem.Allocator) !void { - if (self.mem_arena == null) { - self.mem_arena = std.heap.ArenaAllocator.init(allocator); - self.mem_allocator = self.mem_arena.?.allocator(); - } - self.stack_top = null; - self.stk_size = 0; - } - - // 析構函式(釋放記憶體) - pub fn deinit(self: *Self) void { - if (self.mem_arena == null) return; - self.mem_arena.?.deinit(); - } - - // 獲取堆疊的長度 - pub fn size(self: *Self) usize { - return self.stk_size; - } - - // 判斷堆疊是否為空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 訪問堆疊頂元素 - pub fn peek(self: *Self) T { - if (self.size() == 0) @panic("堆疊為空"); - return self.stack_top.?.val; - } - - // 入堆疊 - pub fn push(self: *Self, num: T) !void { - var node = try self.mem_allocator.create(inc.ListNode(T)); - node.init(num); - node.next = self.stack_top; - self.stack_top = node; - self.stk_size += 1; - } - - // 出堆疊 - pub fn pop(self: *Self) T { - var num = self.peek(); - self.stack_top = self.stack_top.?.next; - self.stk_size -= 1; - return num; - } - - // 將堆疊轉換為陣列 - pub fn toArray(self: *Self) ![]T { - var node = self.stack_top; - var res = try self.mem_allocator.alloc(T, self.size()); - @memset(res, @as(T, 0)); - var i: usize = 0; - while (i < res.len) : (i += 1) { - res[res.len - i - 1] = node.?.val; - node = node.?.next; - } - return res; - } - }; - } - ``` - ??? pythontutor "視覺化執行"
@@ -1886,64 +1802,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_stack.zig" - // 基於陣列實現的堆疊 - fn ArrayStack(comptime T: type) type { - return struct { - const Self = @This(); - - stack: ?std.ArrayList(T) = null, - - // 建構子(分配記憶體+初始化堆疊) - pub fn init(self: *Self, allocator: std.mem.Allocator) void { - if (self.stack == null) { - self.stack = std.ArrayList(T).init(allocator); - } - } - - // 析構方法(釋放記憶體) - pub fn deinit(self: *Self) void { - if (self.stack == null) return; - self.stack.?.deinit(); - } - - // 獲取堆疊的長度 - pub fn size(self: *Self) usize { - return self.stack.?.items.len; - } - - // 判斷堆疊是否為空 - pub fn isEmpty(self: *Self) bool { - return self.size() == 0; - } - - // 訪問堆疊頂元素 - pub fn peek(self: *Self) T { - if (self.isEmpty()) @panic("堆疊為空"); - return self.stack.?.items[self.size() - 1]; - } - - // 入堆疊 - pub fn push(self: *Self, num: T) !void { - try self.stack.?.append(num); - } - - // 出堆疊 - pub fn pop(self: *Self) T { - var num = self.stack.?.pop(); - return num; - } - - // 返回 ArrayList - pub fn toList(self: *Self) std.ArrayList(T) { - return self.stack.?; - } - }; - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_tree/array_representation_of_tree.md b/zh-Hant/docs/chapter_tree/array_representation_of_tree.md index f62d9d204..6d8ba244e 100644 --- a/zh-Hant/docs/chapter_tree/array_representation_of_tree.md +++ b/zh-Hant/docs/chapter_tree/array_representation_of_tree.md @@ -136,12 +136,6 @@ comments: true tree = [1, 2, 3, 4, nil, 6, 7, 8, 9, nil, nil, 12, nil, nil, 15] ``` -=== "Zig" - - ```zig title="" - - ``` - ![任意型別二元樹的陣列表示](array_representation_of_tree.assets/array_representation_with_empty.png){ class="animation-figure" }

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

@@ -1334,12 +1328,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="array_binary_tree.zig" - [class]{ArrayBinaryTree}-[func]{} - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_tree/avl_tree.md b/zh-Hant/docs/chapter_tree/avl_tree.md index d02c633c6..7de668264 100644 --- a/zh-Hant/docs/chapter_tree/avl_tree.md +++ b/zh-Hant/docs/chapter_tree/avl_tree.md @@ -236,12 +236,6 @@ AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二 end ``` -=== "Zig" - - ```zig title="" - - ``` - “節點高度”是指從該節點到它的最遠葉節點的距離,即所經過的“邊”的數量。需要特別注意的是,葉節點的高度為 $0$ ,而空節點的高度為 $-1$ 。我們將建立兩個工具函式,分別用於獲取和更新節點的高度: === "Python" @@ -481,23 +475,6 @@ AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二 end ``` -=== "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$ 。我們同樣將獲取節點平衡因子的功能封裝成函式,方便後續使用: @@ -669,18 +646,6 @@ AVL 樹既是二元搜尋樹,也是平衡二元樹,同時滿足這兩類二 end ``` -=== "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); - } - ``` - !!! tip 設平衡因子為 $f$ ,則一棵 AVL 樹的任意節點的平衡因子皆滿足 $-1 \le f \le 1$ 。 @@ -956,24 +921,6 @@ AVL 樹的特點在於“旋轉”操作,它能夠在不影響二元樹的中 end ``` -=== "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 所示的“左旋”操作。 @@ -1229,24 +1176,6 @@ AVL 樹的特點在於“旋轉”操作,它能夠在不影響二元樹的中 end ``` -=== "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` 執行“右旋”。 @@ -1730,40 +1659,6 @@ AVL 樹的特點在於“旋轉”操作,它能夠在不影響二元樹的中 end ``` -=== "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.   插入節點 @@ -2142,38 +2037,6 @@ AVL 樹的節點插入操作與二元搜尋樹在主體上類似。唯一的區 end ``` -=== "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.   刪除節點 類似地,在二元搜尋樹的刪除節點方法的基礎上,需要從底至頂執行旋轉操作,使所有失衡節點恢復平衡。程式碼如下所示: @@ -2775,51 +2638,6 @@ AVL 樹的節點插入操作與二元搜尋樹在主體上類似。唯一的區 end ``` -=== "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 樹的節點查詢操作與二元搜尋樹一致,在此不再贅述。 diff --git a/zh-Hant/docs/chapter_tree/binary_search_tree.md b/zh-Hant/docs/chapter_tree/binary_search_tree.md index 985cdb82b..77b2a7a76 100755 --- a/zh-Hant/docs/chapter_tree/binary_search_tree.md +++ b/zh-Hant/docs/chapter_tree/binary_search_tree.md @@ -338,30 +338,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 查詢節點 - fn search(self: *Self, num: T) ?*inc.TreeNode(T) { - var cur = self.root; - // 迴圈查詢,越過葉節點後跳出 - while (cur != null) { - // 目標節點在 cur 的右子樹中 - if (cur.?.val < num) { - cur = cur.?.right; - // 目標節點在 cur 的左子樹中 - } else if (cur.?.val > num) { - cur = cur.?.left; - // 找到目標節點,跳出迴圈 - } else { - break; - } - } - // 返回目標節點 - return cur; - } - ``` - ??? pythontutor "視覺化執行"
@@ -826,42 +802,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 插入節點 - fn insert(self: *Self, num: T) !void { - // 若樹為空,則初始化根節點 - if (self.root == null) { - self.root = try self.mem_allocator.create(inc.TreeNode(T)); - return; - } - var cur = self.root; - var pre: ?*inc.TreeNode(T) = null; - // 迴圈查詢,越過葉節點後跳出 - while (cur != null) { - // 找到重複節點,直接返回 - if (cur.?.val == num) return; - pre = cur; - // 插入位置在 cur 的右子樹中 - if (cur.?.val < num) { - cur = cur.?.right; - // 插入位置在 cur 的左子樹中 - } else { - cur = cur.?.left; - } - } - // 插入節點 - var node = try self.mem_allocator.create(inc.TreeNode(T)); - node.init(num); - if (pre.?.val < num) { - pre.?.right = node; - } else { - pre.?.left = node; - } - } - ``` - ??? pythontutor "視覺化執行"
@@ -1648,56 +1588,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_search_tree.zig" - // 刪除節點 - fn remove(self: *Self, num: T) void { - // 若樹為空,直接提前返回 - if (self.root == null) return; - var cur = self.root; - var pre: ?*inc.TreeNode(T) = null; - // 迴圈查詢,越過葉節點後跳出 - while (cur != null) { - // 找到待刪除節點,跳出迴圈 - if (cur.?.val == num) break; - pre = cur; - // 待刪除節點在 cur 的右子樹中 - if (cur.?.val < num) { - cur = cur.?.right; - // 待刪除節點在 cur 的左子樹中 - } else { - cur = cur.?.left; - } - } - // 若無待刪除節點,則直接返回 - if (cur == null) return; - // 子節點數量 = 0 or 1 - if (cur.?.left == null or cur.?.right == null) { - // 當子節點數量 = 0 / 1 時, child = null / 該子節點 - var child = if (cur.?.left != null) cur.?.left else cur.?.right; - // 刪除節點 cur - if (pre.?.left == cur) { - pre.?.left = child; - } else { - pre.?.right = child; - } - // 子節點數量 = 2 - } else { - // 獲取中序走訪中 cur 的下一個節點 - var tmp = cur.?.right; - while (tmp.?.left != null) { - tmp = tmp.?.left; - } - var tmp_val = tmp.?.val; - // 遞迴刪除節點 tmp - self.remove(tmp.?.val); - // 用 tmp 覆蓋 cur - cur.?.val = tmp_val; - } - } - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_tree/binary_tree.md b/zh-Hant/docs/chapter_tree/binary_tree.md index 1c384af41..617254b13 100644 --- a/zh-Hant/docs/chapter_tree/binary_tree.md +++ b/zh-Hant/docs/chapter_tree/binary_tree.md @@ -205,12 +205,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="" - - ``` - 每個節點都有兩個引用(指標),分別指向左子節點(left-child node)右子節點(right-child node),該節點被稱為這兩個子節點的父節點(parent node)。當給定一個二元樹的節點時,我們將該節點的左子節點及其以下節點形成的樹稱為該節點的左子樹(left subtree),同理可得右子樹(right subtree)。 **在二元樹中,除葉節點外,其他所有節點都包含子節點和非空子樹**。如圖 7-1 所示,如果將“節點 2”視為父節點,則其左子節點和右子節點分別是“節點 4”和“節點 5”,左子樹是“節點 4 及其以下節點形成的樹”,右子樹是“節點 5 及其以下節點形成的樹”。 @@ -463,12 +457,6 @@ comments: true n2.right = n5 ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - ??? pythontutor "視覺化執行"
@@ -638,12 +626,6 @@ comments: true n1.left = n2 ``` -=== "Zig" - - ```zig title="binary_tree.zig" - - ``` - ??? pythontutor "視覺化執行"
diff --git a/zh-Hant/docs/chapter_tree/binary_tree_traversal.md b/zh-Hant/docs/chapter_tree/binary_tree_traversal.md index c39b8ac33..ad8d1a370 100755 --- a/zh-Hant/docs/chapter_tree/binary_tree_traversal.md +++ b/zh-Hant/docs/chapter_tree/binary_tree_traversal.md @@ -334,38 +334,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_tree_bfs.zig" - // 層序走訪 - fn levelOrder(comptime T: type, mem_allocator: std.mem.Allocator, root: *inc.TreeNode(T)) !std.ArrayList(T) { - // 初始化佇列,加入根節點 - const L = std.TailQueue(*inc.TreeNode(T)); - var queue = L{}; - var root_node = try mem_allocator.create(L.Node); - root_node.data = root; - queue.append(root_node); - // 初始化一個串列,用於儲存走訪序列 - var list = std.ArrayList(T).init(std.heap.page_allocator); - while (queue.len > 0) { - var queue_node = queue.popFirst().?; // 隊列出隊 - var node = queue_node.data; - try list.append(node.val); // 儲存節點值 - if (node.left != null) { - var tmp_node = try mem_allocator.create(L.Node); - tmp_node.data = node.left.?; - queue.append(tmp_node); // 左子節點入列 - } - if (node.right != null) { - var tmp_node = try mem_allocator.create(L.Node); - tmp_node.data = node.right.?; - queue.append(tmp_node); // 右子節點入列 - } - } - return list; - } - ``` - ??? pythontutor "視覺化執行"
@@ -851,37 +819,6 @@ comments: true end ``` -=== "Zig" - - ```zig title="binary_tree_dfs.zig" - // 前序走訪 - fn preOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 訪問優先順序:根節點 -> 左子樹 -> 右子樹 - try list.append(root.?.val); - try preOrder(T, root.?.left); - try preOrder(T, root.?.right); - } - - // 中序走訪 - fn inOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 訪問優先順序:左子樹 -> 根節點 -> 右子樹 - try inOrder(T, root.?.left); - try list.append(root.?.val); - try inOrder(T, root.?.right); - } - - // 後序走訪 - fn postOrder(comptime T: type, root: ?*inc.TreeNode(T)) !void { - if (root == null) return; - // 訪問優先順序:左子樹 -> 右子樹 -> 根節點 - try postOrder(T, root.?.left); - try postOrder(T, root.?.right); - try list.append(root.?.val); - } - ``` - ??? pythontutor "視覺化執行"