基于数组的序列-Python(学习笔记1-1)

基于数组的序列-Python(学习笔记1-1),第1张

基于数组的序列-Python(学习笔记1-1)

文章目录
      • 一、序列类型
      • 二、低层次数组
        • 1. 数组的引用
        • 2. 紧凑数组
      • 三、动态数组
        • 1. 动态数组的实现
        • 2. 动态数组的均摊分析
        • 3. 内存使用与紧凑数组
      • 四、Python序列类型的效率
        • 1.列表和元组类
        • 2.字符串类

一、序列类型

Python的序列类:列表类(list)、元组类(tuple)、字符串类(str),每个类都支持用下标访问序列元素,且都使用数组这种低层次概念表示序列。

二、低层次数组

数组:在内存中开辟一块连续的区域,以有序存储一组相关变量。数组的每个单元必须占据相同数量的字节。

1. 数组的引用

Python使用数组内部存储机制,即对象引用。在最低层,存储的是一块连续的内存地址序列,这些地址指向一组元素序列。虽然单个元素的相对大小可能不同,但每个元素存储地址的位数是固定的。在这种情况下,可以通过索引值以常量时间访问元素列表或元组。

浅拷贝与深拷贝: 浅拷贝仅拷贝第一层,Python默认的拷贝都是浅拷贝;深拷贝会逐层进行拷贝,直到拷贝的所有引用都是不可变引用为止,使用copy模块的deepcopy函数可以实现深拷贝。

a = [16, 17]
ls1 = [a, 18, 19]
ls2 = [11, 12, 13, 14, 15]

ls2.extend(ls1)
print(ls2)
# [11, 12, 13, 14, 15, [16, 17], 18, 19]

ls1[0] = 0
print(ls1)
print(ls2)
# [0, 18, 19]
# [11, 12, 13, 14, 15, [16, 17], 18, 19]

a[0] = 26
print(ls1)
print(ls2)
# [0, 18, 19]
# [11, 12, 13, 14, 15, [26, 17], 18, 19]

2. 紧凑数组

紧凑数据主要通过array模块提供支撑,array模块定义了一个序列数据结构,与list类似,区别是所有成员都必须是相同的基本类型。优点是会占用更少的内存,例如字符串是用字符数组表示的(而不是用数组的引用,引用结构会将地址存入数组,无论存储单个元素的对象有多少位,如果每个字符都以单字符字符串独立存储,将会占用更多的字节);另一方面的优势是原始数据在内存中是连续存储的。

import array
# 第一个参数用来表示数据类型
a1 = array.array('b', [1, 2, 3, 4])
print(a1)
# array('b', [1, 2, 3, 4])
array模块支持的类型代码
代码数据类型字节的精确位数‘b’signed char1‘B’unsigned char1‘h’signed short2‘H’unsigned short2‘i’signed int2‘I’unsigned int2‘l’signed long4‘L’unsigned long4‘q’signed long long8‘Q’unsigned long long8‘f’float4‘D’double float8 三、动态数组

在计算机系统中,创建低层次数组时,必须明确声明数组的大小,以便系统为其存储分配连续的内存。由于系统可能会占用相邻的内存位置去存储其他数据,因此数组大小不能靠扩展内存单元来无限增加,但对于元组或字符串这种不可变类型,当对象被实例化时,底层数组的大小就已经确定;而对于列表这种可变类型,依赖了动态数组的算法技巧。
一个列表所关联的底层数组,通常比列表的长度更长。如果用户持续增添列表元素,所有预留单元最终将被耗尽,此时列表类向系统请求一个新的、更大的数组,使新数组前面的部分与原数组一样,并回收掉旧数组。

import sys

print(sys.getsizeof("Python"))
# 31
data = []
for i in range(30):
    a = len(data)
    # 对列表实例使用函数getsizeof得到的结果仅包含该列表主要结构的大小,不包含由对象(列表元素)所占用的内存
    b = sys.getsizeof(data)
    print("Length:{0:3d};Size in bytes:{1:4d}".format(a, b))
    data.append("Python")
# Length:  0; Size in bytes:  36
# Length:  1; Size in bytes:  52
# Length:  2; Size in bytes:  52
# Length:  3; Size in bytes:  52
# Length:  4; Size in bytes:  52
# Length:  5; Size in bytes:  68
# Length:  6; Size in bytes:  68
# Length:  7; Size in bytes:  68
# Length:  8; Size in bytes:  68
# Length:  9; Size in bytes: 100
# Length: 10; Size in bytes: 100
# Length: 11; Size in bytes: 100
# Length: 12; Size in bytes: 100
# Length: 13; Size in bytes: 100
# Length: 14; Size in bytes: 100
# Length: 15; Size in bytes: 100
# Length: 16; Size in bytes: 100
# Length: 17; Size in bytes: 136
# Length: 18; Size in bytes: 136
# Length: 19; Size in bytes: 136
# Length: 20; Size in bytes: 136
# Length: 21; Size in bytes: 136
# Length: 22; Size in bytes: 136
# Length: 23; Size in bytes: 136
# Length: 24; Size in bytes: 136
# Length: 25; Size in bytes: 136
# Length: 26; Size in bytes: 176
# Length: 27; Size in bytes: 176
# Length: 28; Size in bytes: 176
# Length: 29; Size in bytes: 176    

可以注意到,空列表已经请求了一定数量字节的内存,因为在Python中每个对象都保存了一定的状态信息。

1. 动态数组的实现

当底层数组已满,而有元素要添入新列表时,会执行以下步骤:
1 )分配一个更大的数组B(新数组的大小通常为已满的旧数组大小的2倍)。
2 )设B[i] = A[i](i = 0,1,···,n-1),其中n表示条目的当前数量。
3 )设A = B,以后使用B作为数组来支持列表。
4 )回收数组A,在数组B中添加元素。

2. 动态数组的均摊分析

在数组的替换过程中,由于增大了一倍的容量,新数组在被替换之前允许增添n个新元素。


命题3-1: 设S是一个由具有初始大小的动态数组实现的数组,实现策略为:当数组已满时,将此数组大小扩大为原来的2倍。S最初为空,对S连续执行n个增添 *** 作的运行时间为O(n)。
证明: 将计算机视为投币装置,对每个固定的运行时间均支付一个网络硬币,当执行一个 *** 作时,要有足够的网络硬币来支付此次 *** 作的运行时间。因此,在任意计算中所花费的网络硬币总数将会和该计算的运行时间成正比。在不需要扩大数组的情况下,向S中增加一个元素所需时间需支付一个网络硬币,且假定数组从k增长到2k,初始化该数组需要k个网络硬币。对每个增添 *** 作索价3个网络硬币,因此不需要扩大数组的增添 *** 作多付了2个网络硬币。当数组S大小为2i且S中已有2i个元素时,增加元素将会出现溢出。由于前一次溢出出现在当元素个数第一次比2i-1大时,所以在2i-1到2i-1中还有尚未使用的网络硬币。因此,可以用3n个网络硬币来支付n次增添 *** 作,即每个增添 *** 作的均摊运行时间为O(1),所以n次增添 *** 作的总体运行时间为O(n)。
数学证明: 当数组S大小为2i且S中已有2i个元素时,增加一个元素的 *** 作时间为2i+1,其他情况下为1,所以2n+k(k≤2n)次增添 *** 作的总时间为 2n + k + ∑ i = 1 n 2 i sum_{i=1}^n2^i ∑i=1n​2i,简化得3 * 2n + k - 1,当n趋近于无穷时,每个增添 *** 作的运行时间为O(1),所以n次增添 *** 作的总体运行时间为O(n)。

命题3-2: 对初始为空的动态数组执行连续n个增添 *** 作,若每次调整数组大小时采用固定的增量,则运行时间为Ω(n2)。
证明: 设c为每次调整数组大小时的固定增量,在连续的n次增添 *** 作中,时间主要花费在分别初始化c、2c、3c···mc上,其中m = ⌈n/c⌉ ,因此运行时间正比于c + 2c + 3c + ··· + mc,求和为
∑ i = 1 m c i sum_{i=1}^mci ∑i=1m​ci = c ∑ i = 1 m i sum_{i=1}^mi ∑i=1m​i = c m ( m + 1 ) 2 {m(m+1)over 2} 2m(m+1)​≥c n c ( n c + 1 ) 2 {{nover c}({nover c}+1)over 2} 2cn​(cn​+1)​≥ n 2 2 c {n^2over 2c} 2cn2​
因此,执行连续的n次增添 *** 作所花费的时间为Ω(n2),所以在每次调整数组大小时,避免使用等差数列。

Python列表类使用动态数组的形式存储内容,但在扩展数组的时候,既不是使用纯粹的几何级数,也不是使用等差数列。

3. 内存使用与紧凑数组

当对动态数组增添数据时,最终数组的大小能正比于元素的总个数,但当许多元素被删除后,元素的实际数量与数组大小便不存在正比关系。有时会采用一种策略:只要实际元素个数小于数组大小的1/4,则将数组大小缩小为原来的一半,这样能确保数组大小至少为元素个数的4倍。

四、Python序列类型的效率 1.列表和元组类

元组比列表的内存利用率更高,因为元组是固定不变的,所以没必要创建拥有剩余空间的动态数组。

非变异行为
*** 作运行时间len(data)O(1)data[j]O(1)data.count(value)O(n)data.index(value)O(k + 1)value in dataO(k + 1)data1 = data2 (similarly !=、<、≤、>、≥)O(k + 1)data[j : k]O(k - j + 1)data1 + data2O(n1 + n2)data * cO(cn)
变异行为
*** 作运行时间data[j] = valueO(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)(均摊)data1.extend(data2)
data1 += data2O(n2)(均摊)data1.reverse()O(n)data.sort()O(nlogn) 2.字符串类

对于生成新字符串的方法,所需要的时间与该方法生成的字符串长度之间呈线性关系;如果以布尔条件进行测试,在最坏的情况下需要检查n个字符,此时运行时间为Ω(n)。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5069702.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-16
下一篇 2022-11-16

发表评论

登录后才能评论

评论列表(0条)

保存