文章目录

  • 一、使用动态数组实现列表
    • 1. 动态数组概念引入
    • 2. 验证列表实现策略
    • 3. 动态数组算法实现
  • 二、摊销法分析时间复杂度
    • 1. 摊销法使用示例
    • 2. 数组容量指数增长
    • 3. 数组容量等差增长
  • 三、列表常见操作时间复杂度
    • 1. 非列表修改类操作
    • 2. 列表修改类操作
      • 2.1 添加元素
      • 2.2 删除元素
        • `pop()`
        • `remove()`
      • 2.3 扩充列表
    • 3. 创建列表类操作

由文章【数据结构与算法Python描述】字符串、元组、列表内存模型简介中的讨论可知,对于Python的strlisttuple三种序列类型,不论连续内存空间是直接存储对象数据(如:str)还是对象数据的引用(如:listtuple),连续内存的大小都需要在序列创建时根据计划保存的元素数量就指定。

然而,Python的解释器内部一定针对列表的实现做了特殊处理,否则,如果后续希望使用append()extend()方法向列表中添加元素就可能会有问题,因为系统可能已经将列表序列临近的内存空间分配用于存储其他数据了,这对于字符串或元组则不会有问题,因为二者均为不可变类型,这意味着二者也不用支持类似列表中方法来实现容量的增长。

针对上述疑问,本文将探究Python中列表的实现原理,以及列表支持的常见操作如append()insert()等的时间复杂度。

一、使用动态数组实现列表

1. 动态数组概念引入

实际上,Python底层实现列表的算法为动态数组,通过该算法实现的列表特点为:

  • 列表实例的底层维护了一个容量大于当前列表长度的数组,如:开发者可能创建的是一个含有5个元素的列表,但实际上支持列表的数组容量实际为8;
  • 当数组容量已满时,系统会先创建一个新的、容量更大的数组,接着使用容量已满的旧数组中的元素来从头开始初始化新数组,最后旧数组会被解释器垃圾回收。

2. 验证列表实现策略

为了验证列表底层的确是基于动态数组实现,现使用下列代码。需要说明的是,下列代码中使用的getsizeof(object)方法来自sys模块,其功能是以字节为单位返回对象占用的内存大小,但对于采用引用型数组的列表而言,该方法的特殊性在于:只会返回列表底层数组以及列表对象实例属性的字节数,而不会考虑数组每一个元素所引用具体数据对象的字节数。

import sys
lst = list()
for each in range(27):lst_length = len(lst)lst_size = sys.getsizeof(lst)print('列表长度:{0:2d}; 占用字节大小:{1:4d}'.format(lst_length, lst_size))lst.append(None)

上述代码的执行结果为:

列表长度: 0; 占用字节大小: 56
列表长度: 1; 占用字节大小: 88
列表长度: 2; 占用字节大小: 88
列表长度: 3; 占用字节大小: 88
列表长度: 4; 占用字节大小: 88
列表长度: 5; 占用字节大小: 120
列表长度: 6; 占用字节大小: 120
列表长度: 7; 占用字节大小: 120
列表长度: 8; 占用字节大小: 120
列表长度: 9; 占用字节大小: 184
列表长度:10; 占用字节大小: 184
列表长度:11; 占用字节大小: 184
列表长度:12; 占用字节大小: 184
列表长度:13; 占用字节大小: 184
列表长度:14; 占用字节大小: 184
列表长度:15; 占用字节大小: 184
列表长度:16; 占用字节大小: 184
列表长度:17; 占用字节大小: 256
列表长度:18; 占用字节大小: 256
列表长度:19; 占用字节大小: 256
列表长度:20; 占用字节大小: 256
列表长度:21; 占用字节大小: 256
列表长度:22; 占用字节大小: 256
列表长度:23; 占用字节大小: 256
列表长度:24; 占用字节大小: 256
列表长度:25; 占用字节大小: 256
列表长度:26; 占用字节大小: 336

分析上述代码的执行结果:

  • 由第一行可知,即使列表长度为0时,该列表对象就已经占用了一定的字节数(在笔者的系统上为56个字节),实际上这是因为每一个Python中的对象都会保存一些状态信息,如:

    • 表明创建该对象的类的引用;
    • 表明当前列表元素个数的长度_array_length
    • 表明当前列表底层数组的容量_array_capacity
    • 代表当前列表底层数组的引用_array
  • 由第二行可知,当插入第一个元素时,底层数组的容量增长了32个字节,在本64位的机器上(即内存地址占8个字节),这意味着此时列表底层的数组可保存4个对象的引用,这和插入第2,3,4个元素后并未见容量变化这一现象是一致的;
  • 由第六行可知,插入第5个元素后,底层数组的容量又增长了32个字节,从而使得其又可以多存储4个对象的引用;
  • 后续底层数组的容量继续增长,区别是增长得更快了,即从单次增长32个字节到64个字节,再到72个字节,进而到80个字节。

3. 动态数组算法实现

尽管Python的list类基于动态数组已经提供了高度优化的实现,但通过亲自实现一个类似功能的类,对于开发者大有裨益。

为了实现类似list的类,关于在于如何“扩充”底层数组AAA的容量。实际上,当底层数组已满,可采用下列算法实现底层数组容量的“扩充”:

  1. 新分配一个容量更大的底层数组BBB,如下图步骤(a)所示;
  2. 当i=0,⋅⋅⋅,n−1i=0, \cdot\cdot\cdot, n-1i=0,⋅⋅⋅,n−1时,设置B[i]=A[i]B[i]=A[i]B[i]=A[i],其中nnn是AAA中元素个数,如下图步骤(b)所示;
  3. 设置A=BA=BA=B,则后续底层数组BBB代表的内存空间将承担对象数据引用(references of objects)的存储任务,如下图步骤(c)所示;
  4. 至此,后续新的元素将会被插入更大容量的新底层数组中。

上述算法的Python代码实现如下所示:

import ctypes
from time import timeclass DynamicArray:"""Python list类的简化版本"""def __init__(self):"""初始化方法,用于创建空数组"""self._array_length = 0  # 实际元素数量self._array_capacity = 1  # 底层数组容量self._array = self._create_array(self._array_capacity)  # 底层数组引用def __len__(self):"""返回数组已存储数据的数量:return: 当前DynamicArray实例对象的长度"""return self._array_lengthdef __getitem__(self, idx):"""返回索引为idx的元素:param idx::return:"""if not 0 <= idx < self._array_length:raise IndexError('索引越界!')return self._array[idx]@propertydef capacity(self):"""返回动态数组容量:return:"""return self._array_capacitydef append(self, obj):"""向数组尾部追加对象数据:param obj::return:"""if self._array_length == self._array_capacity:  # 如果数组容量已满self._resize(2 * self._array_capacity)  # 底层数组容量翻倍self._array[self._array_length] = objself._array_length += 1def _resize(self, capacity):  # 私有工具方法"""将数组容量调整为capacity:param capacity: 底层数组容量:return: None"""resized_array = self._create_array(capacity)  # 新的容量更大底层数组for idx in range(self._array_length):resized_array[idx] = self._array[idx]  # 将旧数组元素拷贝至新数组self._array = resized_array  # 使用新的数组self._array_capacity = capacitydef _create_array(self, _array_capacity):  # 私有工具方法"""返回容量为_array_capacity的新数组:param _array_capacity: 指定创建的底层数组容量:return: “扩容”后的新底层数组"""return (_array_capacity * ctypes.py_object)()  # 创建并返回新的数组def main():dyn_arr = DynamicArray()for each in range(19):dyn_arr_length = len(dyn_arr)dyn_arr_capacity = dyn_arr.capacityprint('列表长度:{0:2d}; 数组当前容量:{1:4d}'.format(dyn_arr_length, dyn_arr_capacity))dyn_arr.append(None)if __name__ == '__main__':main()

上述代码的运行结果为:

列表长度: 0; 数组当前容量: 1
列表长度: 1; 数组当前容量: 1
列表长度: 2; 数组当前容量: 2
列表长度: 3; 数组当前容量: 4
列表长度: 4; 数组当前容量: 4
列表长度: 5; 数组当前容量: 8
列表长度: 6; 数组当前容量: 8
列表长度: 7; 数组当前容量: 8
列表长度: 8; 数组当前容量: 8
列表长度: 9; 数组当前容量: 16
列表长度:10; 数组当前容量: 16
列表长度:11; 数组当前容量: 16
列表长度:12; 数组当前容量: 16
列表长度:13; 数组当前容量: 16
列表长度:14; 数组当前容量: 16
列表长度:15; 数组当前容量: 16
列表长度:16; 数组当前容量: 16
列表长度:17; 数组当前容量: 32
列表长度:18; 数组当前容量: 32

分析算法实现的上述运行结果,可知底层数组的容量的确是在每次旧数组已满的情况下翻倍增长。

需要注意的是:

  • 由于需要遵循封装的思想,即使用者无需了解DynamicArray底层的实现原理,所以诸如底层数组扩容方法_resize(),创建新的底层数组方法_create_array()均为私有方法;
  • 由于需要仿照list类使得DynamicArray支持len()测长度,支持通过非负整数索引访问元素,故分别实现了__len__()__getitem__()方法。

二、摊销法分析时间复杂度

在上述实现的和list功能相似的DynamicArray类中,我们提供了一个和list中功能一样的append()方法,下面以此方法为例介绍一种分析算法时间复杂度的策略——摊销法1

1. 摊销法使用示例

首先,通过上述实现append()的代码你可能注意到,每次当底层数组容量已满,此时如果向底层数组插入元素时,由于需要先创建容量翻倍的底层数组,然后进行nnn次由旧数组向新数组的拷贝操作,故此次插入成本很高;但是在此次操作之后,直至底层数组容量需要再次翻倍之前,每次调用append()插入元素只需要1次基本操作,即采用上述算法实现的append()操作,其一系列插入操作的平均时间复杂度较低。

为了分析上述append()操作的具体时间复杂度,我们介绍这样一个技巧:假设计算机是一个接收算力币来运行的机器:

  • 每一个算力币可以让计算机完成一次基本操作;
  • 如果一次给计算机投递的算力币大于所需其进行的基本操作次数,那么结余的算力币算作寄存在计算机中供后续使用;
  • 最终,计算nnn次append()操作花费的平均算力币数量即为该方法的摊销时间复杂度

基于上述假设,对于初始长度为_array_length = 0,容量为_array_capacity = 1DynamicArray实例,我们通过下列示意图来逐步推演append()方法的时间复杂度:

  • 初始长度为_array_length=20\_array\_length = 2^0_array_length=20,容量为_array_capacity=20\_array\_capacity = 2^0_array_capacity=20时,第(20=1)(2^0=1)(20=1)次追加操作使用3个算力币,索引为0处结余2个算力币;
  • 长度为_array_length=20\_array\_length = 2^0_array_length=20,容量为_array_capacity=20\_array\_capacity = 2^0_array_capacity=20时,第(20+1=2)(2^0+1=2)(20+1=2)次追加操作使用3个算力币,索引为0处结余1算力币(其中一个用于底层新旧数组拷贝),索引为1处结余2个算力币;
  • 长度为_array_length=21\_array\_length = 2^1_array_length=21,容量为_array_capacity=21\_array\_capacity = 2^1_array_capacity=21时,第(21+1=3)(2^1+1=3)(21+1=3)次追加操作使用使用3个算力币,此时索引为0处和2处个结余1个和2个算力币,1处的2个算力币用于底层新旧数组拷贝;
  • 长度为_array_length=22\_array\_length = 2^2_array_length=22,容量为_array_capacity=22\_array\_capacity = 2^2_array_capacity=22时,第(22+1=5)(2^2+1=5)(22+1=5)次追加操作使用使用3个算力币,此时索引为0处和4处结余1个和2个算力币;2,3处的4个算力币用于底层新旧数组拷贝;
  • 长度为_array_length=23\_array\_length = 2^3_array_length=23,容量为_array_capacity=23\_array\_capacity = 2^3_array_capacity=23时,第(23+1=9)(2^3+1=9)(23+1=9)次追加操作使用使用3个算力币,此时索引为0处和8处结余1个和2个算力币;4,5,6,7处的8个算力币用于底层新旧数组拷贝;
  • ⋅⋅⋅⋅⋅⋅⋅\cdot\cdot\cdot\cdot\cdot\cdot\cdot⋅⋅⋅⋅⋅⋅⋅
  • 长度为_array_length=2n\_array\_length = 2^n_array_length=2n,容量为_array_capacity=2n\_array\_capacity = 2^n_array_capacity=2n时,第(2n+1)(2^n+1)(2n+1)次追加操作使用使用3个算力币,此时索引为0处和2n2^n2n处分别结余1个和2个算力币;2n−1,⋅⋅⋅,2n−12^{n-1}, \cdot\cdot\cdot, 2^n-12n−1,⋅⋅⋅,2n−1的2n2^n2n个算力币用于底层新旧数组拷贝。

由上述分析可知,前(2n+1)(2^n+1)(2n+1)次追加操作耗费的算力币为3×(2n+1)3×(2^n+1)3×(2n+1)个,因此:

结论1:如果一个序列底层使用初始容量_array_capacity为1的空序列实现,且每次底层数组容量满溢后都翻倍,那么对该序列进行nnn次追加操作的时间复杂度为O(n)O(n)O(n)。

2. 数组容量指数增长

得出上述结论1的前提是:序列的底层数组在每次满溢时容量都乘以2。实际上,只要序列底层数组在每次满溢时容量都乘以一个常量(底数),即容量以指数级增长,则可证明对序列进行的追加操作,其摊销时间复杂度都是O(1)O(1)O(1)

3. 数组容量等差增长

可能有人会想,每次底层数组容量满溢时让其呈指数级增长会很浪费内存,因此考虑当容量满溢时让其按照等差量级增长。实际上,这种算法的时间复杂度要差很多。

我们考虑极限情况:如果底层数组每次容量满溢时仅增加1,那么追加前nnn个元素所需要的基本操作总数(新旧数组拷贝和追加操作)为:1+(1+1)+(2+1)+(3+1)+⋅⋅⋅+(n−1+1)=n(n+1)/21+(1+1)+(2+1)+(3+1)+\cdot\cdot\cdot+(n-1+1)={\left. n(n+1)\middle/ 2\right.}1+(1+1)+(2+1)+(3+1)+⋅⋅⋅+(n−1+1)=n(n+1)/2,即此时nnn次追加操作的时间复杂度为O(n2)O(n^2)O(n2)。

三、列表常见操作时间复杂度

1. 非列表修改类操作

列表常见的非修改类操作及其时间复杂度如下表所示。实际上,下表所有这些操作在元组中也都支持,且其时间复杂度和列表对应操作一致,但从内存利用率来看,元组更加高效,因为元组的长度一般等于其容量。

操作 时间复杂度
len(data) O(1)O(1)O(1)
data[j] O(1)O(1)O(1)
data.count(value) O(n)O(n)O(n)
data.index(value) O(k+1)O(k+1)O(k+1)
value in data O(k+1)O(k+1)O(k+1)
data1 == data2 O(k+1)O(k+1)O(k+1)
data[j:k] O(k−j+1)O(k-j+1)O(k−j+1)
data1 + data2 O(n1+n2)O({n_1}+{n_2})O(n1​+n2​)
c * data O(cn)O(cn)O(cn)

2. 列表修改类操作

因为大多数的列表修改类操作(除data[j] = value外)都可能导致底层数组的容量扩充,所以下多数修改类的时间复杂度均为摊销时间复杂度

操作 时间复杂度
data[j] = value O(1)O(1)O(1)
data.append(value) O(1)O(1)O(1)2
data.insert(k, value) O(n−k+1)O(n−k + 1)O(n−k+1)2
data.pop() O(1)O(1)O(1)2
data.pop(k)
del data[k]
O(n−k)O(n-k)O(n−k)2
data.remove(value) O(n)O(n)O(n)2
data1.extend(data2)
data1 += data2
O(n2)O(n_2)O(n2​)2
data.reverse() O(n)O(n)O(n)
data.sort() O(nlogn)O(nlogn)O(nlogn)

2.1 添加元素

Python列表中除了支持向尾部追加元素外,还支持insert(k, value)向列表任意位置k处插入值value,而此方法在执行插入(1次)前会将k右侧所有元素向右平移一个单元(共计平移(n−k)(n-k)(n−k)次),从而先为待插入元素空出位置。以下是该插入算法的示意图,显然该操作的时间复杂度为O(n−k+1)O(n-k+1)O(n−k+1)。

下列代码在上述DynamicArray类中实现了insert(pos, value)方法:

def insert(self, position, value):"""在指定位置插入值,并将后续值向右平移:param position: 指定位置:param value: 待插入的值:return: None"""if self._array_length == self._array_capacity:  # 当前底层数组容量已满self._resize(2 * self._array_capacity)  # 创建容量翻倍的底层数组for i in range(self._array_length, position, -1):  # 从最右边的元素开始移动self._array[i] = self._array[i - 1]self._array[position] = value  # 将value插入指定位置self._array_length += 1

2.2 删除元素

pop()

Python的list类提供了若干个从列表中删除元素的方式,其中调用pop()可以将列表最后一个元素删除。这种删除方式最高效,因为所有其他元素的位置都保持不变,显然这是O(1)O(1)O(1)时间复杂度的操作,但需要注意的是该复杂度是经摊销后的,因为Python解释器可能会缩小底层数组的容量以节省内存。

该方法还可以接收一个非负整数作为参数,调用pop(k)会删除索引为kkk的元素值,然后将该元素所有右边的值左移一个单元(如下图所示),这种形式的方法调用,其时间复杂度为O(n−k)O(n-k)O(n−k),因为左移的操作量取决于kkk,这也意味着pop(0)的效率最低。

remove()

Python的list类还提供了另外一个删除用的操作remove(value),该方法可以直接指定想要删除的值(而非通过索引先找到value再对其删除),该方法会删除其找到的第一个value值,如果列表中不存在value,则抛出ValueError异常,基于此,下面是算法的Python实现。

值得注意的是,对于任何value值,该方法的时间复杂度都是O(n)O(n)O(n),因为:

  • 如果列表中存在value,则对于value值,该方法的调用都分为两部分:

    • 从左至右找到位于kkk处的value,复杂度为O(k)O(k)O(k);
    • 将位置kkk右边所有元素向左移动一个单元,复杂度为O(n−k)O(n-k)O(n−k)。
  • 如果列表中不存在value,则算法依然要遍历列表的所有元素。
def remove(self, value):"""删除左起第一个出现的value,如不存在则抛出ValueError异常:param value: 待删除的值:return: None"""for i in range(self._array_length):if self._array[i] == value:  # 在动态数组中找到了待删除的valuefor j in range(i, self._array_length - 1):  # 将value右边每个元素左移一个位置self._array[j] = self._array[j+1]self._array[self._array_length - 1] = None  # 协助进行垃圾回收self._array_length -= 1return raise ValueError('不存在指定要删除的', value)

2.3 扩充列表

Python的列表还提供一个名为extend()的方法,该方法用于将一个一个列表中的所有元素挨个追加到另一个列表后,实际上lst1.extend(lst2)就等价于:

for element in lst2:lst1.append(element)

该方法的时间复杂度也是经摊销后的,因为其也可能导致底层数组的容量扩增。

实际上虽然我们说extend()方法在很大程度上等价于重复调用append()方法,但使用extend()方法的效率通常要比重复调用append()方法效率高很多,原因在于:

  • 调用单个extend()方法通常比重复调用append()少了很多额外的开销,如for循环状态的保持等;
  • 使用extend()方法能够是的解释器提前知道最终序列的长度,从而可能仅需一次扩容,而重复使用append()方法可能导致反复的扩容,则会引起多次新旧数组拷贝过程。

3. 创建列表类操作

Python中支持多种生成新列表的语法,如+*和列表推导式等,虽然在绝大数情况下,这些操作的时间复杂度都是和待创建列表的长度成正比,但是就像功能类似的extend()和重复调用append()方法的关系,实际创建列表操作之间的时间复杂度也有较大差别。

例如,虽然:

squares = [k * k for k in range(1, n+1)]

可以视为:

squares = []
for k in range(1, n+1):squares.append(k*k)

的简写,但是实验证明前者的速度要远快于后者。

类似地,通过语法[0] * n创建一个长度为n且元素初始值均为0的列表,其速度也远快于重复调用nappend()方法。


  1. 所谓摊销可以简单理解为平均之意。 ↩︎

  2. 经摊销后的时间复杂度。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

【数据结构Python描述】手动实现一个list列表类并分析常用操作时间复杂度相关推荐

  1. 【数据结构Python描述】树的简介、基本概念和手动实现一个二叉树

    文章目录 一.树的简介 1. 树的定义 2. 相关概念 二.树的ADT 三.树的实现准备 1. 树的基本分类 2. 树的抽象基类 四.树的概念拾遗 1. 深度 2. 高度 五.二叉树的简介 1. 定义 ...

  2. 【数据结构Python描述】优先级队列描述“银行VIP客户插队办理业务”及“被插队客户愤而离去”的模型实现

    文章目录 一.支持插队模型的优先级队列 队列ADT扩充 队列记录描述 方法理论步骤 `update(item, key, value)` `remove(item)` 二.支持插队模型的优先级队列实现 ...

  3. python中如何创建一个空列表_Python学习笔记(1):列表的四种创建方法

    我的电脑安装的是Anaconda 3开源的Python发行版本,其中是集合3.6版本的Python与可视化编程工具采用的是Spyder. 打开Spyder可视化工具,新建一个空白文件,做好备注为&qu ...

  4. 鱼C工作室《零基础入门学习Python》 学习过程笔记【011列表类的方法】

    011. 如何交换列表中两个位置的值?(用从前那种交换两个变量的值的方法即可) >>> b=[0,1,2] >>> b[1] 1 >>> b[2] ...

  5. 鱼c工作室python课件_鱼C工作室《零基础入门学习Python》 学习过程笔记【011列表类的方法】...

    011. 如何交换列表中两个位置的值?(用从前那种交换两个变量的值的方法即可) >>> b=[0,1,2] >>> b[1] 1 >>> b[2] ...

  6. python中如何创建一个空列表_Python创建空列表的字典2种方法详解

    如果要在 Python 中创建键值是空列表的字典,有多种方法,但是各种方法之间是否由区别?需要作实验验证,并且分析产生的原因.本文针对两种方法做了实验和分析. 如果要在 Python 中创建一个键值都 ...

  7. 数据结构python描述英文版_数据结构——Python语言描述

    本书介绍了线性表,栈,队列,串,树和图等基本数据结构,以及这些数据结构的相关应用,还介绍了查找和排序的常用算法.本书介绍内容时理论和实现并重,并配有一定数量的上机实验和习题用于帮助读者巩固和加深对相关 ...

  8. 数据结构 : 单链表 头插入法尾插入法 及几种常用操作

    头插入法 在初始化之后,就可以着手开始创建单链表了,单链表的创建分为头插入法和尾插入法两种,两者并无本质上的不同,都是利用指针指向下一个结点元素的方式进行逐个创建,只不过使用头插入法最终得到的结果是逆 ...

  9. 关于学习Python的一点学习总结(44->类中的比较操作符号重写)

    84.一些比较操作的符号重写: 1.__lt__(self,other):定义小于号的行为:x<y 调用x.__lt__(y) 2.__le__(self,other):定义大于等于号的行为:x ...

最新文章

  1. 黑计算机学校给的处分,学校处分通告格式
  2. php播放ppt代码,PHP+JavaScript幻灯片代码
  3. java简单数据结构_图解Java常用数据结构
  4. Web开发人员的必备工具 - Emmet (Zen Coding)
  5. python 不定参数_人生苦短,我学不会Python。——函数中不定长参数的写法
  6. 0505.Net基础班第二十天(基础加强总复习)
  7. YYKit是个好东西-YYLabel实现一个文本多个点击事件
  8. JAVA实现EXCEL公式专题(七)——统计函数
  9. iOS 结合YYLabel实现文本的展开和收起
  10. android修改屏幕比例,安卓屏幕比例修改器
  11. vim 编辑器sed 替换字符串方法
  12. HCIA 学习笔记 准备考试
  13. 想查看实时卫星影像?最近一周就不错了
  14. MATLAB(1)---将mat文件转换为csv文件
  15. 原生js打印插件Print.js
  16. Oracle SQL 高版本相关
  17. Python基础学习笔记:匿名函数
  18. 名帖380 张弼 草书《草书帖选》
  19. pc模式 华为mate30_华为Mate30系列10个隐藏黑科技
  20. java两个字符串 相隔天数,计算两个日期之间相隔天数

热门文章

  1. c语言gl函数,R语言:gl()函数
  2. SHGetFileInfo 报错 异常 问题
  3. 基于javaee的养老保险管理系统
  4. 西电微电子考研初试经验贴
  5. 计算机操做系统(十二):进程同步和互斥
  6. Shopee申请开店需要审核吗?
  7. 汇编语言的一些相关资料(上机或者实验)
  8. vue尚品汇商城项目-day00【项目介绍:此项目是基于vue2的前台电商项目和后台管理系统】
  9. CSS二(复合选择器)
  10. python如何求每一行的均值_计算每X行数的平均值