一、低层次数据结构——数组
计算机体系结构的一些低层次内容
1、 位(bit)和字节(byte)
计算机主存由位信息组成,1个字节相当于8位
2、 存储地址
为了跟踪信息存储在哪个字节中,每个存储字节都和一个作为其地址的唯一数字相关联
3、数组(array)
- 数组的定义
一组相关变量一个接一个地存储在计算机存储器中一块连续区域内的数据结构即为数组。 - 数组的特性
数组的每个单元必须占据相同数量的字节(为了允许使用索引值能够在常量时间内访问数组内的任一单元)。 - 创建低层次数组
在计算机系统中,创建低层次数组时,必须明确声明数组的大小,以便系统为其存储分配连续的内存。
二、基于数组实现的python内置序列类
列表类(list):['a','b','c']
元祖类(tuple):(1,2,3)
字符串类(str):'hello world!'
1、共性
- 都支持用下标访问序列元素,如
seq[k]
- 都使用数组这种低层次概念表示序列
2、不同
列表和元组仅仅储存返回对象的引用,是一种引用结构;
字符串是用字符数组表示的,是一种紧凑数组(compact array):
- 引用结构
通常将64位地址存入数组,无论存储单个元素的对象有多少位 - 紧凑数组
将原始数据(即字符转化的Unicode字节)直接存入数组,通过一个名为array
的模块提供支撑。
注:array模块定义了一个类(也命名为array),该类提供了紧凑存储原始数据类型的方法。
array类的构造函数需要以类型代码作为第一个参数(表明要存入数组的数据类型,确定数组的每个元素需要多少位),不支持存储用户自定义数据类型的紧凑数组(自定义数组类型的紧凑数组可以通过ctypes模块实现)。
例如:
primes = array('i',[2,3,5,7,11,13,18,19])
array模块支持的类型代码
代码 | 数据类型 | 字节的精确位数 |
---|---|---|
‘b’ | signed char | 1 |
‘B’ | unsigned char | 1 |
‘u’ | Unicode char | 2 or 4 |
‘h’ | signed short int | 2 |
‘H’ | unsigned short int | 2 |
‘i’ | signed int | 2 or 4 |
‘I’ | unsigned int | 2 or 4 |
‘l’ | signed long int | 4 |
‘L’ | unsigned long int | 4 |
‘f’ | Float | 4 |
‘d’ | float | 8 |
三、动态数组与摊销
由于系统可能占用数组相邻的内存位置去存储其他数据,因此数组大小不能靠扩展内存单元来无限增加。
在实例变量不可变时(如字符串、元祖类型),不受影响,但当实例变量可变时(如列表类,可对列表进行添加元素,修改元素等操作)需要进行数据大小的调整,这依赖一种算法技巧——动态数组(dynamic array)
1、动态数组的动态过程
step1:
一张列表通常关联着一个底层数组,数组通常比列表长度更长。
(例如:用户创建一张有5个元素的列表,系统可能会预留一个能存储8个对象引用的底层数组。)
step2:
若用户持续添加列表元素,所有预留单元最终将被耗尽。此时列表类向系统请求一个新的、更大的数组,并初始化该数组,使其前面部分能与原来的小数组一样。
同时,原数据不再被需要,因此被系统回收。
2、动态数组的实现
扩展数组A时,我们会执行下面的步骤
- 分配一个更大的数组B(通常做法是,新数组大小是已满旧数组大小的2倍)
- 设B[i] = A[i] (i = 0, 1,…,n-1)
- 设A =B,也就是说,我们以后使用B作为数组来支持列表
- 在新的数组中添加元素
下面举例一种使用Python ctypes模块提供的原始数组实现DynamicArray类
import ctypes
class DynamicArray:
def __init__(self):
self._n = 0 #列表当前存储的实际元素的个数
self._capacity = 1 #当前所分配数组中允许存储的元素最大个数
self._A = self._make_array(self._capacity) #当前所分配数组的引用(最初为None)
def __len__(self) : #支持len(array)
return self._n
def __getitem__(self,k): #支持序列下标array[k]
if not 0<= k < self._n:
raise IndexError('invalid index')
return self._A[k]
def append(self,obj):
if self._n == self._capacity:
self._resize(2 * self._capacity) #若添加前数组已满,把数组容量扩大一倍
self._A[self._n] = obj #扩容后,在扩容的第一个位置存储obj
self._n += 1
def _resize(self,c):
B = self._make_array(c) #分配一个更大的数组B
for k in range(self._n): #设B[i] = A[i] (i = 0, 1,…,n-1)
B[k] = self._A[k]
self._A = B #设A =B,也就是说,我们以后使用B作为数组来支持列表
self._capacity = c
def _make_array(self,c):
return (c * ctypes.py_object)() #不太明白ctypes.py_object具体实现方法,大概可能是在内存中分配一块对应大小的数组空间
3、动态数组的摊销
- 策略1 —— 当数组满时,将数组大小扩大为原来的K倍 → O(n)
(证明略) - 策略2 —— 当数组满时,将数组大小扩大N个 → O(n的平方)
(证明略)
要避免使用类似策略2的等差数列
四、Python内置序列类的效率
1、列表和元组类
非变异行为(不改变原序列中的值)
操作 | 运行时间 |
---|---|
len(data) | O(1) |
data[j] | O(1) |
data.count(value) | O(n) |
data.index(value) | O(k+1) |
value in data | O(k+1) |
data1 == data2 (!=,<,<=,>,>=同理) | O(k+1) |
data[j:k] | O(k-j+1) |
data1 + data2 | O(n1+n2) |
C * data1 | O(cn) |
- 搜寻值的出现
每个count、index和__contains__方法均从左往右迭代遍历序列 - 字典比较
两个序列之间的对比被定义为字典(两个序列从左往右迭代对比)。
在最坏情况下,评估这一情况需要运行时间正比于两序列中长度较短的序列的迭代。 - 创建新实例
切片(data[j:k])、相加(data1 + data2)、复制(C * data1)都是在一个或多个原有实例的基础上重新构造一个新的实例。
运行时间取决于构造和初始化实例所耗费的时间。
变异行为(*摊销)
操作 | 运行时间 |
---|---|
data[j] = val | O(1) |
data.append[value] | O(1)* |
data.insert(k,value) | O(n-k+1)* |
data.pop() | O(1) * |
data.pop(k)、del data[k] | O(n-k)* |
data.remove(value) | O(n)* |
data.extend(data2)、data += data2 | O(n2) |
data.reverse() | O(n) |
data.sort() | O(n log n) |
- 向列表中添加元素
apend方法是直接在列表末尾添加 → O(1)
insert方法是将元素添加在列表的第k位,将末尾至第k位的元素依次向右移动一位 → O(n-k+1) - 从列表中删除元素
pop方法是直接将列表末尾元素删除 → O(1)
pop(k)、del data(k)均是将列表中索引为k的元素删除,将索引k+1至末尾的元素依次向左移动一位 → O(n-k)
remove(value)方法则没有高效的情况,因为每次调用都需要先搜索(O(k)),搜索到之后将索引k+1至末尾的元素依次向左移动一位(O(n-k)),总运行时间O(n) - 拓展列表
运行时间正比于另一张列表的长度。但相较于重复调用append方法,extend方法会更高效一些。 - 构造新列表
在几乎所有情况下,构造新列表的渐进效率在创建列表的长度方面是线性的。
(列表推导式比不断增添数据来建表速度明显快一些)
2、字符串类
对字符串许多行为的分析常靠直觉:
生成新字符串的方法(如:capitalize、center、strip方法)需要的时间与该方法生成的字符串长度之间呈线性关系。
字符串的许多行为(如,islower)以布尔条件进行测试,最坏情况下需要检查n个字符,此时运行时间为O(n),但当结果很明显时,循环就很快结束。
- 样例匹配
从算法角度来说,这些行为在某种程度上取决于在较大字符串中找到字符样例,详情在之后讨论。 - 组成字符串
letter = ''
for c in document:
if c.isalpha():
letters += c
- 以上的代码段虽然能实现目标,但效率非常低下。因为字符串的串联(letters+c)是把结果作为新的字符串实例创建,且重新分配给了标识符letters。运行时间和1+2+…+n成正比,运行时间为O(n的平方)
(注:letters += c
之所以会产生新的字符串实例,是因为假如程序中有另一个变量要引用原字符串,则原字符串必须保持不变。另一方面,假如Python知道在该问题中对该字符串没有其他引用,则通过直接修改字符串(作为一个动态数组)可以更高效地实现 +=)
这样的实现方法会更高效一些:
letter = ''.join(c for c in document if c.isalpha())