数据结构 | 双向队列

395 阅读5分钟

双向队列

双向队列是一种前后两端都可以添加数据(入队)和删除数据(出队)的有序线性表

相对于单向队列的只能队尾入队队首出队,提供了更高灵活性

双向队列常用操作

方法名描述时间复杂度
push_first()队首入队O(1)
push_last()队尾入队O(1)
pop_first()队首出队O(1)
pop_last()队尾出队O(1)
peek_first()获取队首元素O(1)
peek_last()获取队尾元素O(1)

不同的编程语言实现的具体方法名可能有所区别

体验双向队列,可以直接使用编程语言已实现的双向队列类

# 导入python已实现的双向队列类
from collections import deque

# 实例化
deq = deque()

# 队尾入队
deq.append(1)
deq.append(2)
# 队首入队
deq.appendleft(3)
deq.appendleft(4)
print(deq)

# 队首元素
print(deq[0])
# 队尾元素
print(deq[-1])

# 队尾出队
deq.pop()
# 队首出队
deq.popleft()
print(deq)
deque([4, 3, 1, 2])
4
2
deque([3, 1])

双向队列可以看作是一个特殊的队列,兼具栈于单向队列特点

  • 栈是一种后进先出(LIFO)的数据结构,只允许在一端进行插入和删除操作
  • 队列是一种先进先出(FIFO)的数据结构,只允许在一端插入,在另一端删除

而双向队列则可以同时实现这两种功能,既可用于队列,也可用于栈,所以称为“双向”队列

双向队列的实现

选择用链表或数组作为底层数据结构

基于双向链表实现双向队列

通常使用双向链表实现双向队列,这也双端插入和删除的情况时间复杂度都为O(1),但如果要删除中间节点则需要O(n)复杂度,但是也很少需要删除中间节点,所以在大部分情况下效率还是很高的

将双向链表的头结点和尾节点分别视作双向队列的队首和队尾,实现两端添加和删除的操作

class ListNode:
    """双向链表"""
    def __init__(self, val):
        self.val: int = val
        self.next: ListNode | None = None  # 后继节点
        self.prev: ListNode | None = None  # 前驱节点

class LinkedListDeque:
    """基于双向链表实现双向队列"""
    def __init__(self):
        self._front = None
        self._rear = None
        self._size = 0

    def size(self):
        """队列长度"""
        return self._size

    def is_empty(self):
        return self._size == 0

    def push(self, num, is_front=False):
        # 构造节点
        node = ListNode(num)
        # 队空,首尾一致
        if self.is_empty():
            self._front = self._rear = node
        elif is_front: # 队首入队
            self._front.prev = node
            node.next = self._front
            self._front = node
        else: # 队尾入队
            self._rear.next = node
            node.prev = self._rear
            self._rear = node
        self._size += 1

    def push_first(self, num):
        """队首入队"""
        self.push(num, True)

    def push_last(self, num):
        """队尾入队"""
        self.push(num)
        
    def pop(self, is_front=False):
        if self.is_empty():
            raise IndexError('出队异常,队列为空')
        if is_front: # 队首
            val = self._front.val
            fnext = self._front.next
            # 头结点后移一个
            if not fnext:
                fnext.prev = None
                self._front.next = None
            self._front = fnext
        else:
            val = self._rear.val
            rprev = self._rear.prev
            # 尾结点前移一个
            if not rprev:
                rprev.next = None
                self._rear.prev = None
            self._rear = rprev
        # 出队长度-1
        self._size -= 1
        return val

    def pop_first(self):
        """队首出队"""
        return self.pop(True)

    def pop_last(self):
        """队尾出队"""
        return self.pop()

    def peek_first(self):
        """访问队首"""
        if self.is_empty():
            raise IndexError('访问队首异常,队空')
        return self._front.val

    def peek_last(self):
        """访问队尾"""
        if self.is_empty():
            raise IndexError('访问队首异常,队空')
        return self._rear.val

    def to_array(self):
        """返回数组用于打印"""
        node = self._front
        res = [0] * self.size()
        for i in range(self.size()):
            res[i] = node.val
            node = node.next
        return res
# test
deq = LinkedListDeque()

# 队尾入队
deq.push_last(1)
deq.push_last(2)
# 队首入队
deq.push_first(3)
deq.push_first(4)
print(deq.to_array())

# 队首元素
print(deq.peek_first())
# 队尾元素
print(deq.peek_last())

# 队尾出队
deq.pop_last()
# 队首出队
deq.pop_first()
print(deq.to_array())
[4, 3, 1, 2]
4
2
[3, 1]

数组实现双向队列

使用环形数组来实现双向队列,避免假溢出现象出现

class ArrayDeque:
    """基于环形数组实现的双向队列"""
    def __init__(self, capacity):
        # 创建时初始化数组容量
        self._nums = [0] * capacity
        # 头指针
        self._front = 0
        # 总长度
        self._size = 0

    def capacity(self):
        """当前容量大小"""
        return len(self._nums)

    def size(self):
        """获取数组长度"""
        return self._size

    def is_empty(self):
        return self._size == 0

    def index(self, i):
        """计算环形数组索引"""
        # 取余操作实现数组首位相连
        return (i + self.capacity()) % self.capacity()

    def push_first(self, num):
        """队首入队"""
        if self._size == self.capacity():
            print('队满')
            return
        # 正常入队
        self._front =self.index(self._front -1) # 首指针前移一位
        self._nums[self._front] = num # 首指针地址赋值
        self._size += 1  # 长度+1

    def push_last(self, num):
        """队尾入队"""
        if self._size == self.capacity():
            print('队满')
            return
        # 正常入队
        rear = self.index(self._front + self._size) # 计算尾指针
        self._nums[rear] = num # 尾指针处赋值
        self._size += 1

    def pop_first(self):
        """队首出队"""
        num = self.peek_first() # 保存首元素
        self._front = self.index(self._front + 1) # 首指针后移一位
        self._size -= 1 # 长度-1
        return num

    def pop_last(self):
        """队尾出队"""
        num = self.peek_last()
        self._size -= 1

    def peek_first(self):
        """访问队首元素"""
        if self.is_empty():
            print('队空')
            return
        return self._nums[self._front]

    def peek_last(self):
        """访问队尾元素"""
        if self.is_empty():
            print('队空')
            return
        rear = self.index(self._front + self._size - 1) # 尾指针位置
        return self._nums[rear]

    def to_array(self):
        """返回数组用于打印"""
        res = []
        for i in range(self._size):
            res.append(self._nums[self.index(self._front + i)])
        return res
# test
deq = ArrayDeque(10)

# 队尾入队
deq.push_last(1)
deq.push_last(2)
# 队首入队
deq.push_first(3)
deq.push_first(4)
print(deq.to_array())

# 队首元素
print(deq.peek_first())
# 队尾元素
print(deq.peek_last())

# 队尾出队
deq.pop_last()
# 队首出队
deq.pop_first()
print(deq.to_array())
[4, 3, 1, 2]
4
2
[3, 1]

优缺点

优点:

  • 支持在队列两端插入和删除元素,能够快速地进行队列和栈的操作
  • 可以模拟更复杂的数据结构,例如双端队列可以用来实现双端搜索、某些情况下的最短路径算法等
  • 在某些操作场景下比普通队列和栈更高效

缺点:

  • 双向队列需要使用更多内存空间来维护引用(指针),比普通队列和栈更占内存
  • 插入和删除元素操作可能会引起引用(指针)的移动,导致时间复杂度比普通队列和栈更高
  • 双向队列难以维护某些特定的性质,例如优先队列的排序特性,需要额外的实现方式