mirror of
https://github.com/krahets/hello-algo.git
synced 2026-07-08 21:46:06 +00:00
Revisit the English version (#1835)
* Review the English version using Claude-4.5. * Update mkdocs.yml * Align the section titles. * Bug fixes
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
# Array
|
||||
|
||||
An <u>array</u> 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 <u>index</u>. Please refer to the figure below to observe how arrays work and grasp these key terms.
|
||||
An <u>array</u> 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 <u>index</u>. The figure below illustrates the main concepts and storage method of arrays.
|
||||
|
||||

|
||||
|
||||
## Common operations on arrays
|
||||
## Common Array Operations
|
||||
|
||||
### Initializing arrays
|
||||
### 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"
|
||||
|
||||
@@ -25,7 +25,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 };
|
||||
```
|
||||
@@ -51,9 +51,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}
|
||||
```
|
||||
|
||||
@@ -95,10 +95,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<i32> = vec![1, 3, 2, 5, 4];
|
||||
```
|
||||
|
||||
@@ -113,109 +113,123 @@ 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)
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title="array.rb"
|
||||
# Initialize array
|
||||
arr = Array.new(5, 0)
|
||||
nums = [1, 3, 2, 5, 4]
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title="array.zig"
|
||||
// Initialize array
|
||||
var arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
|
||||
var nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
const arr = [_]i32{0} ** 5; // { 0, 0, 0, 0, 0 }
|
||||
const nums = [_]i32{ 1, 3, 2, 5, 4 };
|
||||
```
|
||||
|
||||
### Accessing elements
|
||||
??? pythontutor "Visualize code execution"
|
||||
|
||||
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.
|
||||
https://pythontutor.com/render.html#code=%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E6%95%B0%E7%BB%84%0Aarr%20%3D%20%5B0%5D%20*%205%20%20%23%20%5B%200,%200,%200,%200,%200%20%5D%0Anums%20%3D%20%5B1,%203,%202,%205,%204%5D&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
### 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 the figure below to calculate the element's memory address and directly access that element.
|
||||
|
||||

|
||||
|
||||
As observed in the figure above, 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 the figure above, 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.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{random_access}
|
||||
```
|
||||
|
||||
### Inserting elements
|
||||
### Inserting Elements
|
||||
|
||||
Array elements are tightly packed in memory, with no space available to accommodate additional data between them. As illustrated in the figure below, inserting an element in the middle of an array requires shifting all subsequent elements back by one position to create room for the new element.
|
||||
Array elements are stored "tightly adjacent" in memory, with no space between them to store any additional data. As shown in the figure below, 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.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Deleting elements
|
||||
### Removing Elements
|
||||
|
||||
Similarly, as depicted in the figure below, to delete an element at index $i$, all elements following index $i$ must be moved forward by one position.
|
||||
Similarly, as shown in the figure below, to delete the element at index $i$, we need to shift all elements after index $i$ forward by one position.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
In summary, the insertion and deletion operations in arrays present the following disadvantages:
|
||||
Overall, array insertion and deletion operations have the following drawbacks:
|
||||
|
||||
- **High time complexity**: Both insertion and deletion in an array have an average time complexity of $O(n)$, where $n$ is the length of the array.
|
||||
- **Loss of elements**: Due to the fixed length of arrays, elements that exceed the array's capacity are lost during insertion.
|
||||
- **Waste of memory**: Initializing a longer array and utilizing only the front part results in "meaningless" end elements during insertion, leading to some wasted memory space.
|
||||
- **High time complexity**: The average time complexity for both insertion and deletion in arrays is $O(n)$, where $n$ is the length of the array.
|
||||
- **Loss of elements**: Since the length of an array is immutable, after inserting an element, elements that exceed the array's length will be lost.
|
||||
- **Memory waste**: We can initialize a relatively long array and only use the front portion, so that when inserting data, the lost elements at the end are "meaningless", but this causes some memory space to be wasted.
|
||||
|
||||
### Traversing arrays
|
||||
### Traversing Arrays
|
||||
|
||||
In most programming languages, we can traverse an array either by using indices or by directly iterating over each element:
|
||||
In most programming languages, we can traverse an array either by index or by directly iterating through each element in the array:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{traverse}
|
||||
```
|
||||
|
||||
### Finding elements
|
||||
### 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.
|
||||
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.
|
||||
|
||||
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".
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
### Expanding arrays
|
||||
### 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**.
|
||||
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**.
|
||||
|
||||
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:
|
||||
|
||||
```src
|
||||
[file]{array}-[class]{}-[func]{extend}
|
||||
```
|
||||
|
||||
## Advantages and limitations of arrays
|
||||
## Advantages and Limitations of Arrays
|
||||
|
||||
Arrays are stored in contiguous memory spaces and consist of elements of the same type. This approach provides substantial prior information that systems can leverage to optimize the efficiency of data structure operations.
|
||||
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.
|
||||
|
||||
- **High space efficiency**: Arrays allocate a contiguous block of memory for data, eliminating the need for additional structural overhead.
|
||||
- **Support for random access**: Arrays allow $O(1)$ time access to any element.
|
||||
- **Cache locality**: When accessing array elements, the computer not only loads them but also caches the surrounding data, utilizing high-speed cache to enchance subsequent operation speeds.
|
||||
- **High space efficiency**: Arrays allocate 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.
|
||||
|
||||
However, continuous space storage is a double-edged sword, with the following limitations:
|
||||
Contiguous space storage is a double-edged sword with the following limitations:
|
||||
|
||||
- **Low efficiency in insertion and deletion**: As arrays accumulate many elements, inserting or deleting elements requires shifting a large number of elements.
|
||||
- **Fixed length**: The length of an array is fixed after initialization. Expanding an array requires copying all data to a new array, incurring significant costs.
|
||||
- **Space wastage**: If the allocated array size exceeds the what is necessary, the extra space is wasted.
|
||||
- **Low 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.
|
||||
|
||||
## Typical applications of arrays
|
||||
## 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.
|
||||
Arrays are a fundamental and common data structure, frequently used in various algorithms and for implementing various 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.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Arrays and linked lists
|
||||
# Arrays and Linked Lists
|
||||
|
||||

|
||||

|
||||
|
||||
!!! 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.
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
# Linked list
|
||||
# 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 <u>linked list</u> 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 <u>linked list</u> 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.
|
||||
|
||||

|
||||
|
||||
As shown in the figure above, we see that the basic building block of a linked list is the <u>node</u> object. Each node comprises two key components: the node's "value" and a "reference" to the next node.
|
||||
Observing the figure above, the basic unit of a linked list is a <u>node</u> 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"
|
||||
|
||||
@@ -162,7 +162,27 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title=""
|
||||
/* Linked list node class */
|
||||
// Constructor
|
||||
class ListNode(x: Int) {
|
||||
val _val: Int = x // Node value
|
||||
val next: ListNode? = null // Reference to the next node
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```ruby title=""
|
||||
# Linked list node class
|
||||
class ListNode
|
||||
attr_accessor :val # Node value
|
||||
attr_accessor :next # Reference to the next node
|
||||
|
||||
def initialize(val=0, next_node=nil)
|
||||
@val = val
|
||||
@next = next_node
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
@@ -185,16 +205,16 @@ As the code below illustrates, a `ListNode` in a linked list, besides holding a
|
||||
}
|
||||
```
|
||||
|
||||
## Common operations on linked lists
|
||||
## Common Linked List Operations
|
||||
|
||||
### Initializing a linked list
|
||||
### Initializing a Linked List
|
||||
|
||||
Constructing a linked list is a two-step process: first, initializing each node object, and second, forming the reference links between the nodes. After initialization, we can traverse all nodes sequentially from the head node by following the `next` reference.
|
||||
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)
|
||||
@@ -211,7 +231,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);
|
||||
@@ -228,7 +248,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);
|
||||
@@ -245,7 +265,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);
|
||||
@@ -262,7 +282,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)
|
||||
@@ -279,7 +299,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)
|
||||
@@ -296,7 +316,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);
|
||||
@@ -313,7 +333,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);
|
||||
@@ -330,7 +350,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);
|
||||
@@ -347,7 +367,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 }));
|
||||
@@ -365,7 +385,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);
|
||||
@@ -382,7 +402,35 @@ Constructing a linked list is a two-step process: first, initializing each node
|
||||
=== "Kotlin"
|
||||
|
||||
```kotlin title="linked_list.kt"
|
||||
/* Initialize linked list 1 -> 3 -> 2 -> 5 -> 4 */
|
||||
// Initialize each node
|
||||
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;
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
@@ -402,86 +450,90 @@ Constructing a linked list is a two-step process: first, initializing each node
|
||||
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`.
|
||||
??? pythontutor "Visualize code execution"
|
||||
|
||||
### Inserting nodes
|
||||
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%E5%88%9D%E5%A7%8B%E5%8C%96%E9%93%BE%E8%A1%A8%201%20-%3E%203%20-%3E%202%20-%3E%205%20-%3E%204%0A%20%20%20%20%23%20%E5%88%9D%E5%A7%8B%E5%8C%96%E5%90%84%E4%B8%AA%E8%8A%82%E7%82%B9%0A%20%20%20%20n0%20%3D%20ListNode%281%29%0A%20%20%20%20n1%20%3D%20ListNode%283%29%0A%20%20%20%20n2%20%3D%20ListNode%282%29%0A%20%20%20%20n3%20%3D%20ListNode%285%29%0A%20%20%20%20n4%20%3D%20ListNode%284%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%0A%20%20%20%20n0.next%20%3D%20n1%0A%20%20%20%20n1.next%20%3D%20n2%0A%20%20%20%20n2.next%20%3D%20n3%0A%20%20%20%20n3.next%20%3D%20n4&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=%5B%5D&textReferences=false
|
||||
|
||||
Inserting a node into a linked list is very easy. As shown in the figure below, 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)$.
|
||||
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`.
|
||||
|
||||
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.
|
||||
### Inserting a Node
|
||||
|
||||

|
||||
Inserting a node in a linked list is very easy. As shown in the figure below, 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.
|
||||
|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{insert}
|
||||
```
|
||||
|
||||
### Deleting nodes
|
||||
### Removing a Node
|
||||
|
||||
As shown in the figure below, deleting a node from a linked list is also very easy, **involving only the modification of a single node's reference (pointer)**.
|
||||
As shown in the figure below, removing a node in a linked list is also very convenient. **We only need to change one node's reference (pointer)**.
|
||||
|
||||
It's important to note that even though node `P` continues to point to `n1` after being deleted, it becomes inaccessible during linked list traversal. This effectively means that `P` is no longer a part of the linked list.
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{remove}
|
||||
```
|
||||
|
||||
### Accessing nodes
|
||||
### Accessing a Node
|
||||
|
||||
**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)$.
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{access}
|
||||
```
|
||||
|
||||
### Finding nodes
|
||||
### Finding a Node
|
||||
|
||||
Traverse the linked list to locate a node whose value matches `target`, and then output the index of that node within the linked list. This procedure is also an example of linear search. The corresponding code is provided below:
|
||||
Traverse the linked list to find a node with value `target`, and output the index of that node in the linked list. This process is also a linear search. The code is shown below:
|
||||
|
||||
```src
|
||||
[file]{linked_list}-[class]{}-[func]{find}
|
||||
```
|
||||
|
||||
## Arrays vs. linked lists
|
||||
## Arrays vs. Linked Lists
|
||||
|
||||
The table below 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.
|
||||
The table below 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.
|
||||
|
||||
<p align="center"> Table <id> Efficiency comparison of arrays and linked lists </p>
|
||||
<p align="center"> Table <id> Comparison of array and linked list efficiencies </p>
|
||||
|
||||
| | 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)$ |
|
||||
|
||||
## Common types of linked lists
|
||||
## Common Types of Linked Lists
|
||||
|
||||
As shown in the figure below, there are three common types of linked lists.
|
||||
As shown in the figure below, 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
|
||||
@@ -493,10 +545,10 @@ As shown in the figure below, 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
|
||||
}
|
||||
@@ -505,10 +557,10 @@ As shown in the figure below, 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
|
||||
}
|
||||
```
|
||||
@@ -516,14 +568,14 @@ As shown in the figure below, 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,
|
||||
@@ -536,10 +588,10 @@ As shown in the figure below, 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
|
||||
@@ -551,7 +603,7 @@ As shown in the figure below, 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
|
||||
@@ -564,7 +616,7 @@ As shown in the figure below, 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;
|
||||
@@ -580,11 +632,11 @@ As shown in the figure below, 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
|
||||
}
|
||||
```
|
||||
@@ -595,15 +647,15 @@ As shown in the figure below, 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<Rc<RefCell<ListNode>>>, // Pointer to successor node
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // Pointer to predecessor node
|
||||
next: Option<Rc<RefCell<ListNode>>>, // Pointer to the successor node
|
||||
prev: Option<Rc<RefCell<ListNode>>>, // Pointer to the predecessor node
|
||||
}
|
||||
|
||||
/* Constructors */
|
||||
/* Constructor */
|
||||
impl ListNode {
|
||||
fn new(val: i32) -> Self {
|
||||
ListNode {
|
||||
@@ -618,16 +670,16 @@ As shown in the figure below, 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;
|
||||
@@ -639,13 +691,36 @@ As shown in the figure below, 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
|
||||
}
|
||||
```
|
||||
|
||||
=== "Ruby"
|
||||
|
||||
```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
|
||||
|
||||
def initialize(val=0, next_node=nil, prev_node=nil)
|
||||
@val = val
|
||||
@next = next_node
|
||||
@prev = prev_node
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
=== "Zig"
|
||||
|
||||
```zig title=""
|
||||
// Bidirectional linked list node class
|
||||
// Doubly linked list node class
|
||||
pub fn ListNode(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
@@ -666,21 +741,21 @@ As shown in the figure below, there are three common types of linked lists.
|
||||
|
||||

|
||||
|
||||
## Typical applications of linked lists
|
||||
## 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.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,71 +1,71 @@
|
||||
# Memory and cache *
|
||||
# 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.
|
||||
|
||||
## Computer storage devices
|
||||
## Computer Storage Devices
|
||||
|
||||
There are three types of storage devices in computers: <u>hard disk</u>, <u>random-access memory (RAM)</u>, and <u>cache memory</u>. The following table shows their respective roles and performance characteristics in computer systems.
|
||||
Computers include three types of storage devices: <u>hard disk</u>, <u>random-access memory (RAM)</u>, and <u>cache memory</u>. The following table shows their different roles and performance characteristics in a computer system.
|
||||
|
||||
<p align="center"> Table <id> Computer storage devices </p>
|
||||
<p align="center"> Table <id> Computer Storage Devices </p>
|
||||
|
||||
| | 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 the figure below. 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.
|
||||
|
||||

|
||||

|
||||
|
||||
!!! 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 the figure below, 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.
|
||||
|
||||

|
||||

|
||||
|
||||
## Memory efficiency of data structures
|
||||
## 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.
|
||||
|
||||
## Cache efficiency of data structures
|
||||
## 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 <u>cache miss</u> 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 <u>cache miss</u> 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 <u>cache hit rate</u>, 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 <u>cache hit rate</u>, 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.
|
||||
|
||||
@@ -1,81 +1,86 @@
|
||||
# Summary
|
||||
|
||||
### Key review
|
||||
### Key Takeaways
|
||||
|
||||
- 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.
|
||||
|
||||
### 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.
|
||||
|
||||
Reference in New Issue
Block a user