It doesn't matter which end you use. Both will be equally optimal. Unlike a list, which only offers O(1) append and pop operations on the right/top of the list, append and pop operations are guaranteed to be O(1) on both ends of the deque. If they weren't, there'd be no point to its existence and we might as well use a list.
Looking at the source code for CPython 3.8, the methods pop
and popleft
are almost identical:
static PyObject *
deque_pop(dequeobject *deque, PyObject *unused)
{
PyObject *item;
block *prevblock;
if (Py_SIZE(deque) == 0) {
PyErr_SetString(PyExc_IndexError, "pop from an empty deque");
return NULL;
}
item = deque->rightblock->data[deque->rightindex];
deque->rightindex--;
Py_SIZE(deque)--;
deque->state++;
if (deque->rightindex < 0) {
if (Py_SIZE(deque)) {
prevblock = deque->rightblock->leftlink;
assert(deque->leftblock != deque->rightblock);
freeblock(deque->rightblock);
CHECK_NOT_END(prevblock);
MARK_END(prevblock->rightlink);
deque->rightblock = prevblock;
deque->rightindex = BLOCKLEN - 1;
} else {
assert(deque->leftblock == deque->rightblock);
assert(deque->leftindex == deque->rightindex+1);
/* re-center instead of freeing a block */
deque->leftindex = CENTER + 1;
deque->rightindex = CENTER;
}
}
return item;
}
static PyObject *
deque_popleft(dequeobject *deque, PyObject *unused)
{
PyObject *item;
block *prevblock;
if (Py_SIZE(deque) == 0) {
PyErr_SetString(PyExc_IndexError, "pop from an empty deque");
return NULL;
}
assert(deque->leftblock != NULL);
item = deque->leftblock->data[deque->leftindex];
deque->leftindex++;
Py_SIZE(deque)--;
deque->state++;
if (deque->leftindex == BLOCKLEN) {
if (Py_SIZE(deque)) {
assert(deque->leftblock != deque->rightblock);
prevblock = deque->leftblock->rightlink;
freeblock(deque->leftblock);
CHECK_NOT_END(prevblock);
MARK_END(prevblock->leftlink);
deque->leftblock = prevblock;
deque->leftindex = 0;
} else {
assert(deque->leftblock == deque->rightblock);
assert(deque->leftindex == deque->rightindex+1);
/* re-center instead of freeing a block */
deque->leftindex = CENTER + 1;
deque->rightindex = CENTER;
}
}
return item;
}
And the same is true for appends:
static inline int
deque_append_internal(dequeobject *deque, PyObject *item, Py_ssize_t maxlen)
{
if (deque->rightindex == BLOCKLEN - 1) {
block *b = newblock();
if (b == NULL)
return -1;
b->leftlink = deque->rightblock;
CHECK_END(deque->rightblock->rightlink);
deque->rightblock->rightlink = b;
deque->rightblock = b;
MARK_END(b->rightlink);
deque->rightindex = -1;
}
Py_SIZE(deque)++;
deque->rightindex++;
deque->rightblock->data[deque->rightindex] = item;
if (NEEDS_TRIM(deque, maxlen)) {
PyObject *olditem = deque_popleft(deque, NULL);
Py_DECREF(olditem);
} else {
deque->state++;
}
return 0;
}
static inline int
deque_appendleft_internal(dequeobject *deque, PyObject *item, Py_ssize_t maxlen)
{
if (deque->leftindex == 0) {
block *b = newblock();
if (b == NULL)
return -1;
b->rightlink = deque->leftblock;
CHECK_END(deque->leftblock->leftlink);
deque->leftblock->leftlink = b;
deque->leftblock = b;
MARK_END(b->leftlink);
deque->leftindex = BLOCKLEN;
}
Py_SIZE(deque)++;
deque->leftindex--;
deque->leftblock->data[deque->leftindex] = item;
if (NEEDS_TRIM(deque, deque->maxlen)) {
PyObject *olditem = deque_pop(deque, NULL);
Py_DECREF(olditem);
} else {
deque->state++;
}
return 0;
}
Deques in CPython allocate and deallocate memory in blocks to minimize heap access and improve cache locality (sequential elements in an array are likely to belong to the same cache block). These methods all allocate or deallocate memory as necessary and increment/decrement a few indices. All constant time.