栈与队列

157 阅读12分钟

栈与队列

1. 栈

栈是一种遵循先入后出逻辑的数据结构,。只能在栈顶添加或者删除元素,因此栈可以看作一种受限制的数组或链表。

image.png

1.1. 栈的常用操作

一般而言,编程语言都会内置栈类 stack

方法描述时间复杂度
push()入栈至栈顶O(1)
pop()栈顶元素出栈O(1)
peek()访问栈顶元素O(1)
# 初始化栈
# Python 没有内置的栈类,可以把 list 当作栈来使用
stack: list[int] = []

# 元素入栈
stack.append(1)
stack.append(3)
stack.append(2)
stack.append(5)
stack.append(4)

# 访问栈顶元素
peek: int = stack[-1]

# 元素出栈
pop: int = stack.pop()

# 获取栈的长度
size: int = len(stack)

# 判断是否为空
is_empty: bool = len(stack) == 0
/* 初始化栈 */
stack<int> stack;

/* 元素入栈 */
stack.push(1);
stack.push(3);
stack.push(2);
stack.push(5);
stack.push(4);

/* 访问栈顶元素 */
int top = stack.top();

/* 元素出栈 */
stack.pop(); // 不输出返回值

/* 获取栈的长度 */
int size = stack.size();

/* 判断是否为空 */
bool empty = stack.empty();

1.2. 栈的其他实现

根据栈的定义可以得知,栈的实现可以基于链表或者数组实现

1.2.1. 基于链表的实现

在此实现栈时,将链表的头节点视为栈顶,尾节点视为栈底。入栈操作视作在链表的头节点插入元素,出栈视作删除头节点,入栈和出栈方法称为头插法。 image.png

image.png

image.png

class LinkedListStack:
	"""基于链表实现的栈"""
	
	def __init__(self):
	    """构造方法"""
	    self._peek: ListNode | None = None # 初始化栈顶为空
	    self._size: int = 0 # 初始化栈为空栈
	
	def size(self) -> int:
	    """获取栈的长度"""
	    return self._size
	
	def is_empty(self) -> bool:
	    """判断栈是否为空"""
	    return self._size == 0
	
	def push(self, val: int):
	    """入栈"""
	    node = ListNode(val)
	    node.next = self._peek #连接栈顶与新元素,头插法方法
	    self._peek = node
	    self._size += 1
	
	def pop(self) -> int:
	    """出栈"""
	    num = self.peek()
	    self._peek = self._peek.next
	    self._size -= 1
	    return num
	
	def peek(self) -> int:
	    """访问栈顶元素"""
	    if self.is_empty():
	        raise IndexError("栈为空")
	    return self._peek.val
	
	def to_list(self) -> list[int]:
	    """转化为列表用于打印"""
	    arr = []
	    node = self._peek
	    while node: #当前节点元素不为空
	        arr.append(node.val)
	        node = node.next
	    arr.reverse()
	    return arr
/* 基于链表实现的栈 */
class LinkedListStack {
  private:
    ListNode *stackTop; // 将头节点作为栈顶
    int stkSize;        // 栈的长度

  public:
    LinkedListStack() {
        stackTop = nullptr;
        stkSize = 0;
    }

    ~LinkedListStack() {
        // 遍历链表删除节点,释放内存
        freeMemoryLinkedList(stackTop);
    }

    /* 获取栈的长度 */
    int size() {
        return stkSize;
    }

    /* 判断栈是否为空 */
    bool isEmpty() {
        return size() == 0;
    }

    /* 入栈 */
    void push(int num) {
        ListNode *node = new ListNode(num);
        node->next = stackTop;   //头插法
        stackTop = node;
        stkSize++;
    }

    /* 出栈 */
    int pop() {
        int num = top();
        ListNode *tmp = stackTop;
        stackTop = stackTop->next;
        // 释放内存
        delete tmp;
        stkSize--;
        return num;
    }

    /* 访问栈顶元素 */
    int top() {
        if (isEmpty())
            throw out_of_range("栈为空");
        return stackTop->val;
    }

    /* 将 List 转化为 Array 并返回 */
    vector<int> toVector() {
        ListNode *node = stackTop;
        vector<int> res(size());
        for (int i = res.size() - 1; i >= 0; i--) {
            res[i] = node->val;
            node = node->next;
        }
        return res;
    }
};

1.2.2. 基于数组的实现

使用数组实现栈时,将数组的尾部作为栈顶。入栈与出栈操作分别对应在数组尾部添加元素与删除元素,时间复杂度都为 O(1) 。 然而普通的数组会有容量的限制,因此在实现栈时,主要通过动态数组进行实现,以满足入栈元素的增加。 image.png

image.png

image.png

class ArrayStack:
	"""基于数组实现的栈"""
	
	def __init__(self):
	    """构造方法"""
	    self._stack: list[int] = [] #初始化动态数组并作为一个栈,实际上,这里是直接初始化的一个列表
	
	def size(self) -> int:
	    """获取栈的长度"""
	    return len(self._stack)
	
	def is_empty(self) -> bool:
	    """判断栈是否为空"""
	    return self.size() == 0
	
	def push(self, item: int):
	    """入栈"""
	    self._stack.append(item)
	
	def pop(self) -> int:
	    """出栈"""
	    if self.is_empty():
	        raise IndexError("栈为空")
	    return self._stack.pop()
	
	def peek(self) -> int:
	    """访问栈顶元素"""
	    if self.is_empty():
	        raise IndexError("栈为空")
	    return self._stack[-1]
	
	def to_list(self) -> list[int]:
	    """返回列表用于打印"""
	    return self._stack
/* 基于数组实现的栈 */
class ArrayStack {
  private:
    vector<int> stack;

  public:
    /* 获取栈的长度 */
    int size() {
        return stack.size();
    }

    /* 判断栈是否为空 */
    bool isEmpty() {
        return stack.size() == 0;
    }

    /* 入栈 */
    void push(int num) {
        stack.push_back(num);
    }

    /* 出栈 */
    int pop() {
        int num = top();
        stack.pop_back();
        return num;
    }

    /* 访问栈顶元素 */
    int top() {
        if (isEmpty())
            throw out_of_range("栈为空");
        return stack.back();
    }

    /* 返回 Vector */
    vector<int> toVector() {
        return stack;
    }
};

1.2.3. 实现方法的对比

类型支持操作时间效率空间效率
基于链表支持栈的操作与其余操作速度更快,具有更加稳定的效率由于需要额外存储指针存在可能造成一定的空间浪费
基于数组支持栈的操作与其余操作在不超出数组容量时,较快,超出后需扩容,较慢。平均效率较快由于扩容的存在可能造成一定的空间浪费

1.3. 栈的应用

  • 浏览器中的后退与前进、软件中的撤销与反撤销。
  • 程序内存管理

2. 队列

队列(queue)是一种遵循先入先出规则的线性数据结构。队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。先入队和先出队的都是队首。 image.png

2.1. 队列的常见操作

方法描述时间复杂度
push()入队至队尾O(1)
pop()队首元素出队O(1)
peek()访问队首元素O(1)
一般编程语言中都有现成的队列类 queue
from collections import deque

# 初始化队列
# 在 Python 中,我们一般将双向队列类 deque 当作队列使用
# 虽然 queue.Queue() 是纯正的队列类,但不太好用,因此不推荐
que: deque[int] = deque()

# 元素入队
que.append(1)
que.append(3)
que.append(2)
que.append(5)
que.append(4)
'output = 13254'
# 访问队首元素
front: int = que[0]
'output = 1'

# 元素出队
pop: int = que.popleft()
'output = 1'
# 获取队列的长度
size: int = len(que)

# 判断队列是否为空
is_empty: bool = len(que) == 0
/* 初始化队列 */
queue<int> queue;

/* 元素入队 */
queue.push(1);
queue.push(3);
queue.push(2);
queue.push(5);
queue.push(4);

/* 访问队首元素 */
int front = queue.front();
'output = 1'
/* 元素出队 */
queue.pop();
'output = 1'
/* 获取队列的长度 */
int size = queue.size();

/* 判断队列是否为空 */
bool empty = queue.empty();

2.2. 队列的其他实现

队列和栈相同,也可以使用链表和数组实现,但是,数组依旧是使用动态数组来实现。

2.2.1. 基于链表的实现

将链表的“头节点”和“尾节点”分别视为“队首”和“队尾”,规定队尾仅可添加节点,队首仅可删除节点。这样可以保证添加和删除时的时间复杂度均为 O(1) image.png

image.png

image.png

class LinkedListQueue:
	"""基于链表实现的队列"""
	
	def __init__(self):
	    """构造方法"""
	    self._front: ListNode | None = None  # 头节点 front
	    self._rear: ListNode | None = None  # 尾节点 rear
	    self._size: int = 0
	
	def size(self) -> int:
	    """获取队列的长度"""
	    return self._size
	
	def is_empty(self) -> bool:
	    """判断队列是否为空"""
	    return self._size == 0
	
	def push(self, num: int):
	    """入队"""
	    # 在尾节点后添加 num
	    node = ListNode(num)
	    # 如果队列为空,则令头、尾节点都指向该节点
	    if self._front is None:
	        self._front = node
	        self._rear = node
	    # 如果队列不为空,则将该节点添加到尾节点后
	    else:
	        self._rear.next = node
	        self._rear = node
	    self._size += 1
	
	def pop(self) -> int:
	  num = self.peek()
	  if self.size() == 1:
	    self._front = None
	    self._rear = None
	  else:
	    self._front = self._front.next
	  self._size -= 1
	  return num
	
	def peek(self) -> int:
	    """访问队首元素"""
	    if self.is_empty():
	        raise IndexError("队列为空")
	    return self._front.val
	
	def to_list(self) -> list[int]:
	    """转化为列表用于打印"""
	    queue = []
	    temp = self._front
	    while temp:
	        queue.append(temp.val)
	        temp = temp.next
	    return queue
/* 基于链表实现的队列 */
class LinkedListQueue {
  private:
    ListNode *front, *rear; // 头节点 front ,尾节点 rear
    int queSize;

  public:
    LinkedListQueue() {
        front = nullptr;
        rear = nullptr;
        queSize = 0;
    }

    ~LinkedListQueue() {
        // 遍历链表删除节点,释放内存
        freeMemoryLinkedList(front);
    }

    /* 获取队列的长度 */
    int size() {
        return queSize;
    }

    /* 判断队列是否为空 */
    bool isEmpty() {
        return queSize == 0;
    }

    /* 入队 */
    void push(int num) {
        // 在尾节点后添加 num
        ListNode *node = new ListNode(num);
        // 如果队列为空,则令头、尾节点都指向该节点
        if (front == nullptr) {
            front = node;
            rear = node;
        }
        // 如果队列不为空,则将该节点添加到尾节点后
        else {
            rear->next = node;
            rear = node;
        }
        queSize++;
    }

    /* 出队 */
    int pop() {
        int num = peek();
        // 删除头节点
        ListNode *tmp = front;
        front = front->next;
        // 释放内存
        delete tmp;
        queSize--;
        return num;
    }

    /* 访问队首元素 */
    int peek() {
        if (size() == 0)
            throw out_of_range("队列为空");
        return front->val;
    }

    /* 将链表转化为 Vector 并返回 */
    vector<int> toVector() {
        ListNode *node = front;
        vector<int> res(size());
        for (int i = 0; i < res.size(); i++) {
            res[i] = node->val;
            node = node->next;
        }
        return res;
    }
};

2.2.2. 基于数组的实现

在使用数组来实现栈的时候,由于数组的结构,因此在删除首元素的时候的时间复杂度为 O(n) 我们可以使用一个变量 front 指向队首元素的索引,并维护一个变量 size 用于记录队列长度。定义 rear = front + size ,这个公式计算出的 rear 指向队尾元素之后的下一个位置。

基于此设计,数组中包含元素的有效区间为 [front, rear - 1]

  • 入队操作:将输入元素赋值给 rear 索引处,并将 size 增加 1 。
  • 出队操作:只需将 front 增加 1 ,并将 size 减少 1 。 可以看到,入队和出队操作都只需进行一次操作,时间复杂度均为 O(1) 。

image.png

image.png

image.png

由于不断的入队和出队的过程,front 和 rear 都在向右移动,当它们到达数组尾部时(到达或要超过数组的长度限制)就无法继续移动了,此时为解决该问题,则将数组视为首尾相连的“环形数组” 环形数组:让 front 或 rear 在越过数组尾部时,直接回到数组头部继续遍历。这种周期性规律可以通过“取余操作”来实现 那么公式就变成了:rear = (front + size) % capacity

class ArrayQueue:
	"""基于环形数组实现的队列"""
	
	def __init__(self, size: int):
	    """构造方法"""
	    self._nums: list[int] = [0] * size  # 用于存储队列元素的数组
	    self._front: int = 0  # 队首指针,指向队首元素
	    self._size: int = 0  # 队列长度
	
	def capacity(self) -> int:
	    """获取队列的容量"""
	    return len(self._nums)
	
	def size(self) -> int:
	    """获取队列的长度"""
	    return self._size
	
	def is_empty(self) -> bool:
	    """判断队列是否为空"""
	    return self._size == 0
	
	def push(self, num: int):
	    """入队"""
	    if self._size == self.capacity():#到达数组最大容量
	        raise IndexError("队列已满")
	    # 计算队尾指针,指向队尾索引 + 1
	    # 通过取余操作实现 rear 越过数组尾部后回到头部
	    rear: int = (self._front + self._size) % self.capacity()
	    # 将 num 添加至队尾
	    self._nums[rear] = num
	    self._size += 1
	
	def pop(self) -> int:
	    """出队"""
	    num: int = self.peek()
	    # 队首指针向后移动一位,若越过尾部,则返回到数组头部
	    self._front = (self._front + 1) % self.capacity()
	    self._size -= 1
	    return num
	
	def peek(self) -> int:
	    """访问队首元素"""
	    if self.is_empty():
	        raise IndexError("队列为空")
	    return self._nums[self._front]
	
	def to_list(self) -> list[int]:
	    """返回列表用于打印"""
	    res = [0] * self.size()
	    j: int = self._front
	    for i in range(self.size()):
	        res[i] = self._nums[(j % self.capacity())]
	        j += 1
	    return res

"""Driver Code"""
if __name__ == "__main__":
# 初始化队列|
queue = ArrayQueue(10)
/* 基于环形数组实现的队列 */
class ArrayQueue {
  private:
    int *nums;       // 用于存储队列元素的数组
    int front;       // 队首指针,指向队首元素
    int queSize;     // 队列长度
    int queCapacity; // 队列容量

  public:
    ArrayQueue(int capacity) {
        // 初始化数组
        nums = new int[capacity];
        queCapacity = capacity;
        front = queSize = 0;
    }

    ~ArrayQueue() {
        delete[] nums;
    }

    /* 获取队列的容量 */
    int capacity() {
        return queCapacity;
    }

    /* 获取队列的长度 */
    int size() {
        return queSize;
    }

    /* 判断队列是否为空 */
    bool isEmpty() {
        return size() == 0;
    }

    /* 入队 */
    void push(int num) {
        if (queSize == queCapacity) {
            cout << "队列已满" << endl;
            return;
        }
        // 计算队尾指针,指向队尾索引 + 1
        // 通过取余操作实现 rear 越过数组尾部后回到头部
        int rear = (front + queSize) % queCapacity;
        // 将 num 添加至队尾
        nums[rear] = num;
        queSize++;
    }

    /* 出队 */
    int pop() {
        int num = peek();
        // 队首指针向后移动一位,若越过尾部,则返回到数组头部
        front = (front + 1) % queCapacity;
        queSize--;
        return num;
    }

    /* 访问队首元素 */
    int peek() {
        if (isEmpty())
            throw out_of_range("队列为空");
        return nums[front];
    }

    /* 将数组转化为 Vector 并返回 */
    vector<int> toVector() {
        // 仅转换有效长度范围内的列表元素
        vector<int> arr(queSize);
        for (int i = 0, j = front; i < queSize; i++, j++) {
            arr[i] = nums[j % queCapacity];
        }
        return arr;
    }
};

2.3. 队列的应用

  • 淘宝订单:按照顺序进行处理
  • 代办事项:事项先来先办

3. 双向队列

双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。 image.png

3.1. 双向队列常用操作

可以直接使用编程语言中已实现的双向队列类:deque

方法描述时间复杂度
push_first()元素添到队首O(1)
push_last()元素添到队尾O(1)
pop_first()删除队首元素O(1)
pop_last()删除队尾元素O(1)
peek_first()访问队首元素O(1)
peek_last()访问队尾元素O(1)
from collections import deque

# 初始化双向队列
deq: deque[int] = deque()

# 元素入队
deq.append(2)      # 添加至队尾
deq.append(5)
deq.append(4)
deq.appendleft(3)  # 添加至队首
deq.appendleft(1)

# 访问元素
front: int = deq[0]  # 队首元素
rear: int = deq[-1]  # 队尾元素

# 元素出队
pop_front: int = deq.popleft()  # 队首元素出队
pop_rear: int = deq.pop()       # 队尾元素出队

# 获取双向队列的长度
size: int = len(deq)

# 判断双向队列是否为空
is_empty: bool = len(deq) == 0
/* 初始化双向队列 */
deque<int> deque;

/* 元素入队 */
deque.push_back(2);   // 添加至队尾
deque.push_back(5);
deque.push_back(4);
deque.push_front(3);  // 添加至队首
deque.push_front(1);

/* 访问元素 */
int front = deque.front(); // 队首元素
int back = deque.back();   // 队尾元素

/* 元素出队 */
deque.pop_front();  // 队首元素出队
deque.pop_back();   // 队尾元素出队

/* 获取双向队列的长度 */
int size = deque.size();

/* 判断双向队列是否为空 */
bool empty = deque.empty();

3.2. 双向队列的实现:

www.hello-algo.com/chapter_sta…

3.3. 双向队列的应用

代替栈来实现软件中的撤销功能

4. 小结

4.1. 栈:

  • 栈是一种遵循先入后出逻辑的数据结构,栈顶元素称为栈顶,元素加入栈称为入栈,删除称为出栈。只能在栈顶添加或者删除元素。
  • 一般而言,编程语言都会内置栈类 stack
方法描述时间复杂度
push()入栈至栈顶O(1)
pop()栈顶元素出栈O(1)
peek()访问栈顶元素O(1)

4.2. 队列:

  • 队列(queue)是一种遵循先入先出规则的线性数据结构。队列头部称为“队首”,尾部称为“队尾”,将把元素加入队尾的操作称为“入队”,删除队首元素的操作称为“出队”。先入队和先出队的都是队首。
  • 一般编程语言中都有现成的队列类 queue
方法描述时间复杂度
push()入队至队尾O(1)
pop()队首元素出队O(1)
peek()访问队首元素O(1)

4.3. 双向队列:

  • 双向队列(double-ended queue)提供了更高的灵活性,允许在头部和尾部执行元素的添加或删除操作。
  • 可以直接使用编程语言中已实现的双向队列类:deque
方法描述时间复杂度
push_first()元素添到队首O(1)
push_last()元素添到队尾O(1)
pop_first()删除队首元素O(1)
pop_last()删除队尾元素O(1)
peek_first()访问队首元素O(1)
peek_last()访问队尾元素O(1)