translation: Add the initial translation of the chapter of stack and queue (#1033)

* Update the format of Q&As in docs-en

* Fix the code comments of JavaScript and TypeScript

* Add the initial translation of the chapter of stack and queue
This commit is contained in:
Yudong Jin 2024-01-08 19:06:37 +08:00 committed by GitHub
parent c4e4a607e8
commit d3cb600910
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 1263 additions and 76 deletions

View file

@ -6,7 +6,7 @@
/* Driver Code */ /* Driver Code */
/* 初始化栈 */ /* 初始化栈 */
// Javascript 没有内置的栈类,可以把 Array 当作栈来使用 // JavaScript 没有内置的栈类,可以把 Array 当作栈来使用
const stack = []; const stack = [];
/* 元素入栈 */ /* 元素入栈 */

View file

@ -6,7 +6,7 @@
/* Driver Code */ /* Driver Code */
/* 初始化栈 */ /* 初始化栈 */
// Typescript 没有内置的栈类,可以把 Array 当作栈来使用 // TypeScript 没有内置的栈类,可以把 Array 当作栈来使用
const stack: number[] = []; const stack: number[] = [];
/* 元素入栈 */ /* 元素入栈 */

View file

@ -14,68 +14,68 @@
### Q & A ### Q & A
!!! question "Does storing arrays on the stack versus the heap affect time and space efficiency?" **Q**: Does storing arrays on the stack versus the heap affect time and space efficiency?
Arrays stored on both the stack and heap are stored in continuous 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 both the stack and heap are stored in continuous memory spaces, and data operation efficiency is essentially the same. However, stacks and heaps 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. 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. 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. 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.
!!! question "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 same-type elements?
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 consist of nodes connected by references (pointers), and each node can store data of different types, 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, allowing the calculation of offsets to access the corresponding element positions. For example, an array containing both int and long types, with single elements occupying 4 bytes and 8 bytes respectively, cannot use the following formula to calculate offsets, as the array contains elements of two different lengths.
```shell ```shell
# Element memory address = Array memory address + Element length * Element index # Element memory address = Array memory address + Element length * Element index
``` ```
!!! question "After deleting a node, is it necessary to set `P.next` to `None`?" **Q**: After deleting a node, is it necessary 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. 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.
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 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.
!!! question "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 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)`?
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 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)`.
!!! question "In the image '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 image "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?
The diagram is just a qualitative representation; quantitative analysis depends on specific situations. The diagram is just a qualitative representation; quantitative analysis depends on specific situations.
- Different types of node values occupy different amounts of space, such as int, long, double, and object instances. - 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. - The memory space occupied by pointer variables depends on the operating system and compilation environment used, usually 8 bytes or 4 bytes.
!!! question "Is adding elements to the end of a list always `O(1)`?" **Q**: Is adding elements to 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 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)`.
!!! question "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 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?
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. 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.
!!! question "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]` 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?
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]`, 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.
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, 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.
!!! question "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**: 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?
On the one hand, we often prefer to use arrays to implement algorithms, only using linked lists when necessary, mainly for two reasons. On the one hand, we often prefer to use arrays to implement algorithms, only using 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`. - 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. - Cache unfriendly: As the data is not stored continuously, `std::list` has a lower cache utilization rate. Generally, `std::vector` performs better.
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, 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.
!!! question "Does initializing a list `res = [0] * self.size()` result in each element of `res` referencing the same address?" **Q**: Does initializing a list `res = [0] * self.size()` result in each element of `res` referencing the same address?
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. 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.
!!! question "In deleting a node, is it necessary to break the reference to its successor node?" **Q**: In deleting a node, is it necessary to break the reference to its successor node?
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. 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.

View file

@ -26,24 +26,24 @@
### Q & A ### Q & A
!!! question "Is the space complexity of tail recursion $O(1)$?" **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 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)$.
!!! question "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 and is implicitly passed to the object calling it, able to operate on the data contained within an instance of a class.
Here are some examples from common programming languages: Here are some examples from common 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 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 these 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. - 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). - C++ and Python support both procedural programming (functions) and object-oriented programming (methods).
!!! question "Does the 'Common Types of Space Complexity' figure reflect the absolute size of occupied space?" **Q**: Does the "Common Types of Space Complexity" figure 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 figure shows space complexities, which reflect growth trends, not the absolute size of the 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. 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.
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, 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.

View file

@ -15,19 +15,19 @@
### Q & A ### Q & A
!!! question "Why does a hash table contain both linear and non-linear data structures?" **Q**: Why does a hash table 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": each bucket in the array points to a linked list, which, when exceeding a certain threshold, might be transformed into a tree (usually a red-black tree). The underlying structure of a hash table is an array. To resolve hash collisions, we may use "chaining": each bucket in the array points to a linked list, which, when exceeding a certain threshold, might be transformed into a tree (usually a red-black tree).
From a storage perspective, the foundation of a hash table is an array, where each bucket slot 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). From a storage perspective, the foundation of a hash table is an array, where each bucket slot 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).
!!! question "Is the length of the `char` type 1 byte?" **Q**: Is the length of the `char` type 1 byte?
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 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 save Unicode code points), so the length of the char type is 2 bytes.
!!! question "Is there ambiguity in calling data structures based on arrays 'static data structures'? Because operations like push and pop on stacks are 'dynamic.'" **Q**: Is there ambiguity in calling data structures based on arrays "static data structures"? Because operations like push and pop on stacks are "dynamic".
While stacks indeed allow for dynamic data operations, the data structure itself remains "static" (with unchangeable length). Even though data structures based on arrays 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 copied into it. While stacks indeed allow for dynamic data operations, the data structure itself remains "static" (with unchangeable length). Even though data structures based on arrays 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 copied into it.
!!! question "When building stacks (queues) without specifying their size, why are they considered 'static data structures'?" **Q**: When building stacks (queues) without specifying their size, why are they considered "static data structures"?
In high-level programming languages, we don't need to manually specify the initial capacity of stacks (queues); this task is automatically handled internally by the class. For example, the initial capacity of Java's ArrayList is usually 10. Furthermore, the expansion operation is also implemented automatically. See the subsequent "List" chapter for details. In high-level programming languages, we don't need to manually specify the initial capacity of stacks (queues); this task is automatically handled internally by the class. For example, the initial capacity of Java's ArrayList is usually 10. Furthermore, the expansion operation is also implemented automatically. See the subsequent "List" chapter for details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,399 @@
# Double-Ended Queue
In a regular queue, we can only delete elements from the head or add elements to the tail. As shown in the figure below, a "double-ended queue (deque)" offers more flexibility, allowing the addition or removal of elements at both the head and the tail.
![Operations in Double-Ended Queue](deque.assets/deque_operations.png)
## Common Operations in Double-Ended Queue
The common operations in a double-ended queue are listed below, and the specific method names depend on the programming language used.
<p align="center"> Table <id> &nbsp; Efficiency of Double-Ended Queue Operations </p>
| Method Name | Description | Time Complexity |
| ------------- | --------------------------- | --------------- |
| `pushFirst()` | Add an element to the front | $O(1)$ |
| `pushLast()` | Add an element to the rear | $O(1)$ |
| `popFirst()` | Remove the front element | $O(1)$ |
| `popLast()` | Remove the rear element | $O(1)$ |
| `peekFirst()` | Access the front element | $O(1)$ |
| `peekLast()` | Access the rear element | $O(1)$ |
Similarly, we can directly use the double-ended queue classes implemented in programming languages:
=== "Python"
```python title="deque.py"
from collections import deque
# Initialize the deque
deque: deque[int] = deque()
# Enqueue elements
deque.append(2) # Add to the rear
deque.append(5)
deque.append(4)
deque.appendleft(3) # Add to the front
deque.appendleft(1)
# Access elements
front: int = deque[0] # Front element
rear: int = deque[-1] # Rear element
# Dequeue elements
pop_front: int = deque.popleft() # Front element dequeued
pop_rear: int = deque.pop() # Rear element dequeued
# Get the length of the deque
size: int = len(deque)
# Check if the deque is empty
is_empty: bool = len(deque) == 0
```
=== "C++"
```cpp title="deque.cpp"
/* Initialize the deque */
deque<int> deque;
/* Enqueue elements */
deque.push_back(2); // Add to the rear
deque.push_back(5);
deque.push_back(4);
deque.push_front(3); // Add to the front
deque.push_front(1);
/* Access elements */
int front = deque.front(); // Front element
int back = deque.back(); // Rear element
/* Dequeue elements */
deque.pop_front(); // Front element dequeued
deque.pop_back(); // Rear element dequeued
/* Get the length of the deque */
int size = deque.size();
/* Check if the deque is empty */
bool empty = deque.empty();
```
=== "Java"
```java title="deque.java"
/* Initialize the deque */
Deque<Integer> deque = new LinkedList<>();
/* Enqueue elements */
deque.offerLast(2); // Add to the rear
deque.offerLast(5);
deque.offerLast(4);
deque.offerFirst(3); // Add to the front
deque.offerFirst(1);
/* Access elements */
int peekFirst = deque.peekFirst(); // Front element
int peekLast = deque.peekLast(); // Rear element
/* Dequeue elements */
int popFirst = deque.pollFirst(); // Front element dequeued
int popLast = deque.pollLast(); // Rear element dequeued
/* Get the length of the deque */
int size = deque.size();
/* Check if the deque is empty */
boolean isEmpty = deque.isEmpty();
```
=== "C#"
```csharp title="deque.cs"
/* Initialize the deque */
// In C#, LinkedList is used as a deque
LinkedList<int> deque = new();
/* Enqueue elements */
deque.AddLast(2); // Add to the rear
deque.AddLast(5);
deque.AddLast(4);
deque.AddFirst(3); // Add to the front
deque.AddFirst(1);
/* Access elements */
int peekFirst = deque.First.Value; // Front element
int peekLast = deque.Last.Value; // Rear element
/* Dequeue elements */
deque.RemoveFirst(); // Front element dequeued
deque.RemoveLast(); // Rear element dequeued
/* Get the length of the deque */
int size = deque.Count;
/* Check if the deque is empty */
bool isEmpty = deque.Count == 0;
```
=== "Go"
```go title="deque_test.go"
/* Initialize the deque */
// In Go, use list as a deque
deque := list.New()
/* Enqueue elements */
deque.PushBack(2) // Add to the rear
deque.PushBack(5)
deque.PushBack(4)
deque.PushFront(3) // Add to the front
deque.PushFront(1)
/* Access elements */
front := deque.Front() // Front element
rear := deque.Back() // Rear element
/* Dequeue elements */
deque.Remove(front) // Front element dequeued
deque.Remove(rear) // Rear element dequeued
/* Get the length of the deque */
size := deque.Len()
/* Check if the 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
var deque: [Int] = []
/* Enqueue elements */
deque.append(2) // Add to the rear
deque.append(5)
deque.append(4)
deque.insert(3, at: 0) // Add to the front
deque.insert(1, at: 0)
/* Access elements */
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() // Front element dequeued
let popLast = deque.removeLast() // Rear element dequeued
/* Get the length of the deque */
let size = deque.count
/* Check if the 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
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
deque.unshift(3);
deque.unshift(1);
/* Access elements */
const peekFirst = deque[0]; // Front element
const peekLast = deque[deque.length - 1]; // Rear element
/* Dequeue elements */
// Note that shift() has a time complexity of O(n) as it's an array
const popFront = deque.shift(); // Front element dequeued
const popBack = deque.pop(); // Rear element dequeued
/* Get the length of the deque */
const size = deque.length;
/* Check if the 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
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
deque.unshift(3);
deque.unshift(1);
/* Access elements */
const peekFirst: number = deque[0]; // Front element
const peekLast: number = deque[deque.length - 1]; // Rear element
/* Dequeue elements */
// Note that shift() has a time complexity of O(n) as it's an array
const popFront: number = deque.shift() as number; // Front element dequeued
const popBack: number = deque.pop() as number; // Rear element dequeued
/* Get the length of the deque */
const size: number = deque.length;
/* Check if the deque is empty */
const isEmpty: boolean = size === 0;
```
=== "Dart"
```dart title="deque.dart"
/* Initialize the deque */
// In Dart, Queue is defined as a deque
Queue<int> deque = Queue<int>();
/* Enqueue elements */
deque.addLast(2); // Add to the rear
deque.addLast(5);
deque.addLast(4);
deque.addFirst(3); // Add to the front
deque.addFirst(1);
/* Access elements */
int peekFirst = deque.first; // Front element
int peekLast = deque.last; // Rear element
/* Dequeue elements */
int popFirst = deque.removeFirst(); // Front element dequeued
int popLast = deque.removeLast(); // Rear element dequeued
/* Get the length of the deque */
int size = deque.length;
/* Check if the deque is empty */
bool isEmpty = deque.isEmpty;
```
=== "Rust"
```rust title="deque.rs"
/* Initialize the deque */
let mut deque: VecDeque<u32> = VecDeque::new();
/* Enqueue elements */
deque.push_back(2); // Add to the rear
deque.push_back(5);
deque.push_back(4);
deque.push_front(3); // Add to the front
deque.push_front(1);
/* Access elements */
if let Some(front) = deque.front() { // Front element
}
if let Some(rear) = deque.back() { // Rear element
}
/* Dequeue elements */
if let Some(pop_front) = deque.pop_front() { // Front element dequeued
}
if let Some(pop_rear) = deque.pop_back() { // Rear element dequeued
}
/* Get the length of the deque */
let size = deque.len();
/* Check if the deque is empty */
let is_empty = deque.is_empty();
```
=== "C"
```c title="deque.c"
// C does not provide a built-in deque
```
=== "Zig"
```zig title="deque.zig"
```
??? pythontutor "可视化运行"
<iframe width="800" height="600" frameborder="0" src="https://pythontutor.com/iframe-embed.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&codeDivHeight=370&codeDivWidth=300&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>
## Implementing a Double-Ended Queue *
The implementation of a double-ended queue is similar to that of a regular queue, with the choice of either linked lists or arrays as the underlying data structure.
### Implementation Based on Doubly Linked List
Recall from the previous section that we used a regular singly linked list to implement a queue, as it conveniently allows for deleting the head node (corresponding to dequeue operation) and adding new nodes after the tail node (corresponding to enqueue operation).
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 another symmetric direction of operations. For this, we use a "doubly linked list" as the underlying data structure of the double-ended queue.
As shown in the figure below, 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.
=== "LinkedListDeque"
![Implementing Double-Ended Queue with Doubly Linked List for Enqueue and Dequeue Operations](deque.assets/linkedlist_deque.png)
=== "pushLast()"
![linkedlist_deque_push_last](deque.assets/linkedlist_deque_push_last.png)
=== "pushFirst()"
![linkedlist_deque_push_first](deque.assets/linkedlist_deque_push_first.png)
=== "popLast()"
![linkedlist_deque_pop_last](deque.assets/linkedlist_deque_pop_last.png)
=== "popFirst()"
![linkedlist_deque_pop_first](deque.assets/linkedlist_deque_pop_first.png)
The implementation code is as follows:
```src
[file]{linkedlist_deque}-[class]{linked_list_deque}-[func]{}
```
### Implementation Based on Array
As shown in the figure below, similar to implementing a queue with an array, we can also use a circular array to implement a double-ended queue.
=== "ArrayDeque"
![Implementing Double-Ended Queue with Array for Enqueue and Dequeue Operations](deque.assets/array_deque.png)
=== "pushLast()"
![array_deque_push_last](deque.assets/array_deque_push_last.png)
=== "pushFirst()"
![array_deque_push_first](deque.assets/array_deque_push_first.png)
=== "popLast()"
![array_deque_pop_last](deque.assets/array_deque_pop_last.png)
=== "popFirst()"
![array_deque_pop_first](deque.assets/array_deque_pop_first.png)
The implementation only needs to add methods for "front enqueue" and "rear dequeue":
```src
[file]{array_deque}-[func]{}
```
## Applications of Double-Ended Queue
The double-ended queue combines the logic of both stacks and queues, **thus it can implement all the application scenarios of these two, while offering greater flexibility**.
We know that the "undo" feature in software 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 length of the stack 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, which is 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.

View file

@ -0,0 +1,13 @@
# Stack and Queue
<div class="center-table" markdown>
![Stack and Queue](../assets/covers/chapter_stack_and_queue.jpg)
</div>
!!! abstract
Stacks are like stacking cats, while queues are like cats lining up.
They respectively represent the logical relationships of Last-In-First-Out (LIFO) and First-In-First-Out (FIFO).

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,375 @@
# Queue
"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 back of the queue, and people at the front of the queue leave one by one.
As shown in the figure below, we call the front of the queue the "head" and the back the "tail." The operation of adding elements to the tail of the queue is termed "enqueue," and the operation of removing elements from the head is termed "dequeue."
![Queue's First-In-First-Out Rule](queue.assets/queue_operations.png)
## Common Operations on Queue
The common operations on a queue are shown in the table below. Note that method names may vary across different programming languages. Here, we adopt the same naming convention as used for stacks.
<p align="center"> Table <id> &nbsp; Efficiency of Queue Operations </p>
| 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)$ |
We can directly use the ready-made queue classes in programming languages:
=== "Python"
```python title="queue.py"
from collections import deque
# Initialize the 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
que: deque[int] = deque()
# Enqueue elements
que.append(1)
que.append(3)
que.append(2)
que.append(5)
que.append(4)
# Access the front element
front: int = que[0]
# Dequeue an element
pop: int = que.popleft()
# Get the length of the queue
size: int = len(que)
# Check if the queue is empty
is_empty: bool = len(que) == 0
```
=== "C++"
```cpp title="queue.cpp"
/* Initialize the queue */
queue<int> queue;
/* Enqueue elements */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* Access the front element */
int front = queue.front();
/* Dequeue an element */
queue.pop();
/* Get the length of the queue */
int size = queue.size();
/* Check if the queue is empty */
bool empty = queue.empty();
```
=== "Java"
```java title="queue.java"
/* Initialize the queue */
Queue<Integer> queue = new LinkedList<>();
/* Enqueue elements */
queue.offer(1);
queue.offer(3);
queue.offer(2);
queue.offer(5);
queue.offer(4);
/* Access the front element */
int peek = queue.peek();
/* Dequeue an element */
int pop = queue.poll();
/* Get the length of the queue */
int size = queue.size();
/* Check if the queue is empty */
boolean isEmpty = queue.isEmpty();
```
=== "C#"
```csharp title="queue.cs"
/* Initialize the queue */
Queue<int> queue = new();
/* Enqueue elements */
queue.Enqueue(1);
queue.Enqueue(3);
queue.Enqueue(2);
queue.Enqueue(5);
queue.Enqueue(4);
/* Access the front element */
int peek = queue.Peek();
/* Dequeue an element */
int pop = queue.Dequeue();
/* Get the length of the queue */
int size = queue.Count;
/* Check if the queue is empty */
bool isEmpty = queue.Count == 0;
```
=== "Go"
```go title="queue_test.go"
/* Initialize the queue */
// In Go, use list as a queue
queue := list.New()
/* Enqueue elements */
queue.PushBack(1)
queue.PushBack(3)
queue.PushBack(2)
queue.PushBack(5)
queue.PushBack(4)
/* Access the front element */
peek := queue.Front()
/* Dequeue an element */
pop := queue.Front()
queue.Remove(pop)
/* Get the length of the queue */
size := queue.Len()
/* Check if the 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
var queue: [Int] = []
/* Enqueue elements */
queue.append(1)
queue.append(3)
queue.append(2)
queue.append(5)
queue.append(4)
/* Access the front element */
let peek = queue.first!
/* Dequeue an element */
// Since it's an array, removeFirst has a complexity of O(n)
let pool = queue.removeFirst()
/* Get the length of the queue */
let size = queue.count
/* Check if the 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
const queue = [];
/* Enqueue elements */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* Access the 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)
const pop = queue.shift();
/* Get the length of the queue */
const size = queue.length;
/* Check if the 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
const queue: number[] = [];
/* Enqueue elements */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);
/* Access the 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)
const pop = queue.shift();
/* Get the length of the queue */
const size = queue.length;
/* Check if the 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
Queue<int> queue = Queue();
/* Enqueue elements */
queue.add(1);
queue.add(3);
queue.add(2);
queue.add(5);
queue.add(4);
/* Access the front element */
int peek = queue.first;
/* Dequeue an element */
int pop = queue.removeFirst();
/* Get the length of the queue */
int size = queue.length;
/* Check if the 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
let mut deque: VecDeque<u32> = VecDeque::new();
/* Enqueue elements */
deque.push_back(1);
deque.push_back(3);
deque.push_back(2);
deque.push_back(5);
deque.push_back(4);
/* Access the front element */
if let Some(front) = deque.front() {
}
/* Dequeue an element */
if let Some(pop) = deque.pop_front() {
}
/* Get the length of the queue */
let size = deque.len();
/* Check if the queue is empty */
let is_empty = deque.is_empty();
```
=== "C"
```c title="queue.c"
// C does not provide a built-in queue
```
=== "Zig"
```zig title="queue.zig"
```
??? pythontutor "可视化运行"
<iframe width="800" height="600" frameborder="0" src="https://pythontutor.com/iframe-embed.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%E9%98%9F%E5%88%97%0A%20%20%20%20%23%20%E5%9C%A8%20Python%20%E4%B8%AD%EF%BC%8C%E6%88%91%E4%BB%AC%E4%B8%80%E8%88%AC%E5%B0%86%E5%8F%8C%E5%90%91%E9%98%9F%E5%88%97%E7%B1%BB%20deque%20%E7%9C%8B%E4%BD%9C%E9%98%9F%E5%88%97%E4%BD%BF%E7%94%A8%0A%20%20%20%20%23%20%E8%99%BD%E7%84%B6%20queue.Queue%28%29%20%E6%98%AF%E7%BA%AF%E6%AD%A3%E7%9A%84%E9%98%9F%E5%88%97%E7%B1%BB%EF%BC%8C%E4%BD%86%E4%B8%8D%E5%A4%AA%E5%A5%BD%E7%94%A8%0A%20%20%20%20que%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%20que.append%281%29%0A%20%20%20%20que.append%283%29%0A%20%20%20%20que.append%282%29%0A%20%20%20%20que.append%285%29%0A%20%20%20%20que.append%284%29%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E9%98%9F%E9%A6%96%E5%85%83%E7%B4%A0%0A%20%20%20%20front%20%3D%20que%5B0%5D%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%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E9%98%9F%0A%20%20%20%20pop%20%3D%20que.popleft%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E9%98%9F%E5%90%8E%20que%20%3D%22,%20que%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E9%98%9F%E5%88%97%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28que%29%0A%20%20%20%20print%28%22%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%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%28que%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E9%98%9F%E5%88%97%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=370&codeDivWidth=300&cumulative=false&curInstr=3&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>
## Implementing a Queue
To implement a queue, we need a data structure that allows adding elements at one end and removing them at the other. Both linked lists and arrays meet this requirement.
### Implementation Based on Linked List
As shown in the figure below, we can consider the "head node" and "tail node" of a linked list as the "head" and "tail" of the queue, respectively. We restrict the operations so that nodes can only be added at the tail and removed at the head.
=== "LinkedListQueue"
![Implementing Queue with Linked List for Enqueue and Dequeue Operations](queue.assets/linkedlist_queue.png)
=== "push()"
![linkedlist_queue_push](queue.assets/linkedlist_queue_push.png)
=== "pop()"
![linkedlist_queue_pop](queue.assets/linkedlist_queue_pop.png)
Below is the code for implementing a queue using a linked list:
```src
[file]{linkedlist_queue}-[class]{linked_list_queue}-[func]{}
```
### Implementation Based on Array
Deleting the first element in an array has a time complexity of $O(n)$, which would make the dequeue operation inefficient. However, this problem can be cleverly avoided as follows.
We can use a variable `front` to point to the index of the head element and maintain a `size` variable to record the length of the queue. Define `rear = front + size`, which points to the position right after 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 the figure below.
- 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)$.
=== "ArrayQueue"
![Implementing Queue with Array for Enqueue and Dequeue Operations](queue.assets/array_queue.png)
=== "push()"
![array_queue_push](queue.assets/array_queue_push.png)
=== "pop()"
![array_queue_pop](queue.assets/array_queue_pop.png)
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 issue, we can treat the array as a "circular array."
For 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:
```src
[file]{array_queue}-[class]{array_queue}-[func]{}
```
The above implementation of the queue still has limitations: its length is fixed. However, this issue is not difficult to resolve. We can replace the array with a dynamic array to introduce an expansion mechanism. Interested readers can try to implement this themselves.
The comparison of the two implementations is consistent with that of the stack and is not repeated here.
## 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,383 @@
# Stack
"Stack" is a linear data structure that follows the principle of Last-In-First-Out (LIFO).
We can compare a stack to a pile of plates on a table. To access the bottom plate, one must remove the plates on top. If we replace the plates with various types of elements (such as integers, characters, objects, etc.), we obtain the data structure known as a stack.
As shown in the following figure, we refer to the top of the pile of elements as the "top of the stack" and the bottom as the "bottom of the stack." The operation of adding elements to the top of the stack is called "push," and the operation of removing the top element is called "pop."
![Stack's Last-In-First-Out Rule](stack.assets/stack_operations.png)
## Common Operations on Stack
The common operations on a stack are shown in the table below. The specific method names depend on the programming language used. Here, we use `push()`, `pop()`, and `peek()` as examples.
<p align="center"> Table <id> &nbsp; Efficiency of Stack Operations </p>
| 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)$ |
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.
=== "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
stack: list[int] = []
# Push elements onto the stack
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)
# Access the top element of the stack
peek: int = stack[-1]
# Pop an element from the stack
pop: int = stack.pop()
# Get the length of the stack
size: int = len(stack)
# Check if the stack is empty
is_empty: bool = len(stack) == 0
```
=== "C++"
```cpp title="stack.cpp"
/* Initialize the stack */
stack<int> stack;
/* Push elements onto the stack */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* Access the top element of the stack */
int top = stack.top();
/* Pop an element from the stack */
stack.pop(); // No return value
/* Get the length of the stack */
int size = stack.size();
/* Check if the stack is empty */
bool empty = stack.empty();
```
=== "Java"
```java title="stack.java"
/* Initialize the stack */
Stack<Integer> stack = new Stack<>();
/* Push elements onto the stack */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* Access the top element of the stack */
int peek = stack.peek();
/* Pop an element from the stack */
int pop = stack.pop();
/* Get the length of the stack */
int size = stack.size();
/* Check if the stack is empty */
boolean isEmpty = stack.isEmpty();
```
=== "C#"
```csharp title="stack.cs"
/* Initialize the stack */
Stack<int> stack = new();
/* Push elements onto the stack */
stack.Push(1);
stack.Push(3);
stack.Push(2);
stack.Push(5);
stack.Push(4);
/* Access the top element of the stack */
int peek = stack.Peek();
/* Pop an element from the stack */
int pop = stack.Pop();
/* Get the length of the stack */
int size = stack.Count;
/* Check if the stack is 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
var stack []int
/* Push elements onto the stack */
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 */
peek := stack[len(stack)-1]
/* Pop an element from the stack */
pop := stack[len(stack)-1]
stack = stack[:len(stack)-1]
/* Get the length of the stack */
size := len(stack)
/* Check if the stack is 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
var stack: [Int] = []
/* Push elements onto the stack */
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)
/* Access the top element of the stack */
let peek = stack.last!
/* Pop an element from the stack */
let pop = stack.removeLast()
/* Get the length of the stack */
let size = stack.count
/* Check if the stack is 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
const stack = [];
/* Push elements onto the stack */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* Access the top element of the stack */
const peek = stack[stack.length-1];
/* Pop an element from the stack */
const pop = stack.pop();
/* Get the length of the stack */
const size = stack.length;
/* Check if the stack is 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
const stack: number[] = [];
/* Push elements onto the stack */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* Access the top element of the stack */
const peek = stack[stack.length - 1];
/* Pop an element from the stack */
const pop = stack.pop();
/* Get the length of the stack */
const size = stack.length;
/* Check if the stack is 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
List<int> stack = [];
/* Push elements onto the stack */
stack.add(1);
stack.add(3);
stack.add(2);
stack.add(5);
stack.add(4);
/* Access the top element of the stack */
int peek = stack.last;
/* Pop an element from the stack */
int pop = stack.removeLast();
/* Get the length of the stack */
int size = stack.length;
/* Check if the stack is empty */
bool isEmpty = stack.isEmpty;
```
=== "Rust"
```rust title="stack.rs"
/* Initialize the stack */
// Use Vec as a stack
let mut stack: Vec<i32> = Vec::new();
/* Push elements onto the stack */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);
/* Access the top element of the stack */
let top = stack.last().unwrap();
/* Pop an element from the stack */
let pop = stack.pop().unwrap();
/* Get the length of the stack */
let size = stack.len();
/* Check if the stack is empty */
let is_empty = stack.is_empty();
```
=== "C"
```c title="stack.c"
// C does not provide a built-in stack
```
=== "Zig"
```zig title="stack.zig"
```
??? pythontutor "可视化运行"
<iframe width="800" height="600" frameborder="0" src="https://pythontutor.com/iframe-embed.html#code=%22%22%22Driver%20Code%22%22%22%0Aif%20__name__%20%3D%3D%20%22__main__%22%3A%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%A0%88%0A%20%20%20%20%23%20Python%20%E6%B2%A1%E6%9C%89%E5%86%85%E7%BD%AE%E7%9A%84%E6%A0%88%E7%B1%BB%EF%BC%8C%E5%8F%AF%E4%BB%A5%E6%8A%8A%20list%20%E5%BD%93%E4%BD%9C%E6%A0%88%E6%9D%A5%E4%BD%BF%E7%94%A8%0A%20%20%20%20stack%20%3D%20%5B%5D%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%85%A5%E6%A0%88%0A%20%20%20%20stack.append%281%29%0A%20%20%20%20stack.append%283%29%0A%20%20%20%20stack.append%282%29%0A%20%20%20%20stack.append%285%29%0A%20%20%20%20stack.append%284%29%0A%20%20%20%20print%28%22%E6%A0%88%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%AE%BF%E9%97%AE%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%0A%20%20%20%20peek%20%3D%20stack%5B-1%5D%0A%20%20%20%20print%28%22%E6%A0%88%E9%A1%B6%E5%85%83%E7%B4%A0%20peek%20%3D%22,%20peek%29%0A%0A%20%20%20%20%23%20%E5%85%83%E7%B4%A0%E5%87%BA%E6%A0%88%0A%20%20%20%20pop%20%3D%20stack.pop%28%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%85%83%E7%B4%A0%20pop%20%3D%22,%20pop%29%0A%20%20%20%20print%28%22%E5%87%BA%E6%A0%88%E5%90%8E%20stack%20%3D%22,%20stack%29%0A%0A%20%20%20%20%23%20%E8%8E%B7%E5%8F%96%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%0A%20%20%20%20size%20%3D%20len%28stack%29%0A%20%20%20%20print%28%22%E6%A0%88%E7%9A%84%E9%95%BF%E5%BA%A6%20size%20%3D%22,%20size%29%0A%0A%20%20%20%20%23%20%E5%88%A4%E6%96%AD%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%0A%20%20%20%20is_empty%20%3D%20len%28stack%29%20%3D%3D%200%0A%20%20%20%20print%28%22%E6%A0%88%E6%98%AF%E5%90%A6%E4%B8%BA%E7%A9%BA%20%3D%22,%20is_empty%29&codeDivHeight=370&codeDivWidth=300&cumulative=false&curInstr=2&heapPrimitives=nevernest&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false"> </iframe>
## Implementing a Stack
To understand the mechanics of a stack more deeply, 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 "mask" some unrelated operations of arrays or linked lists to make their logic conform to the characteristics of a stack.
### Implementation Based on Linked List
When implementing a stack using a linked list, we can consider the head node of the list as the top of the stack and the tail node as the bottom of the stack.
As shown in the figure below, 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.
=== "LinkedListStack"
![Implementing Stack with Linked List for Push and Pop Operations](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)
Below is an example code for implementing a stack based on a linked list:
```src
[file]{linkedlist_stack}-[class]{linked_list_stack}-[func]{}
```
### Implementation Based on Array
When implementing a stack using an array, we can consider the end of the array as the top of the stack. As shown in the figure below, 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)$.
=== "ArrayStack"
![Implementing Stack with Array for Push and Pop Operations](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)
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:
```src
[file]{array_stack}-[class]{array_stack}-[func]{}
```
## 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.
**Time Efficiency**
In the array-based implementation, both push and pop operations occur in pre-allocated continuous 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 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.
Thus, when the elements for push and pop operations are basic data types like `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.
**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**.
However, since linked list nodes require extra space for storing pointers, **the space occupied by linked list nodes is relatively larger**.
In summary, we cannot simply determine which implementation is more memory-efficient. It requires analysis based on specific circumstances.
## 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.

View file

@ -0,0 +1,31 @@
# Summary
### Key Review
- A stack is a data structure that follows the Last-In-First-Out (LIFO) principle and can be implemented using either 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 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 a stack may lead to some level 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 either arrays or linked lists. The conclusions regarding time and space efficiency for queues are similar to those for stacks.
- A double-ended queue is a more flexible type of queue that allows adding and removing elements from both ends.
### Q & A
**Q**: Is the browser's forward and backward functionality implemented with a doubly linked list?
The forward and backward functionality of a browser fundamentally represents the "stack" concept. When a user visits a new page, it is added to the top of the stack; when they click the back button, the page is popped from the top. A double-ended queue can conveniently implement some additional operations, as mentioned in the "Double-Ended Queue" section.
**Q**: After popping from a stack, is it necessary 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 isn't required; in C and C++, manual memory release is necessary if the node will no longer be used.
**Q**: A double-ended queue seems like two stacks joined together. What are its uses?
A double-ended queue is essentially a combination of a stack and a queue, or like two stacks joined together. It exhibits both stack and queue logic, therefore enabling the implementation of all applications of stacks and queues with added flexibility.
**Q**: How exactly are undo and redo implemented?
Undo and redo are implemented using 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.

View file

@ -209,28 +209,21 @@
// 请注意由于是数组unshift() 方法的时间复杂度为 O(n) // 请注意由于是数组unshift() 方法的时间复杂度为 O(n)
deque.unshift(3); deque.unshift(3);
deque.unshift(1); deque.unshift(1);
console.log("双向队列 deque = ", deque);
/* 访问元素 */ /* 访问元素 */
const peekFirst = deque[0]; const peekFirst = deque[0];
console.log("队首元素 peekFirst = " + peekFirst);
const peekLast = deque[deque.length - 1]; const peekLast = deque[deque.length - 1];
console.log("队尾元素 peekLast = " + peekLast);
/* 元素出队 */ /* 元素出队 */
// 请注意由于是数组shift() 方法的时间复杂度为 O(n) // 请注意由于是数组shift() 方法的时间复杂度为 O(n)
const popFront = deque.shift(); const popFront = deque.shift();
console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque);
const popBack = deque.pop(); const popBack = deque.pop();
console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque);
/* 获取双向队列的长度 */ /* 获取双向队列的长度 */
const size = deque.length; const size = deque.length;
console.log("双向队列长度 size = " + size);
/* 判断双向队列是否为空 */ /* 判断双向队列是否为空 */
const isEmpty = size === 0; const isEmpty = size === 0;
console.log("双向队列是否为空 = " + isEmpty);
``` ```
=== "TS" === "TS"
@ -247,28 +240,21 @@
// 请注意由于是数组unshift() 方法的时间复杂度为 O(n) // 请注意由于是数组unshift() 方法的时间复杂度为 O(n)
deque.unshift(3); deque.unshift(3);
deque.unshift(1); deque.unshift(1);
console.log("双向队列 deque = ", deque);
/* 访问元素 */ /* 访问元素 */
const peekFirst: number = deque[0]; const peekFirst: number = deque[0];
console.log("队首元素 peekFirst = " + peekFirst);
const peekLast: number = deque[deque.length - 1]; const peekLast: number = deque[deque.length - 1];
console.log("队尾元素 peekLast = " + peekLast);
/* 元素出队 */ /* 元素出队 */
// 请注意由于是数组shift() 方法的时间复杂度为 O(n) // 请注意由于是数组shift() 方法的时间复杂度为 O(n)
const popFront: number = deque.shift() as number; const popFront: number = deque.shift() as number;
console.log("队首出队元素 popFront = " + popFront + ",队首出队后 deque = " + deque);
const popBack: number = deque.pop() as number; const popBack: number = deque.pop() as number;
console.log("队尾出队元素 popBack = " + popBack + ",队尾出队后 deque = " + deque);
/* 获取双向队列的长度 */ /* 获取双向队列的长度 */
const size: number = deque.length; const size: number = deque.length;
console.log("双向队列长度 size = " + size);
/* 判断双向队列是否为空 */ /* 判断双向队列是否为空 */
const isEmpty: boolean = size === 0; const isEmpty: boolean = size === 0;
console.log("双向队列是否为空 = " + isEmpty);
``` ```
=== "Dart" === "Dart"

View file

@ -186,7 +186,7 @@
```javascript title="stack.js" ```javascript title="stack.js"
/* 初始化栈 */ /* 初始化栈 */
// Javascript 没有内置的栈类,可以把 Array 当作栈来使用 // JavaScript 没有内置的栈类,可以把 Array 当作栈来使用
const stack = []; const stack = [];
/* 元素入栈 */ /* 元素入栈 */
@ -213,7 +213,7 @@
```typescript title="stack.ts" ```typescript title="stack.ts"
/* 初始化栈 */ /* 初始化栈 */
// Typescript 没有内置的栈类,可以把 Array 当作栈来使用 // TypeScript 没有内置的栈类,可以把 Array 当作栈来使用
const stack: number[] = []; const stack: number[] = [];
/* 元素入栈 */ /* 元素入栈 */