列表(list)是一个抽象的数据结构概念,表示元素的有序集合,支持元素访问、修改、添加、删除和遍历等操作,无需使用者考虑容量限制的问题。列表可以基于链表或数组实现。

  • 链表天然可以看作一个列表,其支持元素增删改查操作,并且可以灵活动态扩容。
  • 数组也支持元素增删改查,但由于其长度不可变,因此只能看作一个具有长度限制的列表。

由于数组不具备扩展长度的能力,因此可以使用动态数组来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

许多编程语言中的标准库提供的列表是基于动态数组实现的,例如Python中的list、Java中的ArrayList、C++中的vector和C#中的List等。通常,将列表动态数组视为等同的概念。

1. 列表常用操作

1.1 初始化列表

列表的初始化可以采用“无初始值”和“有初始值”这两种初始化方法:

1
2
3
4
5
/* 初始化列表 */
// 无初始值
vector<int> nums1;
// 有初始值
vector<int> nums = { 1, 3, 2, 5, 4 };

1.2 访问元素

列表本质上是数组,因此可以在$O(1)$时间内访问和更新元素。

1
2
3
4
5
/* 访问元素 */
int num = nums[1]; // 访问索引 1 处的元素

/* 更新元素 */
nums[1] = 0; // 将索引 1 处的元素更新为 0

1.3 插入与删除元素

在列表末尾添加元素的时间复杂度为$O(1)$,但插入和删除元素的效率仍与数组相同,时间复杂度为$O(n)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 清空列表 */
nums.clear();

/* 在尾部添加元素 */
nums.push_back(1);
nums.push_back(3);
nums.push_back(2);
nums.push_back(5);
nums.push_back(4);

/* 在中间插入元素 */
nums.insert(nums.begin() + 3, 6); // 在索引 3 处插入数字 6

/* 删除元素 */
nums.erase(nums.begin() + 3); // 删除索引 3 处的元素

1.4 遍历列表

与数组一样,列表可以根据索引遍历,也可以直接遍历各元素。

1
2
3
4
5
6
7
8
9
10
11
/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < nums.size(); i++) {
count += nums[i];
}

/* 直接遍历列表元素 */
count = 0;
for (int num : nums) {
count += num;
}

1.5 拼接列表

给定一个新列表nums1,我们可以将其拼接到原列表的尾部。

1
2
3
4
/* 拼接两个列表 */
vector<int> nums1 = { 6, 8, 7, 10, 9 };
// 将列表 nums1 拼接到 nums 之后
nums.insert(nums.end(), nums1.begin(), nums1.end());

1.6 排序列表

1
2
/* 排序列表 */
sort(nums.begin(), nums.end()); // 排序后,列表元素从小到大排列

2. 简单列表的实现

由于Python和C++都内置了列表,直接使用内置的列表listvector即可,因此下面仅提供C实现列表的方法。

列表的设计需要考虑下面三个因素:

  • 初始容量:选取一个合理的数组初始容量。下面的示例中选择10作为初始容量。
  • 数量记录:声明一个变量size,用于记录列表当前元素数量,并随着元素插入和删除实时更新。根据此变量,我们可以定位列表尾部,以及判断是否需要扩容。
  • 扩容机制:若插入元素时列表容量已满,则需要进行扩容。先根据扩容倍数创建一个更大的数组,再将当前数组的所有元素依次移动至新数组。在下面的示例中,规定每次将数组扩容至之前的2倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/* 列表类 */
typedef struct {
int *arr; // 数组(存储列表元素)
int capacity; // 列表容量
int size; // 列表大小
int extendRatio; // 列表每次扩容的倍数
} MyList;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

参考

[1] GitHub 开源项目《hello 算法》
[2] 程杰.大话数据结构【溢彩加强版】[M].清华大学出版社,2020.