diff --git a/codes/java/chapter_stack_and_queue/stack.java b/codes/java/chapter_stack_and_queue/stack.java index 2a725922..52f44a0d 100644 --- a/codes/java/chapter_stack_and_queue/stack.java +++ b/codes/java/chapter_stack_and_queue/stack.java @@ -11,23 +11,23 @@ import java.util.*; public class stack { public static void main(String[] args) { /* 初始化栈 */ - // 在 Java 中,推荐将 LinkedList 当作栈来使用 - LinkedList stack = new LinkedList<>(); + // 在 Java 中,推荐将 ArrayList 当作栈来使用 + List stack = new ArrayList<>(); /* 元素入栈 */ - stack.addLast(1); - stack.addLast(3); - stack.addLast(2); - stack.addLast(5); - stack.addLast(4); + stack.add(1); + stack.add(3); + stack.add(2); + stack.add(5); + stack.add(4); System.out.println("栈 stack = " + stack); /* 访问栈顶元素 */ - int peek = stack.peekLast(); + int peek = stack.get(stack.size() - 1); System.out.println("栈顶元素 peek = " + peek); /* 元素出栈 */ - int pop = stack.removeLast(); + int pop = stack.remove(stack.size() - 1); System.out.println("出栈元素 pop = " + pop + ",出栈后 stack = " + stack); /* 获取栈的长度 */ diff --git a/docs/chapter_computational_complexity/space_time_tradeoff.md b/docs/chapter_computational_complexity/space_time_tradeoff.md index 70017e34..a265aba9 100644 --- a/docs/chapter_computational_complexity/space_time_tradeoff.md +++ b/docs/chapter_computational_complexity/space_time_tradeoff.md @@ -16,7 +16,7 @@ comments: true !!! question "两数之和" - 给定一个整数数组 `nums` 和一个整数目标值 `target` ,请你在该数组中找出 和为目标值 `target` 的那两个整数,并返回它们的数组下标。 + 给定一个整数数组 `nums` 和一个整数目标值 `target` ,请你在该数组中找出“和”为目标值 `target` 的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 diff --git a/docs/chapter_stack_and_queue/queue.assets/array_queue.png b/docs/chapter_stack_and_queue/queue.assets/array_queue.png new file mode 100644 index 00000000..afbecd91 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/array_queue.png differ diff --git a/docs/chapter_stack_and_queue/queue.assets/array_queue_poll.png b/docs/chapter_stack_and_queue/queue.assets/array_queue_poll.png new file mode 100644 index 00000000..9a3c1087 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/array_queue_poll.png differ diff --git a/docs/chapter_stack_and_queue/queue.assets/array_queue_push.png b/docs/chapter_stack_and_queue/queue.assets/array_queue_push.png new file mode 100644 index 00000000..cd164441 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/array_queue_push.png differ diff --git a/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue.png b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue.png new file mode 100644 index 00000000..cebfea13 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue.png differ diff --git a/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_poll.png b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_poll.png new file mode 100644 index 00000000..83a102a5 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_poll.png differ diff --git a/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_push.png b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_push.png new file mode 100644 index 00000000..3e107559 Binary files /dev/null and b/docs/chapter_stack_and_queue/queue.assets/linkedlist_queue_push.png differ diff --git a/docs/chapter_stack_and_queue/queue.md b/docs/chapter_stack_and_queue/queue.md index 35d6c41f..c7bf098c 100644 --- a/docs/chapter_stack_and_queue/queue.md +++ b/docs/chapter_stack_and_queue/queue.md @@ -234,24 +234,24 @@ comments: true /* 初始化队列 */ // Swift 没有内置的队列类,可以把 Array 当作队列来使用 var queue: [Int] = [] - + /* 元素入队 */ queue.append(1) queue.append(3) queue.append(2) queue.append(5) queue.append(4) - + /* 访问队首元素 */ let peek = queue.first! - + /* 元素出队 */ // 使用 Array 模拟时 poll 的复杂度为 O(n) let pool = queue.removeFirst() - + /* 获取队列的长度 */ let size = queue.count - + /* 判断队列是否为空 */ let isEmpty = queue.isEmpty ``` @@ -264,6 +264,17 @@ comments: true 我们将链表的「头结点」和「尾结点」分别看作是队首和队尾,并规定队尾只可添加结点,队首只可删除结点。 +=== "LinkedListQueue" + ![linkedlist_queue](queue.assets/linkedlist_queue.png) + +=== "push()" + ![linkedlist_queue_push](queue.assets/linkedlist_queue_push.png) + +=== "poll()" + ![linkedlist_queue_poll](queue.assets/linkedlist_queue_poll.png) + +以下是使用链表实现队列的示例代码。 + === "Java" ```java title="linkedlist_queue.java" @@ -434,19 +445,19 @@ comments: true // 使用内置包 list 来实现队列 data *list.List } - + // newLinkedListQueue 初始化链表 func newLinkedListQueue() *linkedListQueue { return &linkedListQueue{ data: list.New(), } } - + // offer 入队 func (s *linkedListQueue) offer(value any) { s.data.PushBack(value) } - + // poll 出队 func (s *linkedListQueue) poll() any { if s.isEmpty() { @@ -456,7 +467,7 @@ comments: true s.data.Remove(e) return e.Value } - + // peek 访问队首元素 func (s *linkedListQueue) peek() any { if s.isEmpty() { @@ -465,12 +476,12 @@ comments: true e := s.data.Front() return e.Value } - + // size 获取队列的长度 func (s *linkedListQueue) size() int { return s.data.Len() } - + // isEmpty 判断队列是否为空 func (s *linkedListQueue) isEmpty() bool { return s.data.Len() == 0 @@ -658,19 +669,19 @@ comments: true private var front: ListNode? // 头结点 private var rear: ListNode? // 尾结点 private var _size = 0 - + init() {} - + /* 获取队列的长度 */ func size() -> Int { _size } - + /* 判断队列是否为空 */ func isEmpty() -> Bool { size() == 0 } - + /* 入队 */ func offer(num: Int) { // 尾结点后添加 num @@ -687,7 +698,7 @@ comments: true } _size += 1 } - + /* 出队 */ @discardableResult func poll() -> Int { @@ -697,7 +708,7 @@ comments: true _size -= 1 return num } - + /* 访问队首元素 */ func peek() -> Int { if isEmpty() { @@ -712,11 +723,18 @@ comments: true 数组的删除首元素的时间复杂度为 $O(n)$ ,因此不适合直接用来实现队列。然而,我们可以借助两个指针 `front` , `rear` 来分别记录队首和队尾的索引位置,在入队 / 出队时分别将 `front` / `rear` 向后移动一位即可,这样每次仅需操作一个元素,时间复杂度降至 $O(1)$ 。 -还有一个问题,在入队与出队的过程中,两个指针都在向后移动,而到达尾部后则无法继续移动了。为了解决此问题,我们可以采取一个取巧方案,即将数组看作是“环形”的。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。 +=== "ArrayQueue" + ![array_queue](queue.assets/array_queue.png) -为了适应环形数组的设定,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 +=== "push()" + ![array_queue_push](queue.assets/array_queue_push.png) -基于数组实现的队列有一个缺点,即长度不可变。但这点我们可以通过动态数组来解决,有兴趣的同学可以自行实现。 +=== "poll()" + ![array_queue_poll](queue.assets/array_queue_poll.png) + +细心的同学可能会发现一个问题,即在入队与出队的过程中,两个指针都在向后移动,**在到达尾部后则无法继续移动了**。 + +为了解决此问题,我们可以采取一个取巧方案,**即将数组看作是“环形”的**。具体做法是规定指针越过数组尾部后,再次回到头部接续遍历,这样相当于使数组“首尾相连”了。在环形数组的设定下,获取长度 `size()` 、入队 `offer()` 、出队 `poll()` 方法都需要做相应的取余操作处理,使得当尾指针绕回数组头部时,仍然可以正确处理操作。 === "Java" @@ -898,7 +916,7 @@ comments: true front int // 头指针,指向队首 rear int // 尾指针,指向队尾 + 1 } - + // newArrayQueue 基于环形数组实现的队列 func newArrayQueue(capacity int) *arrayQueue { return &arrayQueue{ @@ -908,18 +926,18 @@ comments: true rear: 0, } } - + // size 获取队列的长度 func (q *arrayQueue) size() int { size := (q.capacity + q.rear - q.front) % q.capacity return size } - + // isEmpty 判断队列是否为空 func (q *arrayQueue) isEmpty() bool { return q.rear-q.front == 0 } - + // offer 入队 func (q *arrayQueue) offer(v int) { // 当 rear == capacity 表示队列已满 @@ -931,7 +949,7 @@ comments: true // 尾指针向后移动一位,越过尾部后返回到数组头部 q.rear = (q.rear + 1) % q.capacity } - + // poll 出队 func (q *arrayQueue) poll() any { if q.isEmpty() { @@ -942,7 +960,7 @@ comments: true q.front = (q.front + 1) % q.capacity return v } - + // peek 访问队首元素 func (q *arrayQueue) peek() any { if q.isEmpty() { @@ -1128,29 +1146,29 @@ comments: true private var nums: [Int] // 用于存储队列元素的数组 private var front = 0 // 头指针,指向队首 private var rear = 0 // 尾指针,指向队尾 + 1 - + init(capacity: Int) { // 初始化数组 nums = Array(repeating: 0, count: capacity) } - + /* 获取队列的容量 */ func capacity() -> Int { nums.count } - + /* 获取队列的长度 */ func size() -> Int { let capacity = capacity() // 由于将数组看作为环形,可能 rear < front ,因此需要取余数 return (capacity + rear - front) % capacity } - + /* 判断队列是否为空 */ func isEmpty() -> Bool { rear - front == 0 } - + /* 入队 */ func offer(num: Int) { if size() == capacity() { @@ -1162,7 +1180,7 @@ comments: true // 尾指针向后移动一位,越过尾部后返回到数组头部 rear = (rear + 1) % capacity() } - + /* 出队 */ @discardableResult func poll() -> Int { @@ -1171,7 +1189,7 @@ comments: true front = (front + 1) % capacity() return num } - + /* 访问队首元素 */ func peek() -> Int { if isEmpty() { @@ -1182,6 +1200,12 @@ comments: true } ``` +以上代码仍存在局限性,即长度不可变。然而,我们可以通过将数组替换为列表(即动态数组)来引入扩容机制,有兴趣的同学可以尝试实现。 + +## 两种实现对比 + +与栈的结论一致,在此不再赘述。 + ## 队列典型应用 - **淘宝订单**。购物者下单后,订单就被加入到队列之中,随后系统再根据顺序依次处理队列中的订单。在双十一时,在短时间内会产生海量的订单,如何处理「高并发」则是工程师们需要重点思考的问题。 diff --git a/docs/chapter_stack_and_queue/stack.assets/array_stack.png b/docs/chapter_stack_and_queue/stack.assets/array_stack.png new file mode 100644 index 00000000..4629ef66 Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/array_stack.png differ diff --git a/docs/chapter_stack_and_queue/stack.assets/array_stack_pop.png b/docs/chapter_stack_and_queue/stack.assets/array_stack_pop.png new file mode 100644 index 00000000..afc87d38 Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/array_stack_pop.png differ diff --git a/docs/chapter_stack_and_queue/stack.assets/array_stack_push.png b/docs/chapter_stack_and_queue/stack.assets/array_stack_push.png new file mode 100644 index 00000000..098dbec3 Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/array_stack_push.png differ diff --git a/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack.png b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack.png new file mode 100644 index 00000000..41d1bb5c Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack.png differ diff --git a/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_pop.png b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_pop.png new file mode 100644 index 00000000..2afd3ca8 Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_pop.png differ diff --git a/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_push.png b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_push.png new file mode 100644 index 00000000..6b08e983 Binary files /dev/null and b/docs/chapter_stack_and_queue/stack.assets/linkedlist_stack_push.png differ diff --git a/docs/chapter_stack_and_queue/stack.md b/docs/chapter_stack_and_queue/stack.md index 66b46187..dd45821c 100644 --- a/docs/chapter_stack_and_queue/stack.md +++ b/docs/chapter_stack_and_queue/stack.md @@ -32,27 +32,27 @@ comments: true -我们可以直接使用编程语言实现好的栈类。 +我们可以直接使用编程语言实现好的栈类。 某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作。 === "Java" ```java title="stack.java" /* 初始化栈 */ - // 在 Java 中,推荐将 LinkedList 当作栈来使用 - LinkedList stack = new LinkedList<>(); + // 在 Java 中,推荐将 ArrayList 当作栈来使用 + List stack = new ArrayList<>(); /* 元素入栈 */ - stack.addLast(1); - stack.addLast(3); - stack.addLast(2); - stack.addLast(5); - stack.addLast(4); + stack.add(1); + stack.add(3); + stack.add(2); + stack.add(5); + stack.add(4); /* 访问栈顶元素 */ - int peek = stack.peekLast(); + int peek = stack.get(stack.size() - 1); /* 元素出栈 */ - int pop = stack.removeLast(); + int pop = stack.remove(stack.size() - 1); /* 获取栈的长度 */ int size = stack.size(); @@ -234,23 +234,23 @@ comments: true /* 初始化栈 */ // Swift 没有内置的栈类,可以把 Array 当作栈来使用 var stack: [Int] = [] - + /* 元素入栈 */ stack.append(1) stack.append(3) stack.append(2) stack.append(5) stack.append(4) - + /* 访问栈顶元素 */ let peek = stack.last! - + /* 元素出栈 */ let pop = stack.removeLast() - + /* 获取栈的长度 */ let size = stack.count - + /* 判断是否为空 */ let isEmpty = stack.isEmpty ``` @@ -263,11 +263,20 @@ comments: true ### 基于链表的实现 -使用「链表」实现栈时,将链表的头结点看作栈顶,尾结点看作栈底。 +使用「链表」实现栈时,将链表的头结点看作栈顶,将尾结点看作栈底。 对于入栈操作,将元素插入到链表头部即可,这种结点添加方式被称为“头插法”。而对于出栈操作,则将头结点从链表中删除即可。 -受益于链表的离散存储方式,栈的扩容更加灵活,删除元素的内存也会被系统自动回收;缺点是无法像数组一样高效地随机访问,并且由于链表结点需存储指针,导致单个元素占用空间更大。 +=== "LinkedListStack" + ![linkedlist_stack](stack.assets/linkedlist_stack.png) + +=== "push()" + ![linkedlist_stack_push](stack.assets/linkedlist_stack_push.png) + +=== "pop()" + ![linkedlist_stack_pop](stack.assets/linkedlist_stack_pop.png) + +以下是基于链表实现栈的示例代码。 === "Java" @@ -406,19 +415,19 @@ comments: true // 使用内置包 list 来实现栈 data *list.List } - + // newLinkedListStack 初始化链表 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() { @@ -428,7 +437,7 @@ comments: true s.data.Remove(e) return e.Value } - + // peek 访问栈顶元素 func (s *linkedListStack) peek() any { if s.isEmpty() { @@ -437,12 +446,12 @@ comments: true e := s.data.Back() return e.Value } - + // size 获取栈的长度 func (s *linkedListStack) size() int { return s.data.Len() } - + // isEmpty 判断栈是否为空 func (s *linkedListStack) isEmpty() bool { return s.data.Len() == 0 @@ -634,19 +643,19 @@ comments: true class LinkedListStack { private var _peek: ListNode? // 将头结点作为栈顶 private var _size = 0 // 栈的长度 - + init() {} - + /* 获取栈的长度 */ func size() -> Int { _size } - + /* 判断栈是否为空 */ func isEmpty() -> Bool { size() == 0 } - + /* 入栈 */ func push(num: Int) { let node = ListNode(x: num) @@ -654,7 +663,7 @@ comments: true _peek = node _size += 1 } - + /* 出栈 */ @discardableResult func pop() -> Int { @@ -663,7 +672,7 @@ comments: true _size -= 1 return num } - + /* 访问栈顶元素 */ func peek() -> Int { if isEmpty() { @@ -676,9 +685,18 @@ comments: true ### 基于数组的实现 -使用「数组」实现栈时,将数组的尾部当作栈顶,这样可以保证入栈与出栈操作的时间复杂度都为 $O(1)$ 。准确地说,由于入栈的元素可能是源源不断的,我们需要使用可以动态扩容的「列表」。 +使用「数组」实现栈时,考虑将数组的尾部当作栈顶。这样设计下,「入栈」与「出栈」操作就对应在数组尾部「添加元素」与「删除元素」,时间复杂度都为 $O(1)$ 。 -基于数组实现的栈,优点是支持随机访问,缺点是会造成一定的空间浪费,因为列表的容量始终 $\geq$ 元素数量。 +=== "ArrayStack" + ![array_stack](stack.assets/array_stack.png) + +=== "push()" + ![array_stack_push](stack.assets/array_stack_push.png) + +=== "pop()" + ![array_stack_pop](stack.assets/array_stack_pop.png) + +由于入栈的元素可能是源源不断的,因此可以使用支持动态扩容的「列表」,这样就无需自行实现数组扩容了。以下是示例代码。 === "Java" @@ -790,30 +808,30 @@ comments: true type arrayStack struct { data []int // 数据 } - + func newArrayStack() *arrayStack { return &arrayStack{ // 设置栈的长度为 0,容量为 16 data: make([]int, 0, 16), } } - + // size 栈的长度 func (s *arrayStack) size() int { return len(s.data) } - + // isEmpty 栈是否为空 func (s *arrayStack) isEmpty() bool { return s.size() == 0 } - + // push 入栈 func (s *arrayStack) push(v int) { // 切片会自动扩容 s.data = append(s.data, v) } - + // pop 出栈 func (s *arrayStack) pop() any { // 弹出栈前,先判断是否为空 @@ -824,7 +842,7 @@ comments: true s.data = s.data[:len(s.data)-1] return val } - + // peek 获取栈顶元素 func (s *arrayStack) peek() any { if s.isEmpty() { @@ -965,27 +983,27 @@ comments: true /* 基于数组实现的栈 */ class ArrayStack { private var stack: [Int] - + init() { // 初始化列表(动态数组) stack = [] } - + /* 获取栈的长度 */ func size() -> Int { stack.count } - + /* 判断栈是否为空 */ func isEmpty() -> Bool { stack.isEmpty } - + /* 入栈 */ func push(num: Int) { stack.append(num) } - + /* 出栈 */ @discardableResult func pop() -> Int { @@ -994,7 +1012,7 @@ comments: true } return stack.removeLast() } - + /* 访问栈顶元素 */ func peek() -> Int { if isEmpty() { @@ -1005,9 +1023,30 @@ comments: true } ``` -!!! tip +## 两种实现对比 - 某些语言并未专门提供栈类,但我们可以直接把该语言的「数组」或「链表」看作栈来使用,并通过“脑补”来屏蔽无关操作,而无需像上述代码去特意包装一层。 +### 支持操作 + +两种实现都支持栈定义中的各项操作,数组实现额外支持随机访问,但这已经超出栈的定义范畴,一般不会用到。 + +### 时间效率 + +在数组(列表)实现中,入栈与出栈操作都是在预先分配好的连续内存中操作,具有很好的缓存本地性,效率很好。然而,如果入栈时超出数组容量,则会触发扩容机制,那么该次入栈操作的时间复杂度为 $O(n)$ 。 + +在链表实现中,链表的扩容非常灵活,不存在上述数组扩容时变慢的问题。然而,入栈操作需要初始化结点对象并修改指针,因而效率不如数组。进一步地思考,如果入栈元素不是 `int` 而是结点对象,那么就可以省去初始化步骤,从而提升效率。 + +综上所述,当入栈与出栈操作的元素是基本数据类型(例如 `int` , `double` )时,则结论如下: + +- 数组实现的栈在触发扩容时会变慢,但由于扩容是低频操作,因此 **总体效率更高**; +- 链表实现的栈可以提供 **更加稳定的效率表现**; + +### 空间效率 + +在初始化列表时,系统会给列表分配“初始容量”,该容量可能超过我们的需求。并且扩容机制一般是按照特定倍率(比如 2 倍)进行扩容,扩容后的容量也可能超出我们的需求。因此,**数组实现栈会造成一定的空间浪费**。 + +当然,由于结点需要额外存储指针,因此 **链表结点比数组元素占用更大**。 + +综上,我们不能简单地确定哪种实现更加省内存,需要 case-by-case 地分析。 ## 栈典型应用 diff --git a/docs/chapter_stack_and_queue/summary.md b/docs/chapter_stack_and_queue/summary.md index 964f1f73..d6066f0f 100644 --- a/docs/chapter_stack_and_queue/summary.md +++ b/docs/chapter_stack_and_queue/summary.md @@ -5,5 +5,7 @@ comments: true # 小结 - 栈是一种遵循先入后出的数据结构,可以使用数组或链表实现。 -- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。 +- 在时间效率方面,栈的数组实现具有更好的平均效率,但扩容时会导致单次入栈操作的时间复杂度劣化至 $O(n)$ 。相对地,栈的链表实现具有更加稳定的效率表现。 +- 在空间效率方面,栈的数组实现会造成一定空间浪费,然而链表结点比数组元素占用内存更大。 +- 队列是一种遵循先入先出的数据结构,可以使用数组或链表实现。对于两种实现的时间效率与空间效率对比,与上述栈的结论相同。 - 双向队列的两端都可以添加与删除元素。