2.数组与列表-python

什么是数组?

数组是线性表数据结构。用连续的内存空间存储相同类型的数据。

  • 线性表:线性表是数据排成一条线一样的结构。每个线性表上的数据最多只有前和后两个方向。
    包括:数组,链表、队列、栈。
  • 非线性表:数据之间并不是简单的前后关系。
    包括:二叉树、堆、图等。
  • 连续的内存空间和相同类型的数据:使数组支持“随机访问”。但在数组中删除、插入数据时,需要做大量的数据搬移工作。

数组如何实现随机访问?

C 语言代码: int[] tmp = new int[4],这个数组在内存中连续放置:

image

插入操作

  • 在数组末尾插入元素,不需要移动数据,时间复杂度为 O(1)。
  • 在数组开头插入元素,那所有的数据都需要依次往后移动一位,所以最坏时间复杂度是 O(n)。
  • 假设每个位置插入元素概率相同,平均情况时间复杂度为 (1+2+…n)/n=O(n)。

如果元素无序,可直接换位:

删除操作

  • 删除数组末尾数据:则最好情况时间复杂度为 O(1);
  • 删除开头的数据:则最坏情况时间复杂度为 O(n);
  • 平均情况时间复杂度也为 O(n)。

预删除思想:JVM 标记清除垃圾回收算法

改进数组

编程语言封装了数组,比如 Java 的 ArrayList, Python 的 List ,可实现自动扩容,多种数据类型组合。

如果你是底层工程师,需要极致的比如开发网络框架,性能的优化需要做到极致,这个时候数组就会优于容器,成为首选。

class Array:
    def __init__(self, capacity) -> None:
        self.data = [-1]*capacity
        self.count = 0
        self.n = capacity

    def insert(self, location, value):
        if self.n == self.count:
            return False

        if location < 0 or location > self.count:
            return False

        for i in range(self.count, location, -1):
            self.data[i] = self.data[i-1]
        
        self.data[location] = value
        self.count += 1
        return True

    def find(self, location):
        if location < 0 or location >= self.count:
            return -1
        
        return self.data[location]
    

    
    def delete(self, location):
        if location < 0 or location >= self.count:
            return False
        
        for i in range(location + 1, self.count):
            self.data[i-1] = self.data[i]
        
        self.count -= 1
        return True
    
        

def test_demo():
    array = Array(5)
    array.insert(0, 1)
    array.insert(0, 2)
    array.insert(1, 3)
    array.insert(2, 4)
    array.insert(4, 5)
    
    # 判断插入不成功
    assert not array.insert(0, 100)
    assert array.find(0) == 2
    assert array.find(2) == 4
    assert array.find(4) == 5
    assert array.find(10) == -1
    assert array.count == 5
    removed = array.delete(4)
    assert removed
    assert array.find(4) == -1
    removed = array.delete(10)
    assert not removed
    # 2 3 4 1 5
    assert array.data == [2, 3, 4, 1, 5]

if __name__ == '__main__':
    test_demo()

python list 源码解析

本篇文章描述了 CPython 中 list 的实现方式。

C 语言用结构体表示 List 对象

C 语言使用结构体实现 list 对象,结构体代码如下。

typedef struct {
    PyObject_VAR_HEAD
    PyObject **ob_item; //指向 list 中的对象
    Py_ssize_t allocated; //内存分配的插槽
} PyListObject;

List 初始化

I = [] 为例

list 的数量是指 len(l)。分配的槽位数量是指在内存中实际分配的数量。通常情况,内存中分配的数量要大于 list 的数量。这是为了当添加新元素时,避免内存再分配。

Append

当运行l.append(1)时, CPython 将调用app1()

list_resize() 会故意分配更多的内存,避免被多次调用。分配内存大小增加:0, 4, 8, 16, 25, 35, 46, 58, 72, 88, …

第一次分配了 4 个槽位,I[0] 指向了数字对象 1 。正方形虚线表示未使用过的槽位。追加操作的均摊复杂度为 O(1) 。

均摊时间复杂度是平均时间复杂度的一种,是一种简化的计算方法。

image

继续追加元素:l.append(2)。调用 list_resize 实现 n + 1 = 2。由于分配了四个空间,不需要分配内存。当再向列表追加两个数字时,l.append(3), l.append(4),如下图如示:

Python lists

Insert

在位置 1 插入整型 5 ,即调用 python 的 l.insert(1, 5) 。CPython 会调用 ins1()

插入操作需要将剩余元素向右迁移:

image

上图虚线表示未使用的槽位(slots),分配了 8 个槽位,但 list 的长度只有 5 。 insert 的时间复杂度为 O(n)。

pop

弹出列表的最后一个元素使用 l.pop(),CPython 使用 listpop() 实现这个过程。如果新内存大小少于分配大小的一半, listpop() 将调用 list_resize 减少 list 内存。

Pop 的时间复杂度是 O(1)。

image

注意,此时槽位 4 仍然指向整型 4 ,但是 list 的大小却是 4 。只有 pop 更多的元素才能调用 list_resize() 减少内存,如果再 pop 一个元素, size - 1 = 4 - 3 = 3, 3 小于分配槽位的一半 8/2 = 4 。所以 list 收缩到 6 个槽位, list 的大小为 3 。虽然槽位 3 和 4 依旧指向整型对象,但是整体大小变成了 3 。

image

remove

Python 可以用 remove 删除指定元素:l.remove(5)。此时将调用 listremove()

CPython 调用 list_ass_slice() 函数对列表进行切分并删除元素。当在位置 1 移除元素 5 时,低偏移(low offset)是 1 ,高偏移(high offset)是 2 :

remove 时间复杂度是 O(n)。

文章参考:

http://www.laurentluce.com/posts/python-list-implementation/