How collections.deque works?

 
 

前言:在 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 ,图文不怎么有关系列

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

  1. C、C++、C#、Java、php、python语言的内在特性及区别

    C.C++.C#.Java.PHP.Python语言的内在特性及区别: C语言,它既有高级语言的特点,又具有汇编语言的特点,它是结构式语言.C语言应用指针:可以直接进行靠近硬件的操作,但是C的指针操作 ...

  2. 使用深度学习检测TOR流量——本质上是在利用报文的时序信息、传输速率建模

    from:https://www.jiqizhixin.com/articles/2018-08-11-11 可以通过分析流量包来检测TOR流量.这项分析可以在TOR 节点上进行,也可以在客户端和入口 ...

  3. ARIMA模型——本质上是error和t-?时刻数据差分的线性模型!!!如果数据序列是非平稳的,并存在一定的增长或下降趋势,则需要对数据进行差分处理!ARIMA(p,d,q)称为差分自回归移动平均模型,AR是自回归, p为自回归项; MA为移动平均,q为移动平均项数,d为时间序列成为平稳时所做的差分次数

    https://www.cnblogs.com/bradleon/p/6827109.html 文章里写得非常好,需详细看.尤其是arima的举例! 可以看到:ARIMA本质上是error和t-?时刻 ...

  4. DQN 处理 CartPole 问题——使用强化学习,本质上是训练MLP,预测每一个动作的得分

    代码: # -*- coding: utf-8 -*- import random import gym import numpy as np from collections import dequ ...

  5. python模块介绍和 import本质

    模块的定义: 用来从逻辑上组织python代码(变量,函数,类,逻辑:实现一个功能),本质上就是.py结尾的python文件. 包的定义: 用来从逻辑上组织模块的,本质上就是一个目录.(必须有一个__ ...

  6. python学习笔记(5)—— tuple 本质探究

    >>> t=(1,2,3,['a','b','c'],4,5) >>> t[3][0]='x' >>> t (1, 2, 3, ['x', 'b' ...

  7. Jsp与servlet本质上的区别

    1.jsp经编译后就变成了Servlet.(JSP的本质就是Servlet,JVM只能识别java的类,不能识别JSP的代码,Web容器将JSP的代码编译成JVM能够识别的java类)2.jsp更擅长 ...

  8. 如何解决Python脚本在Linux和Windows上的格式问题

    python是一种对缩进有严格要求的语言, Python脚本可以使用非常多的工具进行编写,笔者在Linux系统使用JEdit进行Python脚本编写,由于在Linux编写脚本比较痛苦,比如想一眼看出相 ...

  9. jQuery的$.ajax方法响应数据类型有哪几种?本质上原生ajax响应数据格式有哪几种,分别对应哪个属性?

    jQuery的$.ajax方法响应数据类型有:xml.html.script.json.jsonp.text 本质上原生ajax响应数据格式只有2种:xml和text,分别对应xhr.response ...

随机推荐

  1. 转载: 我如何使用 Django + Vue.js 快速构建项目

    原文链接: https://www.ctolib.com/topics-109796.html 正文引用如下 引言 大U的技术课堂 的新年第一课,祝大家新的一年好好学习,天天向上:) 本篇将手把手教你 ...

  2. 选redis还是memcache,源码怎么说

    转自: https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651961272&idx=1&sn=79ad515b01 ...

  3. Echarts的提示(Tooltip)显示额外内容

    官方一般都是: 而我们通常是需要显示额外内容的,比如这样 其中Tooltip也是参考了网友的写法 option = { tooltip: { trigger: 'axis', formatter: f ...

  4. Django框架之DRF APIView Serializer

    一.APIView 我们在使用DjangoRestfulFramework的时候会将每个视图类继承APIView,取代原生Django的View类 APIView的流程分析: rest_framewo ...

  5. day08——文件操作

    day08 文件操作: open() :打开 f (文件句柄)= open("文件的路径(文件放的位置)",mode="操作文件的模式",encoding=&q ...

  6. Python中的sync和wait函数的使用

    转自这篇博文,备忘: https://blog.csdn.net/Likianta/article/details/90123678 https://www.cnblogs.com/xinghun85 ...

  7. Python入门 .变量 常量 基础数据类型 用户输入 流程控制语句 小练习题

    # 2.name = input(“>>>”)通过代码来验证name变量是什么数据类型?--str name = input(">>>") pr ...

  8. jdbc 简单示例和优缺点

    一个使用JDBC的例子: Class.forName("com.mysql.cj.jdbc.Driver"); //加载驱动 Connection connection = Dri ...

  9. python_操作linux上的mysql

    在编写初期,遇见一个问题,发现怎么连接不上mysql,一直报错1045: 最后发现,只要下面的,连接写正确,不会出现这个问题, 只要你保证你的user.pwd是正确的, import pymysqld ...

  10. HttpClient参观记:.net core 2.2 对HttpClient到底做了神马

    .net core 于 10月17日发布了 ASP.NET Core 2.2.0 -preview3,在这个版本中,我看到了一个很让我惊喜的新特性:HTTP Client Performance Im ...