在计算机科学中,数据结构是用来组织和存储数据的方式,以便可以高效地访问和修改。栈和队列是两种最基本的数据结构,它们在各种计算过程中都有广泛的应用。本文将介绍栈和队列的概念、特性以及它们的一些常见应用。
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵循后进先出的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
在现实中我们也有类似的场景,那就是子弹的发射,最后装填进去的子弹是最先发射出去。
在Java中栈又是如何使用的呢?有以下这些方法。
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
E push (E e) | 将e入栈,并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peak() | 获取栈顶元素 |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
示例:
public static void main ( String [] args ) { Stack < Integer > s = new Stack (); s . push ( 1 ); s . push ( 2 ); s . push ( 3 ); s . push ( 4 ); System . out . println ( s . size ()); // 获取栈中有效元素个数 ---> 4 System . out . println ( s . peek ()); // 获取栈顶元素 ---> 4 s . pop (); // 4 出栈,栈中剩余 1 2 3 ,栈顶元素为 3 System . out . println ( s . pop ()); // 3 出栈,栈中剩余 1 2 栈顶元素为 3 if ( s . empty ()){ System . out . println ( " 栈空 " ); } else { System . out . println ( s . size ()); } }
从上图中可以看到,Stack继承了Vector,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安全的。
Stack
类,它是以向量(Vector
)为基础的一个实现,用于存储和管理数据的先进后出的顺序。综上所述,栈是一种通用的数据结构,用于维护数据的先进后出顺序;虚拟机栈是JVM内部为每个线程分配的一个特定区域,用于管理方法调用过程中的数据;而栈帧则是虚拟机栈中用于记录单个方法调用信息的数据块。
队列:只允许在一端进行插入数据的操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出的特点。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头
这就类似于生活中排队打饭的场景,排在前面的先打到饭离开队伍,队伍最后的人最后打到饭离开队伍。
在Java中,Queue是个接口,其底层是通过链表来实现的。
常见的方法及功能:
方法 | 功能 |
---|---|
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素的个数 |
boolean isEmpty() | 检测队列是否为空 |
队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,那么会选择顺序结构还是链式结构呢?
选择顺序结构还是链式结构实现队列,取决于具体的应用场景和需求。以下是两种实现方式的优缺点分析:
内存使用效率高:顺序队列通常使用数组实现,内存空间连续,利用率高。 便于随机访问:数组的特性使得可以在常数时间内随机访问任何元素。 操作简便:在队尾插入和队头删除操作的时间复杂度为O(1)。
可能出现假溢出:当队列没有满但因为尾指针达到数组边界而无法插入新元素时。 大小固定:需要预先定义队列的大小,不利于动态变化的数据量。
大小动态可变:不需要预先定义大小,可以根据需要动态增长。 不存在假溢出问题:链表的特性使得即使队列看起来已满,仍然可以继续添加元素。
内存使用效率低:由于链表需要额外的指针信息,会有额外的内存开销。 不便于随机访问:链表不支持快速的随机访问,只能按序遍历。
综上所述,如果对内存使用效率和随机访问有较高要求,且能够接受固定大小的限制,那么顺序队列(特别是循环队列)可能是更好的选择。如果应用需要队列大小能够动态变化,或者对假溢出问题敏感,那么链式队列可能更适合。在实际应用中,应根据具体需求选择合适的数据结构来实现队列。
public class Queue {
// 双向链表节点
public static class ListNode{
ListNode next;
ListNode prev;
int value;ListNode(int value){
this.value = value;
}
}
ListNode first; // 队头
ListNode last; // 队尾
int size = 0;
// 入队列---向双向链表位置插入新节点
public void offer(int e){
ListNode newNode = new ListNode(e);
if(first == null){
first = newNode;
// last = newNode;
}else{
last.next = newNode;
newNode.prev = last;
// last = newNode;
}
last = newNode;
size++;
}
// 出队列---将双向链表第一个节点删除掉
public int poll(){
// 1. 队列为空
// 2. 队列中只有一个元素----链表中只有一个节点---直接删除
// 3. 队列中有多个元素---链表中有多个节点----将第一个节点删除
int value = 0;
if(first == null){
return null;
}else if(first == last){
last = null;
first = null;
}else{
value = first.value;
first = first.next;
first.prev.next = null;
first.prev = null;
}
--size;
return value;
}
// 获取队头元素---获取链表中第一个节点的值域
public int peek(){
if(first == null){
return null;
}
return first.value;
}
public int size() {
return size;
}
public boolean isEmpty(){
return first == null;
}
}
实际情况中有时还会使用一种队列叫循环队列。环形队列通常使用数组实现。
数组下标循环的小技巧
如何区分空与满
双端队列是指允许两端都可以进行入队和出队操作的队列,那就说明元素可以从队头出队和入队,也可以从队尾出队和入队。
Deque是一个接口,使用时必须创建LinkedList的对象。
在实际情况中,使用Deque接口是比较多的,栈和队列均可使用该接口,
总结
栈和队列是构建更复杂数据结构的基础,如二叉树、图、堆等。它们在不同的算法和系统设计中扮演着关键角色。理解它们的工作原理和应用场景对于任何希望深入学习计算机科学的人来说都是必不可少的。通过掌握这些基本概念,我们可以更好地理解和设计复杂的系统,从而解决实际问题。