双端队列是另一个线性数据结构。虽然它也是一种受限线性表,但与栈和队列不同的是,双端队列的限制很少,它的基本操作也是线性表操作的子集,但从数据类型的角度来讲,它们与线性表又有着巨大的不同。本节将介绍双端队列的定义及其不同实现,并且给出双端队列的一些实际应用。
通过本节学习,应掌握以下内容:
双端队列 (double-end queue
, deque
) 也是插入和删除操作分别被限制在序列两端的线性表,但与栈和队列不同的是,双端队列的限制很少,对于双端队列而言,队尾 (rear
) 和队头 (front
) 均允许插入元素和删除元素。新元素既可以被添加到队头, 也可以被添加到队尾。同理,已有的元素也能从任意一端移除。某种意义上,可以认为双端队列是栈和队列的结合。
尽管双端队列有栈和队列的很多特性,但是它并不要求按照这两种数据结构所限定的 LIFO
原则和 FIFO
原则操作元素。
除了添加和移除元素外,双端队列还具有初始化、判队空和求队长度等辅助操作。具体而言,双端队列的抽象数据类型定义如下:
ADT Deque:
数据对象: D = a i ∣ a i ∈ D a t a T y p e , i = 1 , 2 , . . . , n , n ≥ 0 D={a_i|a_i∈DataType, i=1,2,...,n,n\geq0} D=ai∣ai∈DataType,i=1,2,...,n,n≥0
数据关系: R = < a i , a i + 1 > ∣ a i , a i + 1 ∈ D , i = 1 , 2 , . . . , n − 1 R={|a_i,a_{i+1}∈D,i=1,2,...,n-1} R=<ai,ai+1>∣ai,ai+1∈D,i=1,2,...,n−1
a 1 a_1 a1为队头元素, a n a_n an为队尾元素
基本操作:
1. __itit__(): 初始化双端队列
创建一个空双端队列
2. size(): 求取并返回双端队列中所含元素的个数 n
若双端队列为空,则返回整数0
3. isempty(): 判断是否为空双端队列
判断双端队列中是否存储元素
4. addfront(data): 双端队列队头添加元素
将元素 data 插入队头
5. addrear(data): 双端队列队尾添加元素
将元素 data 插入队尾
6. removefront(): 删除双端队列队头元素
删除并返回队头元素
7. removerear(): 删除双端队列队尾元素
删除并返回队尾元素
和普通队列一样,双端队列同样有顺序存储和链式存储两种存储表示方式。
类似于顺序队列,双端队列的顺序存储结构利用一组地址连续的存储单元依次存放从双端队列头到双端队列尾的元素,同时需要用两个指针 front
和 rear
分别指示队列头元素和队列尾元素的位置。初始化空双端队列时,front=rear=0
,当元素入队时,rear 加 1
,而元素出队时,front 加 1
,同时为了重复利用空闲空间,我们将顺序双端队列假设尾环状空间,最后一个空间和第一个空间视为连续空间(具体原因参考<顺序队列>):
同样顺序双端队列可以是固定长度和动态长度,当双端队列满时,定长顺序双端队列会抛出双端队列满异常,动态顺序双端队列则会动态申请空闲空间。
顺序双端队列的初始化需要 4 部分信息:deque
列表用于存储数据元素,max_size
用于存储 queue
列表的最大长度,以及 front
和 rear
分别用于记录队头元素和队尾元素的索引:
class Deque:
def __init__(self, max_size=6):
self.max_size = max_size
self.deque = [None] * self.max_size
self.front = 0
self.rear = 0
由于 front
和 rear
分别用于记录队头元素和队尾元素的索引,因此我们可以方便的计算出双端队列的长度;同时我们需要考虑双端队列为循环队列,front
可能大于 rear
,不能直接通过 rear-front
,我们需要利用公式计算解决此问题:
( r e a r − f r o n t + m a x _ s i z e ) % m a x _ s i z e (rear-front+max\_size) \% max\_size (rear−front+max_size)%max_size
Python
实现如下:
def size(self):
return (self.rear-self.front+self.max_size) % self.max_size
根据 front
和 rear
的值可以方便的判断双端队列是否为空:
def isempty(self):
return self.rear==self.front
根据 front
和 rear
的值可以方便的判断双端队列是否还有空余空间:
def isfull(self):
return ((self.rear+1) % self.max_size == self.front)
添加元素时,需要首先判断双端队列中是否还有空闲空间,然后根据双端队列为定长顺序双端队列或动态顺序双端队列,添加元素操作稍有不同:
[定长顺序双端队列的添加元素操作] 如果队满,则引发异常:
# 注意队头和队尾修改索引的添加元素的不同顺序
def addrear(self, data):
if not self.isfull():
self.deque[self.rear] = data
self.rear = (self.rear+1) % self.max_size
else:
raise IndexError("Full Deque Exception")
def addfront(self, data):
if self.isfull():
self.resize()
if self.isempty():
# 当双端队列
self.deque[self.rear] = data
self.rear = (self.rear+1) % self.max_size
else:
self.front = (self.front - 1 + self.max_size) % self.max_size
self.deque[self.front] = data
[动态顺序双端队列的添加元素操作] 如果双端队列满,则首先申请新空间,然后再执行添加操作:
def resize(self):
new_size = 2 * self.max_size
new_deque = [None] * new_size
d = new_size - self.max_size
for i in range(self.max_size):
new_deque[(self.front+i+d) % new_size] = self.deque[(self.front+i) % self.max_size]
self.deque = new_deque
self.front = (self.front+d) % new_size
self.max_size = new_size
# 注意队头和队尾修改索引的添加元素的不同顺序
def addrear(self, data):
if self.isfull():
self.resize()
self.deque[self.rear] = data
self.rear = (self.rear+1) % self.max_size
def addfront(self, data):
if self.isfull():
self.resize()
self.front = (self.front - 1 + self.max_size) % self.max_size
self.deque[self.front] = data
与动态顺序队列类似,我们同样需要考虑复制之后的索引,否则可能出现存在不能用的空闲空间:
添加元素的时间复杂度为 O ( 1 ) O(1) O(1)。虽然当动态顺序双端队列满时,原双端队列中的元素需要首先复制到新双端队列中,然后添加新元素,但参考《顺序表及其操作实现》中顺序表追加操作的介绍,由于 n
次添加元素操作的总时间 T ( n ) T(n) T(n) 与 O ( n ) O(n) O(n) 成正比,因此其摊销时间复杂度可以认为是 O ( 1 ) O(1) O(1)。
若双端队列不空,则删除并返回指定端元素:
# 注意队头和队尾修改索引的删除元素的不同顺序
def removefront(self):
if not self.isempty():
result = self.deque[self.front]
self.front = (self.front+1) % self.max_size
return result
else:
raise IndexError("Empty Deque Exception")
def removerear(self):
if not self.isempty():
self.rear = (self.rear - 1 + self.max_size) % self.max_size
result = self.deque[self.rear]
return result
else:
raise IndexError("Empty Deque Exception")
双端队列的另一种存储表示方式是使用链式存储结构,因此也常称为链双端队列,其中 addfront
操作和 addrear
操作分别是通过在链表头部和尾部插入元素来实现的,而 removefront
操作和 removerear
操作分别是通过从头部和尾部删除结点来实现的。为了降低在尾部删除结点的时间复杂度,接下来基于双向链表实现双端队列。
双端队列的结点实现与双向链表并无差别:
class Node:
def __init__(self, data=None):
self.data = data
self.next = None
def __str__(self):
return str(self.data)
双端队列的初始化函数中,使其队头指针 front
和队尾指针 rear
均指向 None
,并初始化双端队列长度:
class Deque:
def __init__(self):
self.front = None
self.rear = None
self.num = 0
返回 num
的值用于求取双端队列的长度,如果没有 num
属性,则需要遍历整个链表才能得到双端队列长度:
def size(self):
return self.num
根据双端队列的长度可以很容易的判断其是否为空双端队列:
def isempty(self):
return self.num <= 0
双端队列添加元素时,可以在队尾或队头插入新元素,因此需要修改 rear
和 front
指针,并且同时也要修改结点的 next
和 previous
指针;如果添加元素前双端队列为空,还需要进行相应处理:
def addrear(self, data):
node = Node(data)
# 如果添加元素前双端队列为空,则添加结点时,需要将front指针也指向该结点
if self.front is None:
self.rear = node
self.front = node
else:
node.previous = self.rear
self.rear.next = node
self.rear = node
self.num += 1
def addfront(self, data):
node = Node(data)
# 如果添加元素前双端队列为空,则添加结点时,需要将rear指针也指向该结点
if self.rear is None:
self.front = node
self.rear = node
else:
node.next = self.front
self.front.previous = node
self.front = node
self.num += 1
若双端队列不空,可以从删除队头或队尾元素并返回,删除操作需要更新队头指针 front
以及尾指针 rear
,同时也要修改结点的 next
和 previous
指针,若出队元素尾队中最后一个结点,还需要进行相应处理:
def removefront(self):
if self.isempty():
raise IndexError("Empty Queue Exception")
result = self.front.data
self.front = self.front.next
self.num -= 1
if self.isempty():
self.rear = self.front
else:
# 若删除操作完成后,双端队列不为空,将 front 指针的前驱指针指向 None
self.front.previous = None
return result
def removerear(self):
if self.isempty():
raise IndexError("Empty Queue Exception")
result = self.rear.data
self.rear = self.rear.previous
self.num -= 1
if self.isempty():
self.front = self.rear
else:
# 若删除操作完成后,双端队列不为空,将 rear 指针的后继指针指向 None
self.rear.next = None
return result
双端队列的不同实现对比与栈的不同实现类似,可以参考《栈及其操作实现》。
接下来,我们首先测试上述实现的双端队列,以验证操作的有效性,然后利用实现的基本操作来解决实际算法问题。
首先初始化一个顺序双端队列 deque
,然后测试相关操作:
# 初始化一个最大长度为5的双端队列
dq = Deque(5)
print('双端队列空?', dq.isempty())
for i in range(3):
print('队头添加元素:', 2*i)
dq.addfront(2*i)
print('队尾添加元素:', 2*i+1)
dq.addrear(2*i+1)
print('双端队列长度为:', dq.size())
for i in range(3):
print('队尾删除元素:', dq.removerear())
print('队头删除元素:', dq.removefront())
print('双端队列长度为:', dq.size())
测试程序输出结果如下:
双端队列空? True
队头添加元素: 0
队尾添加元素: 1
队头添加元素: 2
队尾添加元素: 3
队头添加元素: 4
队尾添加元素: 5
双端队列长度为: 6
队尾删除元素: 5
队头删除元素: 4
队尾删除元素: 3
队头删除元素: 2
队尾删除元素: 1
队头删除元素: 0
双端队列长度为: 0
首先初始化一个链双端队列 queue
,然后测试相关操作:
# 初始化新队列
dq = Deque()
print('双端队列空?', dq.isempty())
for i in range(3):
print('队头添加元素:', i)
dq.addfront(2*i)
print('队尾添加元素:', i+3)
dq.addrear(2*i+1)
print('双端队列长度为:', dq.size())
for i in range(3):
print('队尾删除元素:', dq.removerear())
print('队头删除元素:', dq.removefront())
print('双端队列长度为:', dq.size())
测试程序输出结果如下:
双端队列空? True
队头添加元素: 0
队尾添加元素: 3
队头添加元素: 1
队尾添加元素: 4
队头添加元素: 2
队尾添加元素: 5
双端队列长度为: 6
队尾删除元素: 5
队头删除元素: 4
队尾删除元素: 3
队头删除元素: 2
队尾删除元素: 1
队头删除元素: 0
双端队列长度为: 0
[1] 给定一字符串 string
(如:abamaba),检查其是否为回文。
使用双端队列可以快速检查一字符串是否为回文序列,只需要将字符串中字符依次入队,然后从双端队列两端依次弹出元素,对比它们是否相等:
def ispalindrome(string):
deque = Deque()
for ch in string:
deque.addfront(ch)
flag = True
while deque.size() > 1 and flag:
ch1 = deque.removefront()
ch2 = deque.removerear()
if ch1 != ch2:
flag = False
return flag
验证算法有效性:
print('abcba是否为回文序列:', ispalindrome('abcba'))
print('charaahc是否为回文序列:', ispalindrome('charaahc'))
结果输出如下:
abcba是否为回文序列: True
charaahc是否为回文序列: False
线性表基本概念
顺序表及其操作实现
单链表及其操作实现
双向链表及其操作实现
栈及其操作实现
队列及其操作实现