当前位置:首页>python>Python 列表与元组专项练习:6 道编程题从入门到精通

Python 列表与元组专项练习:6 道编程题从入门到精通

  • 2026-10-11 07:29:32
Python 列表与元组专项练习:6 道编程题从入门到精通

配套专栏: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
 操作 O(1),比列表的 O(n) 快得多
append
末尾添加 O(1) 均摊

题目二:矩阵转置 ⭐⭐

📌 题目描述

编写函数 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)*
 解包将二维列表的每行作为独立参数传入 zip
map(list, ...)
将 zip 返回的元组转换为列表
嵌套列表推导式
外层生成列,内层遍历行

题目三:合并有序列表 ⭐⭐

📌 题目描述

给定两个已升序排列的列表,合并为一个新的升序列表。

输入:[1, 3, 5, 7], [2, 4, 6, 8]输出:[1, 2, 3, 4, 5, 6, 7, 8]

要求:时间复杂度 O(m+n),其中 m、n 分别为两个列表的长度。

💡 编程思路

这是经典的双指针归并问题,也是归并排序的核心步骤:

  1. 用两个指针 i、j 分别指向两个列表的头部。
  2. 每次比较 list1[i] 和 list2[j],将较小的放入结果,对应指针后移。
  3. 当某个列表遍历完后,将另一个列表的剩余部分直接追加。

如果面试官没有限制复杂度,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]

🔗 关联知识点

知识点
说明
双指针技巧
同时遍历两个列表的经典模式
append
 vs extend
append
 加一个元素,extend 加一个列表
切片追加
a[i:]
 取剩余部分,O(1) 创建新列表

题目四:滑动窗口最大值 ⭐⭐⭐

📌 题目描述

给定一个整数列表和一个窗口大小 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")

🔗 关联知识点

知识点
说明
切片 nums[i:i+k]
窗口的直观表示
deque
 双端队列
popleft()
 O(1),比 list.pop(0) 的 O(n) 快
单调队列思想
维护单调性,队首即为最优解

题目五:学生成绩排名系统 ⭐⭐⭐

📌 题目描述

使用 namedtuple 设计一个轻量级学生成绩排名系统,支持以下功能:

  1. 添加学生(姓名、语文、数学、英语三科成绩)
  2. 按总分降序排名
  3. 按指定科目排名
  4. 查询排名区间内的学生
# 示例添加: ("张三", 85, 92, 78)添加: ("李四", 92, 88, 95)添加: ("王五", 78, 95, 82)按总分排名: [("李四", 275), ("张三", 255), ("王五", 255)]按数学排名: [("王五", 95), ("张三", 92), ("李四", 88)]查询第2-3名: [("张三", 255), ("王五", 255)]

💡 编程思路

这道题综合考察 namedtuple、sorted 的 key 参数、切片、元组解包:

  1. 用 namedtuple('Student', 'name chinese math english') 定义数据结构,比字典更轻量、比类更简洁。
  2. 排名用 sorted(students, key=lambda s: s.chinese + s.math + s.english, reverse=True)。
  3. 按科目排名时,利用 getattr(s, subject) 动态获取属性,避免写一堆 if-else。
  4. 排名区间直接用切片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)
末尾添加元素
O(1) 均摊
insert(idx, val)
指定位置插入
O(n)
pop()
删除末尾元素
O(1)
remove(idx)
删除指定位置元素
O(n)
__getitem__(idx)
索引访问(支持负数)
O(1)
__len__()
返回长度
O(1)
__iter__()
支持遍历
O(n)
__repr__()
字符串表示
O(n)

💡 编程思路

这道题直击第4篇的底层原理 —— Python 列表的动态数组实现。核心机制:

  1. 底层存储
    :使用 Python 的 list 模拟一块连续内存(仅用 list.__setitem__ 和 list.__getitem__,不使用 append/insert/pop 等方法)。
  2. 容量管理
    :维护 size(元素个数)和 capacity(总容量)。当 size == capacity 时触发扩容(容量翻倍),模拟 CPython 的 ob_size / allocated 机制。
  3. 插入/删除
    :手动移动元素,体会为什么 insert(0, x) 是 O(n) —— 需要把后面所有元素往后挪一位。
  4. 负数索引
    :-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
 时翻倍,均摊 O(1)
元素手动移动
insert
 后移、remove 前移,理解 O(n) 来源
魔术方法
__getitem__
/__setitem__/__len__/__iter__/__repr__
负数索引映射
idx < 0
 时 idx += size
yield
 生成器
__iter__
 中使用 yield 实现惰性遍历

总结:知识点覆盖矩阵

题目
难度
核心考察点
去重保序
⭐
列表遍历、set 成员判断、append
矩阵转置
⭐⭐
zip
 解包、嵌套推导式、map
合并有序列表
⭐⭐
双指针、extend、切片
滑动窗口最大值
⭐⭐⭐
deque
、切片、单调队列
成绩排名系统
⭐⭐⭐
namedtuple
、sorted(key=)、getattr、切片
简易动态数组
⭐⭐⭐⭐
扩容机制、元素移动、魔术方法、生成器

建议:按顺序从第 1 题做到第 6 题,每道题先独立思考 10-15 分钟再参考答案。第 6 题是本篇的"压轴题",完整实现后你将真正理解 Python 列表的底层工作原理。


本文是《Python 全栈修炼之路》第 04 篇配套练习,欢迎点赞收藏!

最新文章

随机文章