VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > 数据结构 >
  • 第3章 数组与列表

第3章 数组与列表
3.1 引言
数组(Array)是计算机科学中最基础的线性数据结构,其核心思想是通过连续内存空间存储相同类型元素,实现对数据的高效组织与访问。从早期的机器语言到现代编程语言,数组始终是数据处理的基石——操作系统的内存管理、数据库的索引结构、图像的像素存储等底层实现,均依赖数组的高效随机访问特性。
Python中的列表(List)虽常被视为“数组的扩展”,但本质上是动态数组(Dynamic Array)的实现:它突破了传统数组“固定大小”的限制,通过自动扩容机制支持动态元素增减,同时兼容不同类型元素的存储。理解数组的底层原理与Python列表的实现逻辑,是掌握数据结构设计思想的第一步,也是优化代码性能的关键。本章将从数组的基本概念出发,深入剖析Python列表的动态扩容机制,手动实现动态数组,并通过稀疏矩阵案例展示其工程应用。
3.2 数组的基本概念
3.2.1 数组的定义与核心特性
定义3.1 数组:由相同数据类型的元素组成的有序集合,元素在内存中连续存储,通过索引(Index) 唯一标识元素位置。
数组的核心特性源于其连续内存存储方式,具体表现为:

顺序存储:元素在内存中按逻辑顺序连续排列,相邻元素的物理地址差值等于单个元素的大小(如int型元素占4字节,索引i与i+1的地址差为4字节)。

随机访问:通过索引可直接计算元素的内存地址,访问时间复杂度为O(1)。设数组首地址为base,元素大小为size,则索引i的元素地址为:
address(i)=base+i×size ext{address}(i) = ext{base} + i imes ext{size} address(i)=base+i×size

固定大小:传统数组(如C语言数组)在创建时需指定长度,且运行时不可修改。若需调整大小,需手动申请新内存并复制元素,时间复杂度为O(n)。

3.2.2 数组的内存存储模型
以一个包含5个int型元素的数组[10, 20, 30, 40, 50]为例(假设int型占4字节,首地址为0x1000),其内存布局如下:
索引i 元素值 内存地址计算 实际地址(示例)

0	10	0x1000 + 0×4	0x1000
1	20	0x1000 + 1×4	0x1004
2	30	0x1000 + 2×4	0x1008
3	40	0x1000 + 3×4	0x100C
4	50	0x1000 + 4×4	0x1010

这种连续存储模型使得数组能通过简单的地址计算实现随机访问,这是数组区别于链表等其他线性结构的核心优势。
3.2.3 数组的优缺点
优点:
访问高效:随机访问时间复杂度O(1),是所有数据结构中最快的。
空间紧凑:无需额外存储指针或引用(如链表的节点指针),空间利用率接近100%。
缓存友好:连续内存存储符合计算机缓存机制(局部性原理),频繁访问相邻元素时缓存命中率高。
缺点:
大小固定:创建时需确定长度,扩容需O(n)时间(复制元素),灵活性差。
插入/删除低效:在中间或头部插入/删除元素时,需移动后续所有元素,时间复杂度O(n)。例如,在索引1插入元素需移动索引1及之后的所有元素(平均移动n/2个元素)。
3.2.4 不同语言中的数组
静态数组(如C/C++、Java):需显式声明长度和元素类型,大小固定,不支持动态扩容。例如,int arr[5];(C语言)创建长度为5的int数组,越界访问会导致内存错误。
动态数组(如Python列表、Java ArrayList):自动管理容量,支持动态扩容,元素类型可灵活(Python)或受限(Java需指定泛型)。
多维数组:逻辑上为“数组的数组”,物理存储可采用行优先(C语言)或列优先(Fortran)方式,本质仍是连续内存块。
3.3 Python列表的实现原理
Python列表(List)并非传统意义上的数组(因其支持不同类型元素),但底层基于动态数组实现,通过自动扩容机制突破固定大小限制,同时保留数组的随机访问特性。理解列表的内部实现,是编写高效Python代码的基础。
3.3.1 动态数组的核心概念
动态数组在静态数组基础上增加了容量(Capacity) 管理机制,核心属性包括:
大小(Size):当前存储的元素数量(len(list)返回的值)。
容量(Capacity):当前内存空间可容纳的最大元素数量(用户不可见,由解释器管理)。
元素区(Items):指向连续内存空间的指针,存储元素的引用(Python中所有元素均为对象,列表存储的是对象引用而非值)。
核心思想:当size == capacity时,自动申请更大的内存空间,复制元素后释放旧空间,从而实现“动态大小”的效果。
3.3.2 动态扩容机制
Python列表的扩容策略是其性能优化的关键,具体步骤如下:

触发条件:调用append()添加元素后,若size == capacity,触发扩容。

新容量计算:Python采用1.5倍扩容策略(早期为2倍,后改为1.5倍以减少空间浪费)。设原容量为cap,新容量为:
1.new_cap=⌊cap×1.5⌋ ext{new_cap} = lfloor ext{cap} imes 1.5 floor new_cap=⌊cap×1.5⌋
例如,容量4→6→9→13→19...(1.5倍向下取整)。

内存申请与复制:

o申请可容纳new_cap个元素引用的连续内存;
o将原元素区的size个引用复制到新内存(耗时O(n),但均摊到每次append操作后为O(1));
o更新元素区指针指向新内存,释放旧内存。
示例:初始容量为4的列表扩容过程
python

	lst = [] # size=0, capacity=4, items=[None, None, None, None] 
	lst.append(1) # size=1, capacity=4 
	lst.append(2) # size=2, capacity=4 
	lst.append(3) # size=3, capacity=4 
	lst.append(4) # size=4, capacity=4 → 触发扩容,new_cap=6 
	# 复制元素:items变为[1, 2, 3, 4, None, None],size=4, capacity=6 
	lst.append(5) # size=5, capacity=6(无需扩容)

3.3.3 列表操作的时间复杂度分析
列表的核心操作性能取决于是否涉及元素移动或内存复制,具体时间复杂度如下:
操作 语法示例 时间复杂度 说明
索引访问 lst[i] O(1) 直接通过地址计算访问,与元素数量无关。
尾部添加 lst.append(x) O(1)均摊 无扩容时O(1);扩容时O(n),但均摊到n次操作后每次为O(1)。
指定位置插入 lst.insert(k, x) O(n) 需移动k及之后的所有元素(平均移动n/2个元素),时间与元素数量成正比。
尾部删除 lst.pop() O(1) 直接减少size,无需移动元素。
指定位置删除 lst.pop(k) O(n) 需移动k之后的所有元素(平均移动n/2个元素)。
成员检查 x in lst O(n) 线性扫描所有元素,最坏情况需遍历全部元素。
3.3.4 列表与静态数组的关键区别

特性 静态数组(如C语言) Python列表(动态数组)
元素类型 必须相同(如int、float) 可不同(存储对象引用,支持混合类型)
大小管理 固定,创建时指定 动态,自动扩容/缩容(需手动触发缩容)
内存存储 存储元素值 存储对象引用(指针)
扩容方式 手动实现(realloc+memcpy) 自动触发,1.5倍扩容策略

3.4 动态数组的设计与实现
为深入理解动态数组的工作原理,本节手动实现一个简化版动态数组(DynamicArray),模拟Python列表的核心功能,包括初始化、添加、插入、删除及动态扩容/缩容。
3.4.1 核心功能设计
目标:实现支持以下操作的动态数组:
init(self, capacity=4):初始化数组,指定初始容量(默认4)。
append(self, x):尾部添加元素,自动扩容。
insert(self, k, x):在索引k处插入元素,自动扩容。
pop(self, k=None):删除索引k处元素(默认尾部),支持缩容。
getitem(self, k):通过索引k访问元素(支持arr[k]语法)。
len(self):返回元素数量(支持len(arr)语法)。
3.4.2 完整实现代码
python

	import ctypes # 用于调用C语言风格的内存分配函数(模拟底层内存操作) 
	
	class DynamicArray: 
	def __init__(self, capacity=4): 
	self.size = 0 # 当前元素数量 
	self.capacity = capacity # 当前容量(至少为1) 
	if self.capacity < 1: 
	self.capacity = 4 
	# 申请容量为capacity的连续内存,存储Python对象引用(ctypes.py_object对应对象指针) 
	self.items = (ctypes.py_object * self.capacity)() 
	
	def append(self, x): 
	"""尾部添加元素,自动扩容""" 
	if self.size == self.capacity: 
	self._resize(self.capacity * 2) # 简化为2倍扩容(实际Python为1.5倍) 
	self.items[self.size] = x 
	self.size += 1 
	
	def _resize(self, new_capacity): 
	"""扩容/缩容:申请新内存,复制元素,释放旧内存""" 
	if new_capacity < self.size: 
	raise ValueError("新容量小于当前元素数量") 
	# 1. 申请新内存 
	new_items = (ctypes.py_object * new_capacity)() 
	# 2. 复制元素(仅复制已存储的size个元素) 
	for i in range(self.size): 
	new_items[i] = self.items[i] 
	# 3. 更新指针与容量 
	self.items = new_items 
	self.capacity = new_capacity 
	
	def insert(self, k, x): 
	"""在索引k处插入元素,k范围:0 ≤ k ≤ size""" 
	if k < 0 or k > self.size: 
	raise IndexError("插入索引越界") 
	# 容量不足时扩容 
	if self.size == self.capacity: 
	self._resize(self.capacity * 2) 
	# 移动k及之后的元素(从后往前移,避免覆盖) 
	for i in range(self.size, k, -1): 
	self.items[i] = self.items[i-1] 
	self.items[k] = x 
	self.size += 1 
	
	def pop(self, k=None): 
	"""删除索引k处元素,返回被删除值,默认删除尾部(k=size-1)""" 
	if self.size == 0: 
	raise IndexError("无法删除空数组元素") 
	if k is None: 
	k = self.size - 1 # 默认删除尾部 
	if k < 0 or k >= self.size: 
	raise IndexError("删除索引越界") 
	# 记录被删除值 
	x = self.items[k] 
	# 移动k之后的元素(从前往后移) 
	for i in range(k, self.size - 1): 
	self.items[i] = self.items[i+1] 
	self.size -= 1 
	# 缩容:当size < capacity//4时,缩容至capacity//2(节省空间) 
	if self.size < self.capacity // 4 and self.capacity > 4: 
	self._resize(self.capacity // 2) 
	return x 
	
	def __getitem__(self, k): 
	"""支持arr[k]语法访问元素""" 
	if k < 0 or k >= self.size: 
	raise IndexError("访问索引越界") 
	return self.items[k] 
	
	def __len__(self): 
	"""支持len(arr)语法返回元素数量""" 
	return self.size 
	
	def __str__(self): 
	"""格式化输出数组内容""" 
	return "[" + ", ".join(map(str, [self.items[i] for i in range(self.size)])) + "]" 

3.4.3 实现解析

1. **内存管理**:通过`ctypes.py_object * capacity`申请连续内存块,存储Python对象引用(模拟Python列表的底层实现)。 
2. **扩容策略**:为简化实现,采用2倍扩容(实际Python为1.5倍),需注意扩容时新容量不得小于当前元素数量。 
3. **插入/删除元素**- **插入**:从后往前移动元素,避免覆盖待移动元素(如在索引1插入时,需先移动索引3→4、2→3、1→2,再插入新元素到索引1)。 
- **删除**:从前往后移动元素,覆盖被删除元素,减少size后更新内存。 
4. **缩容机制**:当元素数量小于容量的1/4时,缩容至原容量的1/2(如容量16、size=3时缩容至8),避免空间浪费(Python列表不会主动缩容,需通过`lst = lst[:]`触发)。 

3.4.4 使用示例

python

	# 创建动态数组 
	arr = DynamicArray() 
	print(arr) # 输出:[](初始size=0) 
	
	# 尾部添加 
	arr.append(1) 
	arr.append(2) 
	arr.append(3) 
	print(arr) # 输出:[1, 2, 3](size=3, capacity=4) 
	
	# 插入元素 
	arr.insert(1, 10) # 在索引1插入10 
	print(arr) # 输出:[1, 10, 2, 3](size=4, capacity=4 → 插入后size=5,触发扩容至8) 
	
	# 删除元素 
	arr.pop(1) # 删除索引1的元素(10) 
	print(arr) # 输出:[1, 2, 3](size=3, capacity=8 → 因size=3 < 8//4=2不成立,不缩容) 
	
	# 访问元素 
	print(arr[2]) # 输出:3(通过索引访问)

3.5 实战:基于列表的稀疏矩阵实现
3.5.1 问题背景
在科学计算、图像处理等领域,稀疏矩阵(矩阵中大部分元素为0)是常见数据结构。例如,1000×1000的社交网络邻接矩阵中,非0元素(表示用户间的连接)可能仅占0.1%,若用普通二维列表存储([[0]*1000 for _ in range(1000)]),需存储10⁶个元素,其中99.9%为0,空间浪费严重。
解决方案:采用三元组列表法存储稀疏矩阵,仅记录非0元素的(行索引, 列索引, 值),显著节省空间并提升操作效率。
3.5.2 稀疏矩阵的存储策略
三元组列表法:用列表存储所有非0元素的三元组(i, j, val),其中i为行索引,j为列索引,val为元素值,同时记录矩阵的总行数(rows)和总列数(cols)。例如,矩阵:
[003400050] egin{bmatrix} 0 & 0 & 3 4 & 0 & 0 0 & 5 & 0 end{bmatrix} ​040​005​300​​
可表示为rows=3, cols=3, data=[(0,2,3), (1,0,4), (2,1,5)]。
3.5.3 稀疏矩阵类的实现
python

	class SparseMatrix: 
	def __init__(self, rows, cols): 
	self.rows = rows # 矩阵行数 
	self.cols = cols # 矩阵列数 
	self.data = [] # 存储非0元素:[(i, j, val), ...],按行优先排序(非必须,便于查找) 
	
	def set(self, i, j, val): 
	"""设置(i,j)位置元素值,val=0时删除该元素""" 
	if not (0 <= i < self.rows and 0 <= j < self.cols): 
	raise IndexError("矩阵索引越界") 
	# 查找是否已存在(i,j)元素(遍历data列表,O(k),k为非0元素数) 
	for idx in range(len(self.data)): 
	x, y, _ = self.data[idx] 
	if x == i and y == j: 
	if val == 0: 
	del self.data[idx] # 值为0时删除该三元组 
	else: 
	self.data[idx] = (i, j, val) # 更新值 
	return 
	# 新元素且val≠0时添加三元组 
	if val != 0: 
	self.data.append((i, j, val)) 
	# 可选:保持data按行/列索引排序,便于后续二分查找优化 
	self.data.sort(key=lambda x: (x[0], x[1])) 
	
	def get(self, i, j): 
	"""获取(i,j)位置元素值,默认返回0""" 
	# 线性查找(O(k)),可优化为二分查找(需保证data有序) 
	for x, y, val in self.data: 
	if x == i and y == j: 
	return val 
	return 0 
	
	def add(self, other): 
	"""矩阵加法:返回self + other的结果矩阵""" 
	if self.rows != other.rows or self.cols != other.cols: 
	raise ValueError("矩阵尺寸不匹配(行数或列数不同)") 
	result = SparseMatrix(self.rows, self.cols) 
	# 添加self的所有非0元素 
	for i, j, val in self.data: 
	result.set(i, j, val) 
	# 添加other的所有非0元素,重叠位置自动求和 
	for i, j, val in other.data: 
	result.set(i, j, result.get(i, j) + val) 
	return result 
	
	def __str__(self): 
	"""格式化输出矩阵(仅用于小矩阵可视化)""" 
	# 构建全0矩阵,填充非0元素 
	matrix = [[0]*self.cols for _ in range(self.rows)] 
	for i, j, val in self.data: 
	matrix[i][j] = val 
	return "
".join([" ".join(map(str, row)) for row in matrix]) 

3.5.4 性能分析与应用

**空间复杂度**:仅存储非0元素,空间复杂度为O(k)(k为非0元素数),远优于普通矩阵的O(rows×cols)。例如,1000×1000矩阵若k=1000,空间占用仅为普通矩阵的0.1%。 

**时间复杂度**- `set`/`get`操作:线性查找时O(k),若对`data`排序后采用二分查找,可优化至O(log k)。 
- 矩阵加法:遍历两个矩阵的非0元素,时间复杂度O(k1 + k2)(k1、k2为两个矩阵的非0元素数),优于普通矩阵加法的O(rows×cols)。 

**应用场景**:社交网络邻接矩阵、自然语言处理的词向量矩阵、有限元分析的系数矩阵等稀疏数据场景。 

3.6 本章总结

- **数组**是连续内存存储的线性结构,核心优势是O(1)随机访问,缺点是大小固定、插入删除低效; 
- **Python列表**基于动态数组实现,通过1.5倍扩容策略支持动态大小,`append`均摊O(1),`insert/pop(k)`为O(n); 
- **动态数组设计**需关注容量管理(扩容/缩容策略)、元素移动(插入/删除时)和内存效率(如1.5倍扩容平衡时间与空间); 
- **稀疏矩阵案例**展示了列表在非连续数据存储中的优势,通过三元组列表法显著节省空间,提升操作效率。 

数组与列表作为最基础的线性结构,是实现栈、队列、哈希表等高级数据结构的基石,后续章节将基于列表进一步展开线性结构的应用与优化。 

思考题

1. 分析以下Python列表操作的时间复杂度,并解释原因: 

python

	lst = [1, 2, 3, 4, 5] 
	lst.append(6) # 操作1 
	lst.insert(0, 0) # 操作2 
	lst.pop(3) # 操作3 
	5 in lst # 操作4

动态数组扩容时,若采用“每次+1”的扩容策略(而非倍数扩容),则n次append操作的总时间复杂度是多少?(提示:分析第i次append的耗时,求和后除以n)

设计一个基于列表的“循环缓冲区”(固定大小的队列),支持enqueue(x)(入队)和dequeue()(出队)操作,当缓冲区满时新元素覆盖最早入队的元素,要求所有操作时间复杂度为O(1)。
稀疏矩阵的get方法当前为线性查找(O(k)),如何通过排序和二分查找优化为O(log k)?实现优化后的get方法(提示:对三元组按行、列索引排序,使用bisect模块定位)。

本站原创,转载请注明出处:


相关教程