返回首页返回书籍《Hello 算法》
目录

目录 · CONTENTS

  1. 01序
  2. 02前言
  3. 03关于本书
  4. 04如何使用本书
  5. 05小结
  6. 06初识算法
  7. 07算法无处不在
  8. 08算法是什么
  9. 09小结
  10. 10复杂度分析
  11. 11算法效率评估
  12. 12迭代与递归
  13. 13时间复杂度
  14. 14空间复杂度
  15. 15小结
  16. 16练习
  17. 17数据结构
  18. 18数据结构分类
  19. 19基本数据类型
  20. 20数字编码
  21. 21字符编码
  22. 22小结
  23. 23练习
  24. 24数组与链表
  25. 25数组
  26. 26链表
  27. 27列表
  28. 28内存与缓存
  29. 29小结
  30. 30练习
  31. 31栈与队列
  32. 32栈
  33. 33队列
  34. 34双向队列
  35. 35小结
  36. 36练习
  37. 37哈希表
  38. 38哈希表
  39. 39哈希冲突
  40. 40哈希算法
  41. 41小结
  42. 42练习
  43. 43树
  44. 44二叉树
  45. 45二叉树遍历
  46. 46二叉树数组表示
  47. 47二叉搜索树
  48. 48AVL 树
  49. 49小结
  50. 50练习
  51. 51堆
  52. 52堆
  53. 53建堆操作
  54. 54Top-k 问题
  55. 55小结
  56. 56练习
  57. 57图
  58. 58图
  59. 59图的基础操作
  60. 60图的遍历
  61. 61小结
  62. 62练习
  63. 63搜索
  64. 64二分查找
  65. 65二分查找插入点
  66. 66二分查找边界
  67. 67哈希优化策略
  68. 68重识搜索算法
  69. 69小结
  70. 70练习
  71. 71排序
  72. 72排序算法
  73. 73选择排序
  74. 74冒泡排序
  75. 75插入排序
  76. 76快速排序
  77. 77归并排序
  78. 78堆排序
  79. 79桶排序
  80. 80计数排序
  81. 81基数排序
  82. 82小结
  83. 83练习
  84. 84分治
  85. 85分治算法
  86. 86分治搜索策略
  87. 87构建二叉树问题
  88. 88汉诺塔问题
  89. 89小结
  90. 90练习
  91. 91回溯
  92. 92回溯算法
  93. 93全排列问题
  94. 94子集和问题
  95. 95n 皇后问题
  96. 96小结
  97. 97练习
  98. 98动态规划
  99. 99初探动态规划
  100. 100动态规划问题特性
  101. 101动态规划解题思路
  102. 1020-1 背包问题
  103. 103完全背包问题
  104. 104编辑距离问题
  105. 105小结
  106. 106练习
  107. 107贪心
  108. 108贪心算法
  109. 109分数背包问题
  110. 110最大容量问题
  111. 111最大切分乘积问题
  112. 112小结
  113. 113练习
  114. 114附录
  115. 115编程环境安装
  116. 116一起参与创作
  117. 117术语表
  118. 118参考文献
  119. 119纸质书
第 27 / 119 节

第 27 节 · SECTION 27

列表

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

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

当使用数组实现列表时,长度不可变的性质会导致列表的实用性降低。这是因为我们通常无法事先确定需要存储多少数据,从而难以选择合适的列表长度。若长度过小,则很可能无法满足使用需求;若长度过大,则会造成内存空间浪费。

为解决此问题,我们可以使用动态数组(dynamic array)来实现列表。它继承了数组的各项优点,并且可以在程序运行过程中进行动态扩容。

实际上,许多编程语言中的标准库提供的列表是基于动态数组实现的,例如 Python 中的 list 、Java 中的 ArrayList 、C++ 中的 vector 和 C# 中的 List 等。在接下来的讨论中,我们将把“列表”和“动态数组”视为等同的概念。

列表常用操作

初始化列表

我们通常使用“无初始值”和“有初始值”这两种初始化方法:

Python

# 初始化列表
# 无初始值
nums1: list[int] = []
# 有初始值
nums: list[int] = [1, 3, 2, 5, 4]

C++

/* 初始化列表 */
// 需注意,C++ 中 vector 即是本文描述的 nums
// 无初始值
vector<int> nums1;
// 有初始值
vector<int> nums = { 1, 3, 2, 5, 4 };

Java

/* 初始化列表 */
// 无初始值
List<Integer> nums1 = new ArrayList<>();
// 有初始值(注意数组的元素类型需为 int[] 的包装类 Integer[])
Integer[] numbers = new Integer[] { 1, 3, 2, 5, 4 };
List<Integer> nums = new ArrayList<>(Arrays.asList(numbers));

C#

/* 初始化列表 */
// 无初始值
List<int> nums1 = [];
// 有初始值
int[] numbers = [1, 3, 2, 5, 4];
List<int> nums = [.. numbers];

Go

/* 初始化列表 */
// 无初始值
nums1 := []int{}
// 有初始值
nums := []int{1, 3, 2, 5, 4}

Swift

/* 初始化列表 */
// 无初始值
let nums1: [Int] = []
// 有初始值
var nums = [1, 3, 2, 5, 4]

JS

/* 初始化列表 */
// 无初始值
const nums1 = [];
// 有初始值
const nums = [1, 3, 2, 5, 4];

TS

/* 初始化列表 */
// 无初始值
const nums1: number[] = [];
// 有初始值
const nums: number[] = [1, 3, 2, 5, 4];

Dart

/* 初始化列表 */
// 无初始值
List<int> nums1 = [];
// 有初始值
List<int> nums = [1, 3, 2, 5, 4];

Rust

/* 初始化列表 */
// 无初始值
let nums1: Vec<i32> = Vec::new();
// 有初始值
let nums: Vec<i32> = vec![1, 3, 2, 5, 4];

C

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

Kotlin

/* 初始化列表 */
// 无初始值
var nums1 = listOf<Int>()
// 有初始值
var numbers = arrayOf(1, 3, 2, 5, 4)
var nums = numbers.toMutableList()

Ruby

# 初始化列表
# 无初始值
nums1 = []
# 有初始值
nums = [1, 3, 2, 5, 4]

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 初始化列表 %23 无初始值 nums1 %3D [] %23 有初始值 nums %3D [1, 3, 2, 5, 4]&cumulative=false&curInstr=4&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

访问元素

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

Python

# 访问元素
num: int = nums[1]  # 访问索引 1 处的元素

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

C++

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

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

Java

/* 访问元素 */
int num = nums.get(1);  // 访问索引 1 处的元素

/* 更新元素 */
nums.set(1, 0);  // 将索引 1 处的元素更新为 0

C#

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

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

Go

/* 访问元素 */
num := nums[1]  // 访问索引 1 处的元素

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

Swift

/* 访问元素 */
let num = nums[1] // 访问索引 1 处的元素

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

JS

/* 访问元素 */
const num = nums[1];  // 访问索引 1 处的元素

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

TS

/* 访问元素 */
const num: number = nums[1];  // 访问索引 1 处的元素

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

Dart

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

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

Rust

/* 访问元素 */
let num: i32 = nums[1];  // 访问索引 1 处的元素
/* 更新元素 */
nums[1] = 0;             // 将索引 1 处的元素更新为 0

C

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

Kotlin

/* 访问元素 */
val num = nums[1]       // 访问索引 1 处的元素
/* 更新元素 */
nums[1] = 0             // 将索引 1 处的元素更新为 0

Ruby

# 访问元素
num = nums[1] # 访问索引 1 处的元素
# 更新元素
nums[1] = 0 # 将索引 1 处的元素更新为 0

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 初始化列表 nums %3D [1, 3, 2, 5, 4] %23 访问元素 num %3D nums[1] %23 访问索引 1 处的元素 %23 更新元素 nums[1] %3D 0 %23 将索引 1 处的元素更新为 0&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

插入与删除元素

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

Python

# 清空列表
nums.clear()

# 在尾部添加元素
nums.append(1)
nums.append(3)
nums.append(2)
nums.append(5)
nums.append(4)

# 在中间插入元素
nums.insert(3, 6)  # 在索引 3 处插入数字 6

# 删除元素
nums.pop(3)        # 删除索引 3 处的元素

C++

/* 清空列表 */
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 处的元素

Java

/* 清空列表 */
nums.clear();

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

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

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

C#

/* 清空列表 */
nums.Clear();

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

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

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

Go

/* 清空列表 */
nums = nil

/* 在尾部添加元素 */
nums = append(nums, 1)
nums = append(nums, 3)
nums = append(nums, 2)
nums = append(nums, 5)
nums = append(nums, 4)

/* 在中间插入元素 */
nums = append(nums[:3], append([]int{6}, nums[3:]...)...) // 在索引 3 处插入数字 6

/* 删除元素 */
nums = append(nums[:3], nums[4:]...) // 删除索引 3 处的元素

Swift

/* 清空列表 */
nums.removeAll()

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

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

/* 删除元素 */
nums.remove(at: 3) // 删除索引 3 处的元素

JS

/* 清空列表 */
nums.length = 0;

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

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

/* 删除元素 */
nums.splice(3, 1);  // 删除索引 3 处的元素

TS

/* 清空列表 */
nums.length = 0;

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

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

/* 删除元素 */
nums.splice(3, 1);  // 删除索引 3 处的元素

Dart

/* 清空列表 */
nums.clear();

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

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

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

Rust

/* 清空列表 */
nums.clear();

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

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

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

C

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

Kotlin

/* 清空列表 */
nums.clear();

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

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

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

Ruby

# 清空列表
nums.clear

# 在尾部添加元素
nums << 1
nums << 3
nums << 2
nums << 5
nums << 4

# 在中间插入元素
nums.insert(3, 6) # 在索引 3 处插入数字 6

# 删除元素
nums.delete_at(3) # 删除索引 3 处的元素

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 有初始值 nums %3D [1, 3, 2, 5, 4] %23 清空列表 nums.clear() %23 在尾部添加元素 nums.append(1) nums.append(3) nums.append(2) nums.append(5) nums.append(4) %23 在中间插入元素 nums.insert(3, 6) %23 在索引 3 处插入数字 6 %23 删除元素 nums.pop(3) %23 删除索引 3 处的元素&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

遍历列表

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

Python

# 通过索引遍历列表
count = 0
for i in range(len(nums)):
    count += nums[i]

# 直接遍历列表元素
for num in nums:
    count += num

C++

/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < nums.size(); i++) {
    count += nums[i];
}

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

Java

/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < nums.size(); i++) {
    count += nums.get(i);
}

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

C#

/* 通过索引遍历列表 */
int count = 0;
for (int i = 0; i < nums.Count; i++) {
    count += nums[i];
}

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

Go

/* 通过索引遍历列表 */
count := 0
for i := 0; i < len(nums); i++ {
    count += nums[i]
}

/* 直接遍历列表元素 */
count = 0
for _, num := range nums {
    count += num
}

Swift

/* 通过索引遍历列表 */
var count = 0
for i in nums.indices {
    count += nums[i]
}

/* 直接遍历列表元素 */
count = 0
for num in nums {
    count += num
}

JS

/* 通过索引遍历列表 */
let count = 0;
for (let i = 0; i < nums.length; i++) {
    count += nums[i];
}

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

TS

/* 通过索引遍历列表 */
let count = 0;
for (let i = 0; i < nums.length; i++) {
    count += nums[i];
}

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

Dart

/* 通过索引遍历列表 */
int count = 0;
for (var i = 0; i < nums.length; i++) {
    count += nums[i];
}

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

Rust

// 通过索引遍历列表
let mut _count = 0;
for i in 0..nums.len() {
    _count += nums[i];
}

// 直接遍历列表元素
_count = 0;
for num in &nums {
    _count += num;
}

C

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

Kotlin

/* 通过索引遍历列表 */
var count = 0
for (i in nums.indices) {
    count += nums[i]
}

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

Ruby

# 通过索引遍历列表
count = 0
for i in 0...nums.length
    count += nums[i]
end

# 直接遍历列表元素
count = 0
for num in nums
    count += num
end

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 初始化列表 nums %3D [1, 3, 2, 5, 4] %23 通过索引遍历列表 count %3D 0 for i in range(len(nums))%3A count %2B%3D nums[i] %23 直接遍历列表元素 for num in nums%3A count %2B%3D num&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

拼接列表

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

Python

# 拼接两个列表
nums1: list[int] = [6, 8, 7, 10, 9]
nums += nums1  # 将列表 nums1 拼接到 nums 之后

C++

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

Java

/* 拼接两个列表 */
List<Integer> nums1 = new ArrayList<>(Arrays.asList(new Integer[] { 6, 8, 7, 10, 9 }));
nums.addAll(nums1);  // 将列表 nums1 拼接到 nums 之后

C#

/* 拼接两个列表 */
List<int> nums1 = [6, 8, 7, 10, 9];
nums.AddRange(nums1);  // 将列表 nums1 拼接到 nums 之后

Go

/* 拼接两个列表 */
nums1 := []int{6, 8, 7, 10, 9}
nums = append(nums, nums1...)  // 将列表 nums1 拼接到 nums 之后

Swift

/* 拼接两个列表 */
let nums1 = [6, 8, 7, 10, 9]
nums.append(contentsOf: nums1) // 将列表 nums1 拼接到 nums 之后

JS

/* 拼接两个列表 */
const nums1 = [6, 8, 7, 10, 9];
nums.push(...nums1);  // 将列表 nums1 拼接到 nums 之后

TS

/* 拼接两个列表 */
const nums1: number[] = [6, 8, 7, 10, 9];
nums.push(...nums1);  // 将列表 nums1 拼接到 nums 之后

Dart

/* 拼接两个列表 */
List<int> nums1 = [6, 8, 7, 10, 9];
nums.addAll(nums1);  // 将列表 nums1 拼接到 nums 之后

Rust

/* 拼接两个列表 */
let nums1: Vec<i32> = vec![6, 8, 7, 10, 9];
nums.extend(nums1);

C

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

Kotlin

/* 拼接两个列表 */
val nums1 = intArrayOf(6, 8, 7, 10, 9).toMutableList()
nums.addAll(nums1)  // 将列表 nums1 拼接到 nums 之后

Ruby

# 拼接两个列表
nums1 = [6, 8, 7, 10, 9]
nums += nums1

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 初始化列表 nums %3D [1, 3, 2, 5, 4] %23 拼接两个列表 nums1 %3D [6, 8, 7, 10, 9] nums %2B%3D nums1 %23 将列表 nums1 拼接到 nums 之后&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

排序列表

完成列表排序后,我们便可以使用在数组类算法题中经常考查的“二分查找”和“双指针”算法。

Python

# 排序列表
nums.sort()  # 排序后,列表元素从小到大排列

C++

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

Java

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

C#

/* 排序列表 */
nums.Sort(); // 排序后,列表元素从小到大排列

Go

/* 排序列表 */
sort.Ints(nums)  // 排序后,列表元素从小到大排列

Swift

/* 排序列表 */
nums.sort() // 排序后,列表元素从小到大排列

JS

/* 排序列表 */
nums.sort((a, b) => a - b);  // 排序后,列表元素从小到大排列

TS

/* 排序列表 */
nums.sort((a, b) => a - b);  // 排序后,列表元素从小到大排列

Dart

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

Rust

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

C

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

Kotlin

/* 排序列表 */
nums.sort() // 排序后,列表元素从小到大排列

Ruby

# 排序列表
nums = nums.sort { |a, b| a <=> b } # 排序后,列表元素从小到大排列

可视化运行

https://pythontutor.com/render.html#code="""Driver Code""" if __name__ %3D%3D "__main__"%3A %23 初始化列表 nums %3D [1, 3, 2, 5, 4] %23 排序列表 nums.sort() %23 排序后,列表元素从小到大排列&cumulative=false&curInstr=3&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=311&rawInputLstJSON=[]&textReferences=false

列表实现

许多编程语言内置了列表,例如 Java、C++、Python 等。它们的实现比较复杂,各个参数的设定也非常考究,例如初始容量、扩容倍数等。感兴趣的读者可以查阅源码进行学习。

为了加深对列表工作原理的理解,我们尝试实现一个简易版列表,包括以下三个重点设计。

  • 初始容量:选取一个合理的数组初始容量。在本示例中,我们选择 10 作为初始容量。
  • 数量记录:声明一个变量 size ,用于记录列表当前元素数量,并随着元素插入和删除实时更新。根据此变量,我们可以定位列表尾部,以及判断是否需要扩容。
  • 扩容机制:若插入元素时列表容量已满,则需要进行扩容。先根据扩容倍数创建一个更大的数组,再将当前数组的所有元素依次移动至新数组。在本示例中,我们规定每次将数组扩容至之前的 2 倍。
[file]{my_list}-[class]{my_list}-[func]{}