栈和队列recoding

916 阅读7分钟

今天我们来聊聊栈和队列,首次接触还是在大二,那时刚接触这些新颖的数据结构,感觉很有意思,但是当时接触的都是关于栈和队列的特性,难度不大,所以深度也不够,现在再次碰到栈和队列,就想着记录一下吧

主题:栈和队列

栈和队列是计算机科学中最基本的数据结构之一。它们在许多算法和程序设计中都扮演着重要的角色。在本文中,我们将介绍什么是栈和队列,它们的定义、特征和用途,并提供一些使用示例,以帮助读者深入了解这两种数据结构。

栈是一种后进先出(Last-In-First-Out)的数据结构。它的名称来自于现实生活中的一种物理结构,就像一摞盘子,你只能从顶部插入或删除。插入一个新元素时,它被压入栈的顶部。删除一个元素时,从栈的顶部弹出一个元素。栈可以用数组或链表实现。

下面是一个简单的栈的实现:

class Stack {
  constructor() {
    this.items = [];
  }

  push(element) {
    this.items.push(element);
  }

  pop() {
    if (this.isEmpty()) {
      return "Underflow";
    }
    return this.items.pop();
  }

  isEmpty() {
    return this.items.length == 0;
  }

  peek() {
    return this.items[this.items.length - 1];
  }

  printStack() {
    var str = "";
    for (var i = 0; i < this.items.length; i++) {
      str += this.items[i] + " ";
    }
    return str;
  }
}

该栈的实现使用了一个数组来存储元素。在该类中,我们定义了五个方法:

  • push(element):将元素压入栈的顶部。
  • pop():从栈的顶部弹出一个元素,并返回弹出的元素。
  • isEmpty():检查栈是否为空。
  • peek():返回栈顶元素,但不弹出它。
  • printStack():打印栈中所有元素。

使用示例:

var stack = new Stack();

console.log(stack.isEmpty()); // 输出 true

stack.push(1);
stack.push(2);
stack.push(3);

console.log(stack.printStack()); // 输出 "1 2 3"

console.log(stack.peek()); // 输出 3

console.log(stack.pop()); // 输出 3

console.log(stack.printStack()); // 输出 "1 2"
  1. 队列

队列是一种先进先出(First-In-First-Out)的数据结构。它的名称也来自于现实生活中的一种物理结构,就像一排人在等待服务,先到先得。向队列添加新元素时,它被添加到队列的末尾。删除一个元素时,从队列的前端删除一个元素。队列可以用数组或链表实现。

下面是一个简单的队列的实现:

class Queue {
  constructor() {
    this.items = [];
  }

  enqueue(element) {
    this.items.push(element);
  }

  dequeue() {
    if (this.isEmpty()) {
      return "Underflow";
    }
    return this.items.shift();
  }

  isEmpty() {
    return this.items.length == 0;
  }

  front() {
    if (this.isEmpty()) {
      return "No elements in Queue";
    }
    return this.items[0];
  }

  printQueue() {
    var str = "";
    for (var i = 0; i < this.items.length; i++) {
      str += this.items[i] + " ";
    }
    return str;
  }
}

该队列的实现使用了一个数组来存储元素。在该类中,我们定义了五个方法:

  • enqueue(element):将元素添加到队列末尾。
  • dequeue():从队列的前端删除一个元素,并返回它。
  • isEmpty():检查队列是否为空。
  • front():返回队列的第一个元素。
  • printQueue():打印队列中所有元素。

使用示例:

var queue = new Queue(); 

console.log(queue.isEmpty()); // 输出 true

queue.enqueue(1); 
queue.enqueue(2); 
queue.enqueue(3);

console.log(queue.printQueue()); // 输出 "1 2 3"

console.log(queue.front()); // 输出 1

console.log(queue.dequeue()); // 输出 1

console.log(queue.printQueue()); // 输出 "2 3"
  1. 栈和队列的应用

栈和队列广泛应用于计算机科学和软件工程中。以下是一些栈和队列的典型应用:

  • 撤销和恢复:撤销和恢复操作通常使用栈来实现。撤销操作将最近的更改弹出栈顶,而恢复操作将已撤销的更改压入栈顶。

  • 编译器和解释器:编译器和解释器使用栈来实现表达式求值。例如,在编写一个程序时,计算机系统必须将中缀表达式转换为后缀表达式,再对其进行求值。栈可以用于此目的,因为它可以储存操作符的优先级以及操作数的顺序。

  • 回文字符串:可以使用堆栈来检查字符串是否为回文。回文指的是正反读都一样的字符串。将字符串中的每个字符压入堆栈,然后从堆栈中弹出字符并与原始字符串进行比较。由于堆栈是后进先出的,所以可以按相反的顺序访问字符,从而实现回文字符串的检查。

  • 队列的应用之一是在网络路由中实现先进先出(FIFO)的数据传输队列。在这种情况下,数据包被添加到队列末尾,并按照其到达的顺序进行处理和发送。此外,队列还可以用于多线程程序和死锁控制,其中任务按照特定的顺序排队等待执行。

但是这些都是基础部分,相信几乎都知道,所以光了解这些是不够的。

来说些深入些的

栈和队列的应用于算法设计

栈和队列不仅是数据结构,它们也是许多算法设计和分析的基础。例如,栈和队列在深度优先搜索和广度优先搜索等图形算法中被广泛使用。这些算法将节点存储在堆栈或队列中,并按照特定的顺序访问它们以搜索整个图形。

此外,栈和队列还在解析算法(如递归下降解析器)中发挥重要作用,这些算法通常使用堆栈来跟踪当前解析的上下文信息。类似地,动态规划算法也经常使用队列,将问题分解为若干子问题,并将它们放入队列中等待处理。

栈与递归

栈和递归之间存在紧密的联系。事实上,很多递归算法都可以通过使用栈帧转换为非递归算法。在递归调用期间,计算机将每个函数调用的状态保存在栈帧中,然后按照后进先出的顺序对它们进行处理。可以使用堆栈来模拟这些栈帧,并在无需递归的情况下实现相同的算法。

此外,递归求解的问题也可以使用堆栈进行非递归求解。例如,在二叉树上进行深度优先搜索通常是一种递归算法。用于执行相同操作的迭代算法需要一个显式的栈来跟踪访问节点的顺序。有许多其他使用栈来模拟递归机制的例子。

队列的不同变体

队列由于具有先进先出的特性,因此其能够应用到无数场景中。在实际应用中,还有许多变体的队列存在:

双端队列(deque):允许从两端插入和删除元素。
优先队列(priority queue):通过给每个元素分配一个优先级,保证优先级高的元素比优先级低的元素更容易被删除或处理。
循环队列(circular queue):将队列封装成环形结构,以充分利用空间,避免队头和队尾重合时的问题。
队列中的双向链表:将数组存储的队列替换为双向链表,以支持动态大小调整和更灵活地添加/删除元素等高级操作。

这些队列变体都在某种程度上优化了标准队列的性能,以适应不同的需求。

栈和队列的并发与同步

多线程程序中栈和队列在实现并发性时非常重要。在设计多线程程序时,必须确保对共享资源的访问是同步的。如果多个线程尝试同时访问共享的栈或队列,可能会导致数据竞争和不一致性。这种情况可以通过使用锁、信号量等同步机制来解决。

此外,栈和队列还可以用于处理多进程间的通信。例如,当一个进程需要向另一个进程发送消息时,可以将消息存储在队列中。允许多个进程在不同的时期读取和写入这个队列,从而实现进程之间的通信。

总结:

  1. 总结

这些有关更深入的栈和队列知识,重要的是包括它们的应用于算法设计、栈与递归的联系、队列的变体,以及并发和同步问题。

栈和队列虽然是非常基础的数据结构,但仍广泛应用于许多不同的场景中,并经常被用作高级算法的基石。