低层次数组
- 计算机主存由位信息组成,多位组成更大的单元,例如一个字节相当于8位。
- 计算机将信息存储在字节中,存储地址表示的就是该字节在计算机中的二进制表示。
- 计算机内存中每个字节都被连续编了号,基于这种存储地址,我们将计算机主存称为随机存取存储器(RAM),访问任何单一字节的时间都为O(1)。
- 一组相关变量能够接连存储在计算机存储器的一块连续区域内,称为数组。
- 数组中的每个位置称为单元,并用整数索引值描述该数组。
- 为了允许使用索引值能够在常量时间内访问数组内的任一单元,数组的每个单元必须占据相同数量的字节。
引用数组
-
Python使用数组内部存储机制(即对象引用),来表示一列表或者元组实例。
-
在Python中一切皆对象,判断一个对象是否可变就是看该对象是直接以低层次存储在计算机中,还是以对象引用的方式来存储。
比如在Python中字符串是不可变的,因为字符串是直接存储在RAM上的,如下字符串‘zzyx’:
列表是可变的,因为列表存储的是对象引用:
但是列表中的字符是不可变的,如下你可以变更列表的直接元素,但是不能变更不可变元素的内部内容:
>>> a=['ss',(1,2,3)] >>> a[0]='aa' >>> print(a) ['aa', (1, 2, 3)] >>> a[1][0]=0 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment >>>
-
浅拷贝就是利用的这个原理,浅拷贝就是使用对象引用的方式,所以当修改原对象时,浅拷贝拷贝的对象也会改变。深拷贝就是直接在RAM存储中开辟新的空间将一样的内容存储起来,然后拷贝完的对象指向该存储位置。
所以当被拷贝对象是不可变的时候,深拷贝和浅拷贝是一样的。
-
在Python中扩展一个列表通常使用
extend
命令,该命令不是将另一个列表的元素复制过来,而是将元素的引用复制到末尾,如a.extend(b)
:
Python中的紧凑数组
使用紧凑结构的优势
- 使用紧凑结构会占用更少的内存
- 原始数据在内存中是连续存放的,而引用结构没有这种情况。
Python创建不同类型紧凑数组的方法:
使用array
模块,例如:
primes = array('i',[2,3,5,7])
参数’i’表示的是类型代码:表明该数组类型,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’小写L | signed long int | 4 |
L | unsigned long int | 4 |
‘f’ | Float | 4 |
‘d’ | float | 8 |
array模块不支持用户自定义的数据类型。
动态数组和摊销
创建低层次数组时必须声明数组的大小,以便系统为其存储分配连续的内存。
由于系统可能会占用相邻的内存位置去存储其他数据,因此数组大小不能靠扩展内存单元来无限增加。
Python的列表没有增加数量的限制,底层就是使用了动态数组的算法。
动态数组的算法原理和寄居蟹差不多,用户初始创建一个5个元素的数组,系统可能会给你一个能存储8个对象引用的底层数组,就好比寄居蟹找到一个稍微比自身大点的贝壳,都会存有预留单元,当预留单元耗尽后,列表类就会向系统请求一个新的,更大的数组,就好比寄居蟹换了一个更大的贝壳。
实现动态数组
我们不能扩展数组,但是我们可以通过跟换新的数组来实现对列表的对动态扩容,即实现动态数组,具体步骤如下:
- 分配一个更大的数组B。
- 设B[I] = A[i] (i = 0,···,n-1),其中n表示条目的当前数量。
- 设A = B,也就是说,我们以后使用B作为数组来支持列表。
- 在新的数组里增添元素。
使用ctypes模块实现动态数组类代码:
#! /usr/bin/env python
# -*-coding:utf-8-*-
import ctypes
class DynamicArray:
""" 使用ctyes模块实现动态数组 """
def __init__(self):
""" 创建一个空数组 """
self._n = 0 # 数组实际存储元素数量
self._capacity = 1 # 默认数组容量
self._A = self._make_array(self._capacity) # 创建底层数组
def __len__(self):
""" 返回数组中元素的数量 """
return self._n
def __getitem__(self, k):
""" 返回第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
self._n += 1
def _resize(self, c):
""" 将原数组的内容移到扩容后的数组内 """
B = self._make_array(c)
for k in range(self._n):
B[k] = self._A[k]
self._A = B
self._capacity = c
@staticmethod
def _make_array(self, c):
""" 调用ctype模块方法实现底层数组创建 """
return (c * ctypes.py_object)()
if __name__ == "__main__":
myarray = DynamicArray()
myarray.append(3)
print(myarray[0])
- 动态数组的设计理想增长模式:大小按几何增长,确保数据结构占用O(n)的内存,一般增大2倍
- 避免使用等差数列:每次扩增的时候增加固定的数量
- 当动态数组的元素删除后,也要保证数据结构占用O(n)的内存,此时采用紧凑底层数组的方式:无论实际元素个数比数组大小的1/4少多少,都对半平分数组。
Python序列类型的效率
Python的列表类
向列表中增添元素
在添加元素的时候需要将添加位置的右边的元素往前右移动一个单位,所以一般情况下appned
方法比insert
方法效率高。
代码DynamicArray类insert方法的实现:
def insert(self, k, value):
""" 插入一个元素 """
if self._n == self._capacity:
self._resize(2 * self._capacity)
for j in range(self._n, k, -1):
self._A[j] = self._A[j-1]
self._A[k] = value
self._n += 1
从列表删除元素
-
删除元素需要将删除的位置的右边的元素往左移动一个单位
-
pop()
删除列表的最后一个元素效率是最高的,删除第一个元素效率是最低的。 -
remove()
删除指定值的第一个元素,remove的运行时间是固定的,不论删除的元素在那个位置都是**Ω(n)**的运行时间,因为在从开头找到指定的值删除后,剩余的从k到最后的迭代用于往左移动元素。
代码DynamicArray类remove方法的实现:
def remove(self, value):
""" 删除指定的第一个出现的元素 """
# 没有实现紧凑底层数组
for k in range(self._n):
if self._A[k] == value:
for j in range(k, self._n-1):
self._A[j] = self._A[j+1]
self._A[_n-1] = None
raise ValueError('value not found')
扩展列表
将一个列表的所有元素增添到另一张列表的末尾,采用extend方法是最合适的,效率最高的。
- 与调用很多独立的函数相比,调用一个函数完成所有工作的开销更小
extend
能提前计算出更新完列表的最终大小- 如果使用循环
append
方法,底层动态数组会有多次调整大小的风险,若调用一次extend
方法,最多执行一次调整操作。
构造新列表
列表推导式最快。
使用乘法操作初始化一个具有固定值的列表,是一种很好的习惯。
比如:[None]*n
Python的字符串类
组成字符串
例子:有一个较大的字符串document,我们的目标是生成一个新的字符串letters,该字符串仅包含原字符串的英文字母。
一般会采用循环判断的方式,如下:
letters = ''
for c in document:
if c.isalpha():
letters += c
这种效率是非常低的:
letters += c
会产生新的字符串实例且重新分配给标识符letters
,运行时间会是1+2+···+n为Ω(n^2)
优化做法是使用临时表存储单个数据,然后使用字符串类的join方法组合最终结果:
temp = []
for c in document:
if c.isalpha():
temp.append(c)
letters = ''.join(temp)
使用列表推导式进一步优化:
letters = ''.join([c for c in document if c.isalpha()])
使用生成器的理解来优化,省去临时表:
letters = ''.join(c for c in document if c.isalpha())
使用列表推导式进一步优化:
letters = ''.join([c for c in document if c.isalpha()])
使用生成器的理解来优化,省去临时表:
letters = ''.join(c for c in document if c.isalpha())