配套专栏:Python 全栈修炼之路 第 4 篇《列表与元组 —— 有序集合的双子星》
难度分布:⭐ → ⭐⭐ → ⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐ → ⭐⭐⭐⭐
核心覆盖:切片、推导式、排序、元组解包、namedtuple、浅拷贝陷阱、动态数组原理
题目一:列表去重保序 ⭐
📌 题目描述
编写函数 unique_preserve_order(lst),去除列表中的重复元素,保持首次出现的顺序。
输入:[3, 1, 2, 3, 4, 2, 5, 1]输出:[3, 1, 2, 4, 5]
限制:不使用 set() 直接转换(因为 set 无法保证顺序)。
💡 编程思路
这道题考察的是列表遍历 + 成员判断。核心逻辑很简单 —— 依次遍历列表,遇到没见过的元素就收集起来。关键在于如何高效判断"是否见过":
- 方法一:用辅助列表 +
in 判断。每次 in 都是 O(n),整体 O(n²)。 - 方法二:用辅助
set 记录已见元素,in 操作降为 O(1),整体 O(n)。
方法二利用了集合的 O(1) 查找特性,是面试中最推荐的写法。
🖥️ 参考代码
def unique_preserve_order(lst): """去重并保持首次出现顺序 —— O(n) 解法""" seen = set() result = [] for item in lst: if item not in seen: seen.add(item) result.append(item) return result# 测试if __name__ == "__main__": print(unique_preserve_order([3, 1, 2, 3, 4, 2, 5, 1])) # [3, 1, 2, 4, 5] # 边界测试 print(unique_preserve_order([])) # [] print(unique_preserve_order([1, 1, 1])) # [1] print(unique_preserve_order(["a", "b", "a"])) # ['a', 'b']
🔗 关联知识点
| |
|---|
| for item in lst |
set | in |
append | |
题目二:矩阵转置 ⭐⭐
📌 题目描述
编写函数 transpose(matrix),将一个二维矩阵(列表的列表)进行转置:行变列,列变行。
输入:[[1, 2, 3], [4, 5, 6]]输出:[[1, 4], [2, 5], [3, 6]]
💡 编程思路
矩阵转置的本质:第 i 行第 j 列的元素 → 第 j 行第 i 列。
- 方法一(嵌套循环):外层遍历列索引,内层遍历行索引,逐个取出元素构建新行。直观但稍显冗长。
- 方法二(列表推导式):
[[row[i] for row in matrix] for i in range(len(matrix[0]))],一行搞定,Pythonic。 - 方法三(
zip 解包):list(map(list, zip(*matrix))),利用 * 解包 + zip 配对,最简洁。
三种方法都应掌握,面试中方法三最能体现 Python 功底。
🖥️ 参考代码
def transpose_loop(matrix): """方法一:嵌套循环""" if not matrix: return [] rows, cols = len(matrix), len(matrix[0]) return [[matrix[r][c] for r in range(rows)] for c in range(cols)]def transpose_zip(matrix): """方法二:zip + 解包(最 Pythonic)""" return list(map(list, zip(*matrix)))# 测试if __name__ == "__main__": matrix = [[1, 2, 3], [4, 5, 6]] print(transpose_loop(matrix)) # [[1, 4], [2, 5], [3, 6]] print(transpose_zip(matrix)) # [[1, 4], [2, 5], [3, 6]] # 非方阵测试 print(transpose_zip([[1, 2, 3, 4]])) # [[1], [2], [3], [4]] # 空矩阵 print(transpose_zip([])) # []
🔗 关联知识点
| |
|---|
zip(*matrix) | * |
map(list, ...) | |
| |
题目三:合并有序列表 ⭐⭐
📌 题目描述
给定两个已升序排列的列表,合并为一个新的升序列表。
输入:[1, 3, 5, 7], [2, 4, 6, 8]输出:[1, 2, 3, 4, 5, 6, 7, 8]
要求:时间复杂度 O(m+n),其中 m、n 分别为两个列表的长度。
💡 编程思路
这是经典的双指针归并问题,也是归并排序的核心步骤:
- 每次比较
list1[i] 和 list2[j],将较小的放入结果,对应指针后移。 - 当某个列表遍历完后,将另一个列表的剩余部分直接追加。
如果面试官没有限制复杂度,sorted(a + b) 一行搞定,但时间复杂度为 O((m+n) log(m+n)),不如双指针优。
🖥️ 参考代码
def merge_sorted(a, b): """双指针归并 —— O(m+n)""" i, j = 0, 0 result = [] while i < len(a) and j < len(b): if a[i] <= b[j]: result.append(a[i]) i += 1 else: result.append(b[j]) j += 1 # 追加剩余部分 result.extend(a[i:]) result.extend(b[j:]) return result# 测试if __name__ == "__main__": print(merge_sorted([1, 3, 5, 7], [2, 4, 6, 8])) # [1, 2, 3, 4, 5, 6, 7, 8] # 有重复元素 print(merge_sorted([1, 2, 4], [2, 3, 5])) # [1, 2, 2, 3, 4, 5] # 空列表 print(merge_sorted([], [1, 2, 3])) # [1, 2, 3] # 长度不等 print(merge_sorted([1, 5], [2, 3, 4, 6])) # [1, 2, 3, 4, 5, 6]
🔗 关联知识点
题目四:滑动窗口最大值 ⭐⭐⭐
📌 题目描述
给定一个整数列表和一个窗口大小 k,返回每个窗口中的最大值列表。
输入:nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3输出:[3, 3, 5, 5, 6, 7]解释:窗口位置 最大值[1 3 -1] -3 5 3 6 7 3 1 [3 -1 -3] 5 3 6 7 3 1 3 [-1 -3 5] 3 6 7 5 1 3 -1 [-3 5 3] 6 7 5 1 3 -1 -3 [5 3 6] 7 6 1 3 -1 -3 5 [3 6 7] 7
💡 编程思路
这道题是 LeetCode 239 的经典题目。有两种思路:
- 方法一(暴力):对每个窗口切片,用
max() 求最大值。简单但 O(n·k),窗口大时性能差。 - 方法二(双端队列):维护一个单调递减的双端队列,队列中存储元素的索引。队首始终是当前窗口的最大值索引。当窗口滑动时,移除超出窗口的索引,移除所有比新元素小的索引。时间复杂度 O(n)。
方法二虽然代码稍长,但体现了对双端队列和单调性的理解,是面试的高分写法。
🖥️ 参考代码
from collections import dequedef max_sliding_window_brute(nums, k): """方法一:暴力切片 —— O(n·k)""" return [max(nums[i:i + k]) for i in range(len(nums) - k + 1)]def max_sliding_window_deque(nums, k): """方法二:单调双端队列 —— O(n)""" if not nums or k <= 0: return [] dq = deque() # 存索引,保持对应值单调递减 result = [] for i, num in enumerate(nums): # 1. 移除超出窗口的索引 while dq and dq[0] < i - k + 1: dq.popleft() # 2. 移除所有比当前元素小的索引(它们不可能成为最大值) while dq and nums[dq[-1]] < num: dq.pop() # 3. 当前索引入队 dq.append(i) # 4. 窗口形成后,记录最大值(队首) if i >= k - 1: result.append(nums[dq[0]]) return result# 测试if __name__ == "__main__": nums = [1, 3, -1, -3, 5, 3, 6, 7] k = 3 print(max_sliding_window_brute(nums, k)) # [3, 3, 5, 5, 6, 7] print(max_sliding_window_deque(nums, k)) # [3, 3, 5, 5, 6, 7] # 性能对比 import timeit big_nums = list(range(10000)) t1 = timeit.timeit(lambda: max_sliding_window_brute(big_nums, 500), number=10) t2 = timeit.timeit(lambda: max_sliding_window_deque(big_nums, 500), number=10) print(f"\n暴力法: {t1:.4f}s") print(f"双端队列: {t2:.4f}s")
🔗 关联知识点
| |
|---|
| |
deque | popleft() O(1),比 list.pop(0) 的 O(n) 快 |
| |
题目五:学生成绩排名系统 ⭐⭐⭐
📌 题目描述
使用 namedtuple 设计一个轻量级学生成绩排名系统,支持以下功能:
# 示例添加: ("张三", 85, 92, 78)添加: ("李四", 92, 88, 95)添加: ("王五", 78, 95, 82)按总分排名: [("李四", 275), ("张三", 255), ("王五", 255)]按数学排名: [("王五", 95), ("张三", 92), ("李四", 88)]查询第2-3名: [("张三", 255), ("王五", 255)]
💡 编程思路
这道题综合考察 namedtuple、sorted 的 key 参数、切片、元组解包:
- 用
namedtuple('Student', 'name chinese math english') 定义数据结构,比字典更轻量、比类更简洁。 - 排名用
sorted(students, key=lambda s: s.chinese + s.math + s.english, reverse=True)。 - 按科目排名时,利用
getattr(s, subject) 动态获取属性,避免写一堆 if-else。 - 排名区间直接用切片
ranked[start-1:end]。
🖥️ 参考代码
from collections import namedtupleStudent = namedtuple('Student', 'name chinese math english')class GradeRanker: """基于 namedtuple 的成绩排名系统""" def __init__(self): self.students = [] def add(self, name, chinese, math, english): self.students.append(Student(name, chinese, math, english)) @staticmethod def _total(s): return s.chinese + s.math + s.english def rank_by_total(self): """按总分降序排名,返回 (学生, 总分) 列表""" return sorted( [(s, self._total(s)) for s in self.students], key=lambda x: x[1], reverse=True ) def rank_by_subject(self, subject): """按指定科目降序排名""" return sorted( [(s, getattr(s, subject)) for s in self.students], key=lambda x: x[1], reverse=True ) def query_range(self, start, end): """查询第 start 名到第 end 名(闭区间)""" ranked = self.rank_by_total() return ranked[start - 1: end] def print_ranking(self): """格式化打印排名""" ranked = self.rank_by_total() print(f"{'排名':<6}{'姓名':<10}{'语文':<8}{'数学':<8}{'英语':<8}{'总分':<8}") print("-" * 48) for i, (s, total) in enumerate(ranked, 1): print(f"{i:<6}{s.name:<10}{s.chinese:<8}{s.math:<8}{s.english:<8}{total:<8}")# 测试if __name__ == "__main__": ranker = GradeRanker() ranker.add("张三", 85, 92, 78) ranker.add("李四", 92, 88, 95) ranker.add("王五", 78, 95, 82) ranker.add("赵六", 88, 76, 90) ranker.add("孙七", 95, 82, 88) print("=== 按总分排名 ===") ranker.print_ranking() print("\n=== 按数学排名 ===") for s, score in ranker.rank_by_subject('math'): print(f" {s.name}: {score}") print("\n=== 第2-4名 ===") for s, total in ranker.query_range(2, 4): print(f" {s.name}: {total}")
🔗 关联知识点
| |
|---|
namedtuple | |
getattr(s, field) | |
sorted(key=, reverse=) | |
| ranked[start-1:end] |
题目六:实现一个简易动态数组 ⭐⭐⭐⭐
📌 题目描述
不使用 Python 内置的 list,仅用底层类型实现一个简易的动态数组MyList,支持以下操作:
| | |
|---|
append(val) | | |
insert(idx, val) | | |
pop() | | |
remove(idx) | | |
__getitem__(idx) | | |
__len__() | | |
__iter__() | | |
__repr__() | | |
💡 编程思路
这道题直击第4篇的底层原理 —— Python 列表的动态数组实现。核心机制:
- 底层存储:使用 Python 的
list 模拟一块连续内存(仅用 list.__setitem__ 和 list.__getitem__,不使用 append/insert/pop 等方法)。 - 容量管理:维护
size(元素个数)和 capacity(总容量)。当 size == capacity 时触发扩容(容量翻倍),模拟 CPython 的 ob_size / allocated 机制。 - 插入/删除:手动移动元素,体会为什么
insert(0, x) 是 O(n) —— 需要把后面所有元素往后挪一位。 - 负数索引:
-1 映射为 size - 1,与 Python 内置行为一致。
说明:由于 Python 没有真正的指针和连续内存操作,我们用固定大小的 list + 手动管理 size 来模拟。在 C 语言中,这部分对应 malloc / realloc + 元素移动。
🖥️ 参考代码
class MyList: """简易动态数组 —— 模拟 Python list 的底层实现""" def __init__(self, initial_capacity=4): self._data = [None] * initial_capacity # 底层"连续内存" self._size = 0 # 当前元素个数 self._capacity = initial_capacity # 总容量 # ---------- 内部方法 ---------- def _resize(self, new_capacity): """扩容:分配更大的空间,复制旧数据""" new_data = [None] * new_capacity for i in range(self._size): new_data[i] = self._data[i] self._data = new_data self._capacity = new_capacity def _check_index(self, idx): """索引合法性检查,支持负数索引""" if idx < 0: idx += self._size if idx < 0 or idx >= self._size: raise IndexError(f"MyList index out of range: {idx}") return idx # ---------- 公开接口 ---------- def append(self, val): """末尾添加 —— O(1) 均摊""" if self._size == self._capacity: self._resize(self._capacity * 2) # 扩容为 2 倍 self._data[self._size] = val self._size += 1 def insert(self, idx, val): """指定位置插入 —— O(n)""" if idx < 0: idx += self._size if idx < 0: idx = 0 if idx > self._size: idx = self._size if self._size == self._capacity: self._resize(self._capacity * 2) # 手动后移元素 for i in range(self._size, idx, -1): self._data[i] = self._data[i - 1] self._data[idx] = val self._size += 1 def pop(self): """删除末尾元素 —— O(1)""" if self._size == 0: raise IndexError("pop from empty MyList") val = self._data[self._size - 1] self._data[self._size - 1] = None # 释放引用 self._size -= 1 return val def remove_at(self, idx): """删除指定位置元素 —— O(n)""" idx = self._check_index(idx) val = self._data[idx] # 手动前移元素 for i in range(idx, self._size - 1): self._data[i] = self._data[i + 1] self._data[self._size - 1] = None self._size -= 1 return val def __getitem__(self, idx): """索引访问 —— O(1)""" idx = self._check_index(idx) return self._data[idx] def __setitem__(self, idx, val): """索引赋值 —— O(1)""" idx = self._check_index(idx) self._data[idx] = val def __len__(self): return self._size def __iter__(self): for i in range(self._size): yield self._data[i] def __repr__(self): items = ", ".join(repr(self._data[i]) for i in range(self._size)) return f"MyList([{items}])" @property def capacity(self): """当前容量(调试用)""" return self._capacity# 测试if __name__ == "__main__": ml = MyList() # append 测试 for i in range(10): ml.append(i) print(ml) # MyList([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) print(f"size={len(ml)}, capacity={ml.capacity}") # size=10, capacity=16 # insert 测试 ml.insert(0, 100) ml.insert(5, 200) print(ml) # MyList([100, 0, 1, 2, 3, 200, 4, 5, 6, 7, 8, 9]) # 负数索引 print(ml[-1]) # 9 print(ml[-2]) # 8 # pop 测试 print(ml.pop()) # 9 print(ml.pop()) # 8 # remove_at 测试 ml.remove_at(0) # 删除 100 print(ml) # 遍历测试 for item in ml: print(item, end=" ") print() # 性能验证:append 均摊 O(1) import timeit t = timeit.timeit("ml.append(1)", setup="from __main__ import MyList; ml=MyList()", number=100000) print(f"\n10万次 append: {t:.4f}s")
🔗 关联知识点
| |
|---|
| size == capacity |
| insert |
| __getitem__/__setitem__/__len__/__iter__/__repr__ |
| idx < 0 |
yield | __iter__ |
总结:知识点覆盖矩阵
建议:按顺序从第 1 题做到第 6 题,每道题先独立思考 10-15 分钟再参考答案。第 6 题是本篇的"压轴题",完整实现后你将真正理解 Python 列表的底层工作原理。
本文是《Python 全栈修炼之路》第 04 篇配套练习,欢迎点赞收藏!