首页 > 编程语言 >python deque的内在实现 本质上就是双向链表所以用于stack、队列非常方便

python deque的内在实现 本质上就是双向链表所以用于stack、队列非常方便

时间:2023-05-31 10:31:39浏览次数:52  
标签:deque return python len 链表 rightblock NULL block

How collections.deque works?

Cosven

 

 

前言:在 Python 生态中,我们经常使用 collections.deque 来实现栈、队列这些只需要进行头尾操作的数据结构,它的 append/pop 操作都是 O(1) 时间复杂度。list 的 pop(0) 的时间复杂度是 O(n), 在这个场景中,它的效率没有 deque 高。那 deque 内部是怎样实现的呢? 我从 GitHub 上挖出了 CPython collections 模块的第二个 commit 的源码。

dequeobject 对象定义

注释写得优雅了,无法进行更加精简的总结。

/* The block length may be set to any number over 1.  Larger numbers
 * reduce the number of calls to the memory allocator but take more
 * memory.  Ideally, BLOCKLEN should be set with an eye to the
 * length of a cache line.
 */

#define BLOCKLEN 62
#define CENTER ((BLOCKLEN - 1) / 2)

/* A `dequeobject` is composed of a doubly-linked list of `block` nodes.
 * This list is not circular (the leftmost block has leftlink==NULL,
 * and the rightmost block has rightlink==NULL).  A deque d's first
 * element is at d.leftblock[leftindex] and its last element is at
 * d.rightblock[rightindex]; note that, unlike as for Python slice
 * indices, these indices are inclusive on both ends.  By being inclusive
 * on both ends, algorithms for left and right operations become
 * symmetrical which simplifies the design.
 *
 * The list of blocks is never empty, so d.leftblock and d.rightblock
 * are never equal to NULL.
 *
 * The indices, d.leftindex and d.rightindex are always in the range
 *     0 <= index < BLOCKLEN.
 * Their exact relationship is:
 *     (d.leftindex + d.len - 1) % BLOCKLEN == d.rightindex.
 *
 * Empty deques have d.len == 0; d.leftblock==d.rightblock;
 * d.leftindex == CENTER+1; and d.rightindex == CENTER.
 * Checking for d.len == 0 is the intended way to see whether d is empty.
 *
 * Whenever d.leftblock == d.rightblock,
 *     d.leftindex + d.len - 1 == d.rightindex.
 *
 * However, when d.leftblock != d.rightblock, d.leftindex and d.rightindex
 * become indices into distinct blocks and either may be larger than the
 * other.
 */

typedef struct BLOCK {
    struct BLOCK *leftlink;
    struct BLOCK *rightlink;
    PyObject *data[BLOCKLEN];
} block;

typedef struct {
    PyObject_HEAD
    block *leftblock;
    block *rightblock;
    int leftindex;  /* in range(BLOCKLEN) */
    int rightindex; /* in range(BLOCKLEN) */
    int len;
    long state; /* incremented whenever the indices move */
    PyObject *weakreflist; /* List of weak references */
} dequeobject;

下面是我为 Block 结构体画的一个图

+----------------------------------------+
                |          data: 62 objects              |
 +----------+   |                                        |   +-----------+
 | leftlink |---|  | ... | Obj1 | Obj2 | Obj3 | ... |    |---| rightlink |
 +----------+   |           30     31     32             |   +-----------+
                +----------------------------------------+

创建一个 block

static block *
newblock(block *leftlink, block *rightlink, int len) {
    block *b;
    /* To prevent len from overflowing INT_MAX on 64-bit machines, we
     * refuse to allocate new blocks if the current len is dangerously
     * close.  There is some extra margin to prevent spurious arithmetic
     * overflows at various places.  The following check ensures that
     * the blocks allocated to the deque, in the worst case, can only
     * have INT_MAX-2 entries in total.
     */
    if (len >= INT_MAX - 2*BLOCKLEN) {
        PyErr_SetString(PyExc_OverflowError,
                "cannot add more blocks to the deque");
        return NULL;
    }
    b = PyMem_Malloc(sizeof(block));
    if (b == NULL) {
        PyErr_NoMemory();
        return NULL;
    }
    b->leftlink = leftlink;
    b->rightlink = rightlink;
    return b;
}

创建一个 dequeobject

  1. 创建一个 block
  2. 实例化一个 dequeobject Python 对象(这一块的内在逻辑目前我也不太懂)
  3. leftblock 和 rightblock 指针都指向这个 block
  4. leftindex 是 CENTER+1,rightindex 是 CENTER
  5. 初始化其他一些属性, len state 等

这个第一步和第四步都有点意思,第一步创建一个 block,也就是说, deque 对象创建的时候,就预先分配了一块内存。第四步隐约告诉我们, 当元素来的时候,它先会被放在中间,然后逐渐往头和尾散开。

static PyObject *
deque_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    dequeobject *deque;
    block *b;

    if (type == &deque_type && !_PyArg_NoKeywords("deque()", kwds))
        return NULL;

    /* create dequeobject structure */
    deque = (dequeobject *)type->tp_alloc(type, 0);
    if (deque == NULL)
        return NULL;

    b = newblock(NULL, NULL, 0);
    if (b == NULL) {
        Py_DECREF(deque);
        return NULL;
    }

    assert(BLOCKLEN >= 2);
    deque->leftblock = b;
    deque->rightblock = b;
    deque->leftindex = CENTER + 1;
    deque->rightindex = CENTER;
    deque->len = 0;
    deque->state = 0;
    deque->weakreflist = NULL;

    return (PyObject *)deque;
}

deque.append 实现

步骤:

  1. 如果 rightblock 可以容纳更多的元素,则放在 rightblock 中
  2. 如果不能,就新建一个 block,然后更新若干指针,将元素放在更新后的 rightblock 中
static PyObject *
deque_append(dequeobject *deque, PyObject *item)
{
    deque->state++;
    if (deque->rightindex == BLOCKLEN-1) {
        block *b = newblock(deque->rightblock, NULL, deque->len);
        if (b == NULL)
            return NULL;
        assert(deque->rightblock->rightlink == NULL);
        deque->rightblock->rightlink = b;
        deque->rightblock = b;
        deque->rightindex = -1;
    }
    Py_INCREF(item);
    deque->len++;
    deque->rightindex++;
    deque->rightblock->data[deque->rightindex] = item;
    Py_RETURN_NONE;
}

看了 append 实现后,我们可以自行脑补一下 pop 和 popleft 的实现。

小结

deque 内部将一组内存块组织成双向链表的形式,每个内存块可以看成一个 Python 对象的数组, 这个数组与普通数据不同,它是从数组中部往头尾两边填充数据,而平常所见数组大都是从头往后。 得益于 deque 这样的结构,它的 pop/popleft/append/appendleft 四种操作的时间复杂度均是 O(1), 用它来实现队列、栈数据结构会非常方便和高效。但也正因为这样的设计, 它不能像数组那样通过 index 来访问、移除元素。链表 + 数组、或者链表 + 字典 这样的设计在实践中有很广泛的应用,比如 LRUCache, LFUCache,有兴趣的同鞋可以继续探索。

  • PS1: LRUCache 在面试中不要太常见
  • PS2: 出 LFUCache 题的面试官都是变态
  • PS3: 头图来自 quora ,图文不怎么有关系列

标签:deque,return,python,len,链表,rightblock,NULL,block
From: https://blog.51cto.com/u_11908275/6384779

相关文章

  • python 校验 ipv4 ipv6 格式是否正确,是否属于某网段
    使用python自带的ipaddress模块一、ipv4importipaddress#判断ipv4地址格式是否正确如:ip="192.168.1.101"ip=ipaddress.IPv4Address(ipv4)#判断subnet地址格式是否正确如:subnet="192.168.1.0/24"network=ipaddress.IPv4Network(subnet)#判断ipv4......
  • python 切片
    Python列表切片Python中符合序列的有序序列都支持切片(slice),例如列表,字符串,元组。切片操作基本表达式:object[start_index:end_index:step]切片表达式包含两个":",用于分隔三个参数(start_index、end_index、step),当只有一个":"时,默认第三个参数step=1。start_index:表示起始索......
  • Python 实现进度条
    Python实现进度条1、案例一代码importsysimporttimedefprogress_bar():foriinrange(1,101):print("\r",end="")print("Downloadprogress:{}%:".format(i),"▋"*(i//2),end="")......
  • 【视频】支持向量机算法原理和Python用户流失数据挖掘SVM实例
    全文链接:http://tecdat.cn/?p=32604原文出处:拓端数据部落公众号分析师:BaileyZheng和Lijie Zhang即使是同一种植物,由于生长的地理环境的不同,它们的特征会有所差异。例如鸢尾花,可分为山鸢尾、杂色鸢尾、维吉尼亚鸢尾。假设此时您得到了一朵鸢尾花,如何判断它属于哪一类呢?支......
  • Python信贷风控模型:Adaboost,XGBoost,SGD, SVC,随机森林, KNN预测信贷违约支付|附代码
    全文链接:http://tecdat.cn/?p=26184最近我们被客户要求撰写关于信贷风控模型的研究报告,包括一些图形和统计输出。在此数据集中,我们必须预测信贷的违约支付,并找出哪些变量是违约支付的最强预测因子?以及不同人口统计学变量的类别,拖欠还款的概率如何变化?有25个变量:ID: 每个客户......
  • 【视频】风险价值VaR原理与Python蒙特卡罗Monte Carlo模拟计算投资组合实例|附代码数
    原文链接:http://tecdat.cn/?p=22862 最近我们被客户要求撰写关于风险价值的研究报告,包括一些图形和统计输出。风险价值(VaR)是一种统计数据,用于量化公司、投资组合在特定时间范围内可能发生的财务损失程度什么是风险价值(VaR)?该指标最常被投资银行和商业银行用来确定其机构......
  • python二维数组切片
    随堂测试这道题错了,于是怒而发blog,是分割维度的标识符,如果对象是二维数组,则切片应当是x[,]的形式,逗号之前和之后分别表示对象的第0个维度和第1个维度;如果对象是三维数组,则切片应当是x[,,],里面有两个逗号,分割出三个间隔,三个间隔的前、中和后分别表示对象的第0、1、2个维度。每个......
  • python内置库--json
    关于jsonJSON是一种按照JavaScript对象语法的数据格式相关介绍https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/JSON很多网页和app前后端数据交换的数据的格式就是json,打开F12或者抓包工具就可以看到py的json模块常用函数相关函数介绍json.dumps()......
  • python 操作 xlsx
    目录读取/写入:openpyxldemo1读取/写入:openpyxldemo1importopenpyxlimportos#创建exceldefwrite_excel_xlsx(path,sheet_name,value):ifnotos.path.exists(path):write_new_excel_xlsx(path,sheet_name,value)else:append_write_exc......
  • Python程序与设计
    Python学习笔记2-27在命令行窗口中启动的Python解释器中实现在Python自带的IDLE中实现print("Helloworld")编码规范每个import语句只导入一个模块,尽量避免一次导入多个模块不要在行尾添加分号“:”,也不要用分号将两条命令放在同一行建议每行不超过80个字符使用必要的空行可以增加代......