Data structures: arrays and linked lists

Table of contents

1 array

1.1 Common operations on arrays

1. Initialize the array

2. Access elements

3. Insert elements

4. Delete elements

5. Traverse the array

6. Find elements

7. Expand the array

1.2 Advantages and limitations of arrays

1.3 Typical applications of arrays

2 linked list

2.1 Common operations on linked lists

1. Initialize the linked list

2. Insert node

3. Delete node

4. Access nodes

5. Find nodes

2.2 Array VS linked list

2.3 Common linked list types

2.4 Typical applications of linked lists

3 list

3.1 Common operations on lists

1. Initialization list

2. Access elements

3. Inserting and deleting elements

4. Traverse the list

5. Splicing list

6. Sorted list

3.2 List implementation

4 Summary

1. Key review

2.   Q & A


1 array

"Array" is a linear data structure that stores elements of the same type in continuous memory space. We call the position of an element in the array the "index" of the element. Figure 4-1 illustrates the main terms and concepts of arrays.

Array definition and storage

Figure 4-1 Array definition and storage method

1.1 Common operations on arrays

1. Initialize the array

We can choose two initialization methods for arrays according to our needs: no initial value and given initial value. Most programming languages ​​initialize array elements to 0 when no initial value is specified.

int arr[5] = { 0 }; // { 0, 0, 0, 0, 0 }
int nums[5] = { 1, 3, 2, 5, 4 };

2. Access elements

Array elements are stored in contiguous memory space, which means that calculating the memory address of an array element is very easy. Given the memory address of the array (that is, the memory address of the first element) and the index of an element, we can use the formula shown in Figure 4-2 to calculate the memory address of the element, thereby directly accessing the element.

Memory address calculation of array elements

Figure 4-2 Memory address calculation of array elements

Looking at Figure 4-2, we see that the first element of the array has index 0, which seems counterintuitive because it would be more natural to start counting from 1. But from the perspective of the address calculation formula, the meaning of the index is essentially the offset of the memory address . The address offset of the first element is 0 , so it makes sense that its index is 0.

Accessing elements in an array is very efficient. We can randomly access any element in the array in O(1).

/* 随机访问元素 */
int randomAccess(int *nums, int size) {
    // 在区间 [0, size) 中随机抽取一个数字
    int randomIndex = rand() % size;
    // 获取并返回随机元素
    int randomNum = nums[randomIndex];
    return randomNum;
}

3. Insert elements

Array elements are "next to each other" in memory, and there is no space between them to store any data. As shown in Figure 4-3, if you want to insert an element in the middle of the array, you need to move all the elements after the element backward by one, and then assign the element to the index.

Example of inserting elements into array

Figure 4-3 Example of inserting elements into an array

It is worth noting that since the length of the array is fixed, inserting an element will definitely cause the "loss" of the tail element of the array. We leave the solution to this problem to the list chapter.

/* 在数组的索引 index 处插入元素 num */
void insert(int *nums, int size, int num, int index) {
    // 把索引 index 以及之后的所有元素向后移动一位
    for (int i = size - 1; i > index; i--) {
        nums[i] = nums[i - 1];
    }
    // 将 num 赋给 index 处元素
    nums[index] = num;
}

4. Delete elements

In the same way, as shown in Figure 4-4, if you want to delete the element at index i, you need to move the elements after index i forward one position.

Array deletion element example

Figure 4-4 Example of deleting elements from an array

Please note that after the element is deleted, the original element at the end becomes "meaningless", so we do not need to modify it specifically.

/* 删除索引 index 处元素 */
// 注意:stdio.h 占用了 remove 关键词
void removeItem(int *nums, int size, int index) {
    // 把索引 index 之后的所有元素向前移动一位
    for (int i = index; i < size - 1; i++) {
        nums[i] = nums[i + 1];
    }
}

In general, array insertion and deletion operations have the following disadvantages.

  • High time complexity : The average time complexity of array insertion and deletion is O(n), where n is the array length.
  • Missing elements : Since the length of an array is immutable, elements beyond the array length are lost after inserting elements.
  • Memory waste : We can initialize a relatively long array and only use the front part, so that when inserting data, the lost last elements are "meaningless", but this will also cause a waste of some memory space.

5. Traverse the array

In most programming languages, we can either traverse an array by index or directly traverse to obtain each element in the array.

/* 遍历数组 */
void traverse(int *nums, int size) {
    int count = 0;
    // 通过索引遍历数组
    for (int i = 0; i < size; i++) {
        count++;
    }
}

6. Find elements

To find a specified element in an array, you need to traverse the array. In each round, it is judged whether the element value matches, and if it matches, the corresponding index is output.

Because arrays are linear data structures, the above search operation is called "linear search".

/* 在数组中查找指定元素 */
int find(int *nums, int size, int target) {
    for (int i = 0; i < size; i++) {
        if (nums[i] == target)
            return i;
    }
    return -1;
}

7. Expand the array

In a complex system environment, it is difficult for the program to ensure that the memory space after the array is available, making it impossible to safely expand the array capacity. So in most programming languages, the length of an array is immutable .

If we want to expand the array, we need to re-create a larger array, and then copy the elements of the original array to the new array in sequence. This is an O(n) operation, which is very time-consuming when the array is large.

/* 扩展数组长度 */
int *extend(int *nums, int size, int enlarge) {
    // 初始化一个扩展长度后的数组
    int *res = (int *)malloc(sizeof(int) * (size + enlarge));
    // 将原数组中的所有元素复制到新数组
    for (int i = 0; i < size; i++) {
        res[i] = nums[i];
    }
    // 初始化扩展后的空间
    for (int i = size; i < size + enlarge; i++) {
        res[i] = 0;
    }
    // 返回扩展后的新数组
    return res;
}

1.2 Advantages and limitations of arrays

Arrays are stored in contiguous memory space and have the same element type. This approach contains rich a priori information that the system can use to optimize the operational efficiency of the data structure.

  • High space efficiency : Arrays allocate contiguous memory blocks for data without additional structural overhead.
  • Supports random access : Arrays allow access to any element in O(1) time.
  • Cache locality : When an array element is accessed, the computer not only loads it but also caches other data around it, thereby using the cache to speed up subsequent operations.

Continuous space storage is a double-edged sword, which has the following disadvantages.

  • Insertion and deletion are inefficient : When there are many elements in the array, insertion and deletion operations require moving a large number of elements.
  • Immutable length : The length of the array is fixed after initialization. Expanding the array requires copying all data to a new array, which is very expensive.
  • Waste of space : If the size of the array allocated is larger than what is actually required, then the extra space is wasted.

1.3 Typical applications of arrays

Array is a basic and common data structure. It is frequently used in various algorithms and can also be used to implement various complex data structures.

  • Random access : If we want to randomly select some samples, we can store them in an array, generate a random sequence, and randomly select samples based on the index.
  • Sorting and Searching : Arrays are the most commonly used data structures for sorting and searching algorithms. Quick sort, merge sort, binary search, etc. are all mainly performed on arrays.
  • Lookup table : When we need to quickly find an element or need to find the correspondence between an element, we can use an array as a lookup table. If we want to map characters to ASCII codes, we can use the ASCII code value of the character as an index, and the corresponding elements are stored in the corresponding positions in the array.
  • Machine learning : Neural networks make extensive use of linear algebra operations between vectors, matrices, and tensors, and these data are all 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 data structures such as stacks, queues, hash tables, heaps, and graphs. For example, the adjacency matrix representation of a graph is actually a two-dimensional array.

2 linked list

Memory space is a common resource for all programs. In a complex system operating environment, free memory space may be scattered throughout the memory. We know that the memory space for storing arrays must be continuous, and when the array is very large, the memory may not be able to provide such a large continuous space. At this time, the flexibility advantage of the linked list is reflected.

"Linked list" is a linear data structure in which each element is a node object, and each node is connected through "references". The reference records the memory address of the next node through which the next node can be accessed from the current node.

The design of the linked list allows each node to be stored dispersedly throughout the memory, and their memory addresses do not need to be consecutive.

Linked list definition and storage method

Figure 4-5 Linked list definition and storage method

Observe Figure 4-5, the unit of the linked list is the "node node" object. Each node contains two pieces of data: the node's "value" and a "reference" to the next node.

  • The first node of the linked list is called the "head node" and the last node is called the "tail node".
  • The tail node points to "null", which is recorded as null, nullptr and None in Java, C++ and Python respectively.
  • In languages ​​that support pointers, such as C, C++, Go and Rust, the above "reference" should be replaced by "pointer".

ListNode As shown in the following code, in addition to containing the value, the linked list node  also needs to save an additional reference (pointer). Therefore, for the same amount of data, a linked list takes up more memory space than an array .

/* 链表节点结构体 */
struct ListNode {
    int val;               // 节点值
    struct ListNode *next; // 指向下一节点的指针
};

typedef struct ListNode ListNode;

/* 构造函数 */
ListNode *newListNode(int val) {
    ListNode *node, *next;
    node = (ListNode *) malloc(sizeof(ListNode));
    node->val = val;
    node->next = NULL;
    return node;
}

2.1 Common operations on linked lists

1. Initialize the linked list

There are two steps to establish a linked list. The first step is to initialize each node object, and the second step is to build a reference pointing relationship. After the initialization is completed, we can start from the head node of the linked list and  next access all nodes in sequence through reference points.

/* 初始化链表 1 -> 3 -> 2 -> 5 -> 4 */
// 初始化各个节点
ListNode* n0 = newListNode(1);
ListNode* n1 = newListNode(3);
ListNode* n2 = newListNode(2);
ListNode* n3 = newListNode(5);
ListNode* n4 = newListNode(4);
// 构建引用指向
n0->next = n1;
n1->next = n2;
n2->next = n3;
n3->next = n4;

The array as a whole is a variable, for example, the array  nums contains elements  nums[0] and  nums[1] etc., while the linked list is composed of multiple independent node objects. We usually use the head node as a representative name for a linked list . For example, the linked list in the above code can be recorded as a linked list  n0 .

2. Insert node

Inserting nodes into a linked list is very easy.  As shown in Figure 4-6, assuming we want to insert a new node between  two adjacent nodes  n0 and   , we only need to change the two node references (pointers) , and the time complexity is O(1).n1P

In contrast, the time complexity of inserting elements into an array is O(n), which is less efficient when dealing with large amounts of data.

Example of inserting node into linked list

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

/* 在链表的节点 n0 之后插入节点 P */
void insert(ListNode *n0, ListNode *P) {
    ListNode *n1 = n0->next;
    P->next = n1;
    n0->next = P;
}

3. Delete node

As shown in Figure 4-7, it is also very convenient to delete nodes in the linked list. You only need to change the reference (pointer) of a node .

P Please note that although the node is still pointing  after the deletion operation is completed  n1 , it is actually inaccessible by traversing the linked list  P , which means that  P it no longer belongs to the linked list.

Delete node from linked list

Figure 4-7 Delete node from linked list

/* 删除链表的节点 n0 之后的首个节点 */
// 注意:stdio.h 占用了 remove 关键词
void removeNode(ListNode *n0) {
    if (!n0->next)
        return;
    // n0 -> P -> n1
    ListNode *P = n0->next;
    ListNode *n1 = P->next;
    n0->next = n1;
    // 释放内存
    free(P);
}

4. Access nodes

Accessing nodes in a linked list is inefficient . As mentioned in the previous section, we can access any element in the 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. In other words, accessing the i-th node of the linked list requires i-1 rounds of loops, and the time complexity is O(n).

/* 访问链表中索引为 index 的节点 */
ListNode *access(ListNode *head, int index) {
    while (head && head->next && index) {
        head = head->next;
        index--;
    }
    return head;
}

5. Find nodes

Traverse the linked list, find  target the node with value in the linked list, and output the index of the node in the linked list. This process is also a linear search.

/* 在链表中查找值为 target 的首个节点 */
int find(ListNode *head, int target) {
    int index = 0;
    while (head) {
        if (head->val == target)
            return index;
        head = head->next;
        index++;
    }
    return -1;
}

2.2 Array VS linked list

Table 4-1 summarizes and compares the characteristics and operation efficiency of arrays and linked lists. Since they adopt two opposite storage strategies, their various properties and operating efficiencies also present opposing characteristics.

Table 4-1 Efficiency comparison between arrays and linked lists

array linked list
Storage method contiguous memory space Spread memory space
cache locality friendly unfriendly
Capacity expansion Immutable length Flexible to expand
memory efficiency Occupies little memory and wastes some space Taking up a lot of memory
access element O(1) O(n)
Add element O(n) O(1)
Delete element O(n) O(1)

2.3 Common linked list types

As shown in Figure 4-8, there are three common types of linked lists.

  • One-way linked list : the ordinary linked list introduced above. The nodes of a one-way linked list contain two pieces of data: a value and a reference to the next node. We call the first node the head node, the last node the tail node, and the tail node points to None.
  • Circular linked list : If we make the tail node of the one-way linked list point to the head node (that is, connected end to end), we will get a circular linked list. In a circular linked list, any node can be regarded as the head node.
  • Doubly linked list : Compared with singly linked list, doubly linked list records references in both directions. The node definition of a doubly linked list contains references (pointers) to both the successor node (next node) and the predecessor node (previous node). Compared with one-way linked lists, doubly linked lists are more flexible and can traverse the linked list in both directions, but they also require more memory space.

/* 双向链表节点结构体 */
struct ListNode {
    int val;               // 节点值
    struct ListNode *next; // 指向后继节点的指针
    struct ListNode *prev; // 指向前驱节点的指针
};

typedef struct ListNode ListNode;

/* 构造函数 */
ListNode *newListNode(int val) {
    ListNode *node, *next;
    node = (ListNode *) malloc(sizeof(ListNode));
    node->val = val;
    node->next = NULL;
    node->prev = NULL;
    return node;
}

Common types of linked lists

Figure 4-8 Common types of linked lists

2.4 Typical applications of linked lists

One-way linked lists are commonly used to implement data structures such as stacks, queues, hash tables, and graphs.

  • Stack and queue : When the insertion and deletion operations are performed at one end of the linked list, it exhibits the first-in-last-out characteristic, corresponding to the stack; when the insertion operation is performed at one end of the linked list, and the deletion operation is performed at the other end of the linked list, it exhibits The first-in-first-out feature corresponds to the queue.
  • Hash table : The chain address method is one of the mainstream solutions to resolve hash conflicts. In this solution, all conflicting elements will be placed in a linked list.
  • Graph : An adjacency list is a common way to represent a graph, in which each vertex of the graph is associated with a linked list, and each element of the linked list represents other vertices connected to that vertex.

Doubly linked lists are often used in scenarios where the previous and next elements need to be quickly found.

  • Advanced data structures : For example, in red-black trees and B-trees, we need to access the parent node of a node. This can be achieved by saving a reference to the parent node in the node, similar to a doubly linked list.
  • Browser history : In a web browser, when a user clicks the forward or back button, the browser needs to know the previous and next web pages the user has visited. The characteristics of doubly linked lists make this operation easy.
  • LRU algorithm : In the cache eviction algorithm (LRU), we need to quickly find the least recently used data and support the rapid addition and removal of nodes. At this time, it is very appropriate to use a doubly linked list.

Circular linked lists are often used in scenarios that require periodic operations, such as operating system resource scheduling.

  • Time slice round robin scheduling algorithm : In the operating system, the time slice round robin scheduling algorithm is a common CPU scheduling algorithm, which needs to cycle through a group of processes. Each process is given a time slice, and when the time slice runs out, the CPU switches to the next process. This cyclic operation can be achieved through a circular linked list.
  • Data buffer : In some implementations of data buffers, 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 put into a circular linked list to achieve seamless playback.

3 list

Immutable array length reduces practicality . In practice, we may not be able to determine in advance how much data needs to be stored, which makes the choice of array length difficult. If the length is too small, the array needs to be frequently expanded when data is continuously added; if the length is too large, memory space will be wasted.

To solve this problem, a data structure called "dynamic array" emerged, which is an array of variable length, also often called "list". Lists are implemented based on arrays, inherit the advantages of arrays, and can be dynamically expanded while the program is running. We can freely add elements to the list without worrying about exceeding the capacity limit.

3.1 Common operations on lists

1. Initialization list

We usually use two initialization methods: "no initial value" and "with initial value".

// C 未提供内置动态数组

2. Access elements

Lists are essentially arrays, so elements can be accessed and updated in O(1) time, which is very efficient.

// C 未提供内置动态数组

3. Inserting and deleting elements

Compared to arrays, lists can add and remove elements freely. The time complexity of adding elements to the end of the list is �(1), but the efficiency of inserting and deleting elements is still the same as that of the array, and the time complexity is O(n).

// C 未提供内置动态数组

4. Traverse the list

Like arrays, lists can be traversed based on index, or individual elements can be traversed directly.

// C 未提供内置动态数组

5. Splicing list

Given a new list  list1 , we can concatenate the list to the end of the original list.

// C 未提供内置动态数组

6. Sorted list

After sorting the list, we can use the "binary search" and "double pointer" algorithms that are often examined in array algorithm problems.

// C 未提供内置动态数组

3.2 List implementation

Many programming languages ​​provide built-in lists, such as Java, C++, Python, etc. Their implementation is relatively complex, and the setting of each parameter is also very sophisticated, such as initial capacity, expansion multiple, etc. Interested readers can check the source code to learn.

In order to deepen our understanding of how lists work, we try to implement a simplified version of the list, including the following three key designs.

  • Initial capacity : Choose a reasonable initial capacity of the array. In this example, we choose 10 as the initial capacity.
  • Quantity record : Declare a variable  size to record the current number of elements in the list, and update it in real time as elements are inserted and deleted. Based on this variable, we can locate the end of the list and determine whether expansion is needed.
  • Expansion mechanism : If the list capacity is full when elements are inserted, expansion is required. First create a larger array based on the expansion multiple, and then move all elements of the current array to the new array in sequence. In this example, we stipulate that the array will be expanded to 2 times the previous size each time.

/* 列表类简易实现 */
struct myList {
    int *nums;       // 数组(存储列表元素)
    int capacity;    // 列表容量
    int size;        // 列表大小
    int extendRatio; // 列表每次扩容的倍数
};

typedef struct myList myList;

/* 构造函数 */
myList *newMyList() {
    myList *list = malloc(sizeof(myList));
    list->capacity = 10;
    list->nums = malloc(sizeof(int) * list->capacity);
    list->size = 0;
    list->extendRatio = 2;
    return list;
}

/* 析构函数 */
void delMyList(myList *list) {
    free(list->nums);
    free(list);
}

/* 获取列表长度 */
int size(myList *list) {
    return list->size;
}

/* 获取列表容量 */
int capacity(myList *list) {
    return list->capacity;
}

/* 访问元素 */
int get(myList *list, int index) {
    assert(index >= 0 && index < list->size);
    return list->nums[index];
}

/* 更新元素 */
void set(myList *list, int index, int num) {
    assert(index >= 0 && index < list->size);
    list->nums[index] = num;
}

/* 尾部添加元素 */
void add(myList *list, int num) {
    if (size(list) == capacity(list)) {
        extendCapacity(list); // 扩容
    }
    list->nums[size(list)] = num;
    list->size++;
}

/* 中间插入元素 */
void insert(myList *list, int index, int num) {
    assert(index >= 0 && index < size(list));
    // 元素数量超出容量时,触发扩容机制
    if (size(list) == capacity(list)) {
        extendCapacity(list); // 扩容
    }
    for (int i = size(list); i > index; --i) {
        list->nums[i] = list->nums[i - 1];
    }
    list->nums[index] = num;
    list->size++;
}

/* 删除元素 */
// 注意:stdio.h 占用了 remove 关键词
int removeNum(myList *list, int index) {
    assert(index >= 0 && index < size(list));
    int num = list->nums[index];
    for (int i = index; i < size(list) - 1; i++) {
        list->nums[i] = list->nums[i + 1];
    }
    list->size--;
    return num;
}

/* 列表扩容 */
void extendCapacity(myList *list) {
    // 先分配空间
    int newCapacity = capacity(list) * list->extendRatio;
    int *extend = (int *)malloc(sizeof(int) * newCapacity);
    int *temp = list->nums;

    // 拷贝旧数据到新数据
    for (int i = 0; i < size(list); i++)
        extend[i] = list->nums[i];

    // 释放旧数据
    free(temp);

    // 更新新数据
    list->nums = extend;
    list->capacity = newCapacity;
}

/* 将列表转换为 Array 用于打印 */
int *toArray(myList *list) {
    return list->nums;
}

4 Summary

1. Key review

  • Arrays and linked lists are two basic data structures, which respectively represent two ways of storing data in computer memory: continuous space storage and dispersed space storage. The characteristics of the two show complementary characteristics.
  • Arrays support random access and occupy less memory; however, inserting and deleting elements is inefficient, and the length is immutable after initialization.
  • Linked lists achieve efficient node insertion and deletion by changing references (pointers), and the length can be flexibly adjusted; however, node access efficiency is low and takes up a lot of memory. Common types of linked lists include singly linked lists, circular linked lists, and doubly linked lists.
  • Dynamic array, also known as list, is a data structure based on array implementation. It retains the advantages of arrays while allowing flexible adjustment of length. The emergence of lists greatly improves the ease of use of arrays, but it may lead to a waste of some memory space.

2.   Q & A

Does array storage on the stack and storage on the heap have any impact on time efficiency and space efficiency?

Arrays stored on the stack and on the heap are stored in continuous memory space, and the data operation efficiency is basically the same. However, stacks and heaps have their own characteristics, resulting in the following differences.

  1. Allocation and release efficiency: The stack is a smaller piece of memory, and the allocation is automatically completed by the compiler; while the heap memory is relatively larger, can be dynamically allocated in the code, and is easier to fragment. Therefore, allocation and deallocation operations on the heap are generally slower than those on the stack.
  2. Size limit: Stack memory is relatively small, and the size of the heap is generally limited by available memory. Therefore, the heap is more suitable for storing large arrays.
  3. Flexibility: The size of the array on the stack needs to be determined at compile time, while the size of the array on the heap can be determined dynamically at runtime.

Why do arrays require elements of the same type, but the same type is not emphasized in linked lists?

A linked list is composed of nodes, which are connected through references (pointers). 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, so that the position of the corresponding element can be obtained by calculating the offset. For example, if the array contains both int and long types, and a single element occupies 4 bytes and 8 bytes respectively, then the following formula cannot be used to calculate the offset at this time, because the array contains elements of two lengths.

# 元素内存地址 = 数组内存地址 + 元素长度 * 元素索引

After deleting the node, do I need to set it  P.next to None?

It’s okay not to modify it  P.next . From the perspective of the linked list, traversing from the beginning node to the end node has not been encountered  P . This means that the node  P has been deleted from the linked list. At this time,  P wherever the node points, it will not affect the linked list.

From a garbage collection perspective, for languages ​​with automatic garbage collection such as Java, Python, Go, etc.,  P whether a node is recycled depends on whether there is still a reference pointing to it, rather than  P.next its value. In languages ​​like C and C++, we need to release node memory manually.

The time complexity of insertion and deletion operations in a linked list is O(1). But before adding or deleting, it takes O(n) to find elements, so why is the time complexity not O(n)?

If you search for elements first and then delete elements, it is indeed O(n). However, the advantages of O(1) additions and deletions of linked lists can be reflected in other applications. For example, a two-way queue is suitable to be implemented using a linked list. We maintain a pointer variable that always points to the head node and tail node. Each insertion and deletion operation is O(1).

In the picture "Linked List Definition and Storage Method", does the light blue storage node pointer occupy a memory address? Or should it be half and half with the node value?

The schematic diagram in the article is only a qualitative representation, and the quantitative representation needs to be analyzed according to the specific situation.

  • Different types of node values ​​occupy different spaces, such as int, long, double and instance objects.
  • The size of the memory space occupied by pointer variables depends on the operating system and compilation environment used, and is mostly 8 bytes or 4 bytes.

Is adding an element to the end of a list always O(1)?

If the length of the list is exceeded when adding elements, the list needs to be expanded before adding. The system will apply for a new memory and move all the elements of the original list there. At this time, the time complexity will be O(n).

"The emergence of lists has greatly improved the practicality of arrays, but the side effect is that some memory space will be wasted." Does the waste of space here refer to the memory occupied by additional variables such as capacity, length, and expansion multiples?

The waste of space here mainly has two meanings: on the one hand, the list will set an initial length, and we do not necessarily need to use so much. On the other hand, in order to prevent frequent expansion, expansion is generally multiplied by a coefficient, such as ×1.5. This also leaves a lot of empty spots, and we often can't fill them completely.

After initialization in Python  n = [1, 2, 3] , the addresses of these three elements are connected, but the initialization  m = [2, 1, 3] will find that the id of each of their elements is not continuous, but the same as  n in. The addresses of these elements are not consecutive, so  m is it still an array?

If the list elements are replaced by linked list nodes  n = [n1, n2, n3, n4, n5] , usually these five node objects are also scattered and stored throughout the memory. However, given a list index, we can still obtain the node memory address and access the corresponding node in O(1) time. This is because the array stores references to nodes, not the nodes themselves.

Unlike many languages, numbers in Python are also wrapped as objects, and what is stored in the list is not the number itself, but a reference to the number. Therefore, we will find that the same numbers in the two arrays have the same id, and the memory addresses of these numbers do not need to be consecutive.

std::list in C++ STL has implemented a doubly linked list, but it seems that some algorithm books do not use this directly. Are there any limitations?

On the one hand, we tend to prefer using arrays to implement algorithms and using linked lists only when necessary, for two main reasons.

  • Space overhead: Since each element requires two extra pointers (one for the previous element and one for the next element), it  std::list usually  std::vector takes up more space than .
  • Cache-unfriendly: Since the data is not stored continuously, std::list the cache utilization is low. In general, std::vector the performance will be better.

On the other hand, the main cases where linked lists are necessary are binary trees and graphs. Stacks and queues tend to use  stack sums  provided by programming languages queue ​​rather than linked lists.

Guess you like

Origin blog.csdn.net/timberman666/article/details/133499921