目录
栈和队列
线性表:一次保存单个同类型元素,多个元素之间逻辑上连接
栈和队列其实是操作受限的线性表。之前的数组、链表可以在任意位置插入和删除,但栈和队列只能在一端插入和删除元素
栈
-
只能从一段插入元素,也只能从这一端取出元素(栈顶):添加和删除元素的一端称为栈顶,另一端称为栈底。最先添加的元素在栈的最底端(栈底),最后添加的元素在栈的最顶端(栈顶)。而从栈中取出元素的顺序和从栈中添加元素的顺序恰好相反,即最后添加的元素最先取出(Last In First Out, LIFO)
-
在操作一个栈时,只能操作这个栈的栈顶元素,其他元素对于我们来说是不可见的
-
栈的特点:先进后出,后进先出的线性表。LIFO
栈在现实生活中的应用(无处不再):
-
无处不在的undo(撤销)操作:
e.g. 撤销相当于从栈顶取出错误的元素,然后读取当前栈顶元素
在任何一个编辑器中输错了一个内容使用ctrl+z就返回到了上一次输入的内容。
在任何一个浏览器中点击<-就能返回上一次浏览的网站
-
操作系统栈:程序在执行过程中,从A函数调用B函数,从B函数调用C函数,返回执行时,如何得知从哪开始继续执行呢,其实背后就是栈这个结构
e.g. 记录程序中,函数A调用函数B的行数,压入栈。执行结束后逐一出栈。
栈的实现:
-
基于数组实现的栈--顺序栈(大部分情况)
栈只能在栈顶插入元素,在栈顶删除元素,即只能在数组的末尾插入和删除元素
2. 基于链表实现的栈--链式栈
核心操作:
记得size属性!!!用于记录当前栈中元素个数
push(E e): 向栈中添加元素 入栈/压栈
E pop()出栈操作,弹出栈顶元素
E peek()查看栈顶元素,但不返回
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
/**
*基于数组实现的顺序栈
* @param <E>
*/
public class MyStack<E> {
private int size;
private List<E> myStack = new ArrayList<E>();
/**
* 向栈中添加元素
* @param val
*/
public void push(E val){
myStack.add(val);
size++;
}
/**
* 弹出当前栈顶元素,返回栈顶元素的值
* @return
*/
public E pop(){
// 注意要先判空,栈为空无法弹出
if(isEmpty()){
throw new NoSuchElementException("Stack is empty, can't pop!");
}
E val = myStack.remove(size-1);
size --;
return val;
// return myStack.remove(size--);
}
private boolean isEmpty() {
return size == 0;
}
/**
* 查看当前栈顶元素值,但不弹出该元素
* @return
*/
public E peek(){
if(isEmpty()){
throw new NoSuchElementException("Stack is empty, can't peek!");
}
return myStack.get(size-1);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0; i < size; i++) {
sb.append(myStack.get(i));
// 未走到最后一个
if(i != size-1){
sb.append(", ");
}
}
sb.append("] top");
return sb.toString();
}
}
队列
栈和队列其实是一码事,都是只能在线性表的一端进行插入和删除操作,因此二者可以相互转换
队列:FIFO,先进先出的数据结构,元素从队尾添加到队列中,元素队首出队列。元素的出队顺序和入队顺序保持一致
现实生活中的应用:各种“排队”操作
队列中结构较多:普通FIFO队列、双端队列Deque、循环队列LoopQueue和优先队PriortyQueue
java中内置队列:java.util.Queue 类似 java.util.LinkedList
队列的实现:
1.基于数组实现的队列(顺序队列)——常用于搭建循环队列
2.基于链表实现的队列(链式队列)——常用于搭建普通的队列
由于出队操作只能在队列的头部进行,若采用数组的方案,每次出队一个元素就得搬移剩下的所有元素向前移动一个单位。此时采用链表的方案更适合队列的结构
操作:
push(E val) 出队列
pop()弹出队首元素
peek()查看队首元素
import seqlist.queue.Queue;
import java.util.NoSuchElementException;
class Node<E> {
E val;
Node<E> next;
public Node(E val) {
this.val = val;
}
}
public class LinkedQueue<E> implements Queue<E> {
// 当前队列中的元素个数
private int size;
// 当前队列的队首元素
private Node<E> head;
// 当前队列的尾部元素
private Node<E> tail;
@Override
public void offer(E val) {
// 产生一个新节点
Node<E> node = new Node<>(val);
if (head == null) {
head = tail = node;
}else {
// 链表的尾插
tail.next = node;
tail = node;
}
size ++;
}
@Override
public E poll() {
if (isEmpty()) {
throw new NoSuchElementException("queue is empty!cannot poll!");
}
// 删除当前的队首元素,即head
E val = head.val;
Node<E> node = head;
head = head.next;
node.next = node = null;
size --;
return val;
}
@Override
public E peek() {
if (isEmpty()) {
throw new NoSuchElementException("queue is empty!cannot peek!");
}
return head.val;
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("front [");
for (Node x = head;x != null;x = x.next) {
sb.append(x.val);
if (x.next != null) {
sb.append(", ");
}
}
sb.append("] tail");
return sb.toString();
}
}
循环队列
- 循环队列就是使用长度固定的数组实现,数组头部就是队首(head),数组的尾部就是队尾(tail),即head永远指向循环队列的第一个元素,tail永远指向循环队列有效元素的后一个位置, 故数组[head,tail)是循环队列的有效元素。
- 所谓的循环队列指的就是当head或者tail引用走到数组末尾时,下一次再继续向后移动,其实返回数组的头部继续操作
- 使用两个引用head和tail实现出队入队操作,添加元素在数组尾部添加,删除元素只需要移动head引用所指向的地址即可(逻辑删除)。避免了从数组头部删除元素时,需要频繁移动元素的情况。即循环队列在删除元素时,不需要进行数据的搬移,当有新的元素在添加时就会覆盖掉之前的元素
- 用途:操作系统的生产消费者模型,MySQL数据库的InnoDB存储引擎中的redo日志
要点:
-
数组为空,循环队列为空,此时head==tail
-
在循环队列中浪费一个空间,用于判断队列是否已满。当循环队列已满时,(tail+1)%n ==head
head和tail的移动不能简单的+1,因为可能数组越界,应使用取模操作,即head和tail走到数组最后一个索引位置时,想要下一次返回数组头部,需要用tail+1对数组长度n取模
基于数组的实现:
import seqlist.queue.Queue;
import java.util.NoSuchElementException;
public class LoopQueue implements Queue<Integer> {
// 定长数组
private Integer[] data;
// 指向队首元素
private int head;
// 指向队尾元素的下一个索引
private int tail;
public LoopQueue(int size) {
// 因为循环队列中要浪费一个空间判断是否已满
data = new Integer[size + 1];
}
@Override
public void offer(Integer val) {
if (isFull()) {
throw new ArrayIndexOutOfBoundsException("loopQueue is full,cannot offer");
}
data[tail] = val;
tail = (tail + 1) % data.length;
}
@Override
public Integer poll() {
if (isEmpty()) {
throw new NoSuchElementException("loopQueue is empty!cannot poll");
}
// 移动队首元素
Integer val = data[head];
// 移动引用一定是 + 1 % n
head = (head + 1) % data.length;
return val;
}
@Override
public Integer peek() {
if (isEmpty()) {
throw new NoSuchElementException("loopQueue is empty!cannot peek");
}
return data[head];
}
@Override
public boolean isEmpty() {
return head == tail;
}
public boolean isFull() {
return (tail + 1) % data.length == head;
}
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("front [");
int lastIndex = tail == 0 ? data.length - 1 : tail - 1;
for (int i = head; i != tail;) {
sb.append(data[i]);
// 最后一个有效元素的索引是多少?
if (i != lastIndex) {
sb.append(", ");
}
i = (i + 1) % data.length;
}
sb.append("] tail");
return sb.toString();
}
}
双端队列Deque
Queue的子接口,这个队列既可以尾插头出,也可以头插尾出
以后无论使用的时栈还是接口,统一使用双端队列接口。stack类效率低,被时代抛弃
Deque<E> stack = new LinkedList<>();
Deque<E> queue = new LinkedList<>();