一、栈(Stack)
1、栈的概念
栈是一种特殊的线性表,它只能在一端进行插入和删除操作的特殊线性表。它按照后进先出的原则储存数据,先进入的元素被压入栈底最后的数据在栈顶。
压栈:栈的插入操作叫做进栈/压栈/入栈,入栈的元素在栈顶。
出栈:栈的删除操作叫做出栈,出数据在栈顶。
2、栈的使用
方法 | 功能 |
Stack() | 构造一个空的栈 |
E push(E e) | 将元素入栈 |
E pop() | 将栈顶元素出栈 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效个数大小 |
boolean empty() | 判断栈是否为空 |
3、栈的模拟实现
如图可见Stack继承了Vector,Vector的大小是可以动态变化的,它和ArrayList类似,但Vector是线程安全(方法大多是同步的,在多线程环境下可以安全地访问),但在一些不需要线程安全的场景下ArrayList通常是更好的选择,它的性能更高。
public class MyStack {
//首先定义一个数组
int []array;
//size为栈中有效元素个数
int size;
//在构造方法中进行数组初始化
public MyStack(){
array=new int[5];
}
//入栈操作
public void push(int val) {
//首先判断数组是否满了
if(isFull()){
//满了就要进行扩容
array= Arrays.copyOf(array,array.length*2);
}
array[size]=val;
size++;
}
public boolean isFull(){
//有效数组个数是否等于数组长度
return size== array.length;
}
//出栈操作
public int pop() {
//判断数组是否为空
if(isEmpty()){
return -1;
}
int ret=array[size-1];
size--;
return ret;
}
public boolean isEmpty(){
//判断有效元素个数是否为0
return size==0;
}
public int peek() {
if (isEmpty()){
//此处的-1为空的意思(null)
return -1;
}
return array[size-1];
}
public int size(){
return size;
}
当然,栈也可以通过单向链表和双向链表来实现
在单向链表中,链表的头部相当于栈顶,当进行入栈操作时,在链表头部插入一个新节点,新节点的数据就是要入栈的元素,也就成为链表的头结点(使用头插法),也就是栈顶元素,出栈操作就是删除链表的头结点。
- 采用的是头插法,入栈和出栈的时间复杂度都为O(1);
- 采用的是尾插法,入栈的时间复杂度为O(n),如果有last,那么时间复杂度为O(1),但是出栈时间复杂度一定是O(n);
在双向链表中无论是使用头插法还是尾插法时间复杂度都是O(1);
4、栈的优缺点
优点:
- 简单高效,在许多算法和程序中,能快速储存和获取数据,执行效率高。
- 数据保护,只允许在栈顶进行操作,避免了对中间和底部数据的意外修改。
- 内存管理方便,系统自动管理。
缺点:
- 数据访问受限,操作不便,不适合需要频繁随机访问数据的场景。
- 空间大小固定,可能会发生栈溢出错误。
二、队列(Queue)
1、队列的概念
队列是一种特殊的数据结构,它遵循先进先出的原则,就像现实生活中排队一样,先进入队列的元素会被先处理。
入队列:进行插入操作的一端称为队尾。
出队列:进行删除操作的一端称为队头。
2、队列的使用
方法 | 功能 |
boolean offer(E e) | 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
以上的方法有两种,它们的使用不同:
队列的使用 :
由于Queue是一个接口,不能直接实例化对象,所以在实例化时必须实例化LinkedList的对象,LinkedList实现了Queue接口。
3、队列的模拟实现
我们使用双向链表来实现:
public class implementsQueue {
//节点的创建
static class ListNode{
public int val;
//用来获取前一个节点
public ListNode prev;
//用来获取后一个节点
public ListNode next;
public ListNode(int val){
this.val=val;
}
}
//first指向队头,last指向队尾
public ListNode first;
public ListNode last;
//入队操作:尾插法
public void offer(int val) {
ListNode node=new ListNode(val);
//判断是否一个元素都没有
if(first==null){
first=node;
last=node;
}else{
//这里表示不是第一次入队
last.next=node;
node.prev=last;
last=node;
}
}
//出队:删除第一个节点
public int poll() {
//还是先判断队列为空不为空
if(first==null){
return -1;
}
//用于存储队头元素的值,便于返回
int val=first.val;
//判断是否只有一个元素
if(first==last){
first=null;
last=null;
}else{
//头节点直接指向后一个节点,再将它的前驱置空
first=first.next;
first.prev=null;
}
return val;
}
//得到队头元素的值
public int peek(){
if(first==null){
return -1;
}
int val=first.val;
return val;
}
//得到有效元素的个数
public int size(){
//用于计数
int count=0;
//为了保持头结点不变,用于遍历队列
ListNode cur=first;
while(cur!=null){
count++;
cur=cur.next;
}
return count;
}
//判断队列是否为空
public boolean isEmpty(){
if(first==null){
return true;
}
return false;
}
}
4、循环队列
环形队列通常用数组实现:
数组下标循环小技巧:
1、下标最后再往后(n小于array.length): index=(index+n)%array.length
2、下标最前再往前(n小于array.length): index=(index+array.length-n)%array,length
区分空和满:
使用标记
- 初始状态:队列为空时,front=rear=0,标志位isFull=false
- 每次入队检查rear是否与front重合,如果重合设置isfull=true,表示队列已满,否则,正常入队并移动rear
- 出队时,移动front指针,设置isfull=false。
class MyCircularQueue {
public int[]elem;
public int front;
public int rear;
public boolean isfull=false;
public MyCircularQueue(int k) {
this.elem=new int[k];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear]=value;
rear=(rear+1)%elem.length;
if(rear==front){
isfull=true;
}
return true;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
front=(front+1)%elem.length;
isfull=false;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
public int Rear() {
if(isEmpty()){
return -1;
}
int index=-1;
if(rear==0){
index=elem.length-1;
}else{
index=rear-1;
}
return elem[index];
}
public boolean isEmpty() {
return !isfull&&front==rear;
}
public boolean isFull() {
return isfull;
}
}
5、双端队列
Deque(双端队列),是一种特殊的线性表,它允许在队列的两端进行插入和删除操作。
Deque是一个接口,使用时必须创建LinkedList对象
6、队列的优缺点
优点:
- 先进先出特性,不会出现混乱。
- 操作简单,降低了开发的难度和出错的概率。
- 内存管理高效,连续存储的,可利用数组实现,链式存储适合处理动态变化的数据量。
缺点:
- 空间限制,可能会出现空间浪费或者溢出的情况。
- 操作局限性,对中间元素的访问和操作比较困难。
- 性能问题,频繁的入队和出队操作,可能会1导致性能下降,链式存储的队列存在节点指针的额外开销,遍历队列时效率相对较低。