在浏览器中,我们根据自己的需求或页面引导,一层一层的点入某个页面,逐级深入,当我们想反悔上一层的时候,可以使用浏览器的 “←” 后退按钮。那么浏览器是怎样记住上一步的页面是什么呢?返回的必然是最近一次的访问的页面。
同样地, 在编辑器中,我们编写了某个东西,觉得进行不下去了,想返回之前的版本,发现没有进行备份,那么我们会使用 “Ctrl + Z”,它会一步一步的回退到最开始的样子。有点像把电影倒着播放的感觉
将一个数字转化为二进制表示,如 ,11 => 1011,转换的过程是:整数num,num对2求商和余数,商再对2求商和余数,直到商为1,转化后的二进制的表示结果就是将最后一个商数和所有的余数连接起来,如图:
方法的递归调用:参数从最外层传到最里层,结果从最里层返回到最外层 。也是一个V字型的调用过程
以上的应用场景都有一个特征,就是先处理的事情,后使用,有一个“V”字的曲线。即 先进后出,这时就出现了栈这种数据结构。
栈不是新的数据结构,其本质是线性表,是限定仅在表尾进行插入和删除操作的线性表。后进先出的线性表。(栈首先是个线性表,栈元素之间具有线性关系)
栈顶(top):允许进行插入和删除的一端,插入操作称为“进栈、压栈、入栈”,删除操作称为“出栈”
栈底(bottom):不允许进行操作的一端
顺序存储结构
数组:类似于游标,top端即移动的一端,bottom栈底为0刻度线
public class Stack<E> extends Vector<E> {
public boolean empty() {
return size() == 0;
}
public synchronized E pop() {
E obj;
int len = size();
obj = peek();
removeElementAt(len - 1);
return obj;
}
public E push(E item) {
addElement(item);
return item;
}
public synchronized E peek() {
int len = size();
if (len == 0)
throw new EmptyStackException();
return elementAt(len - 1);
}
public synchronized int search(Object o) {
int i = lastIndexOf(o);
if (i >= 0) {
return size() - i;
}
return -1;
}
}
Stack类继承Vector类,从 search 方法也可以看出来,JDK 中 Stack 类是通过 线性存储结构 实现的栈。
链式存储结构
链式存储结构,需要定义一个节点,用于保存数据元素和下一节点(指针),链式存储结构的入栈和出栈通过对象的新建和释放、调整指针和栈的长度来实现。
private class Node {
private T data;// 保存节点的数据元素
private Node next;// 保存下一个节点的引用
public Node() {
}
public Node(T data, Node next) {
this.data = data;
this.next = next;
}
}
public class LinkStack<T> {
private Node top;// 存放栈顶节点
private int size = 0;// 存放栈中已有的元素个数
// 创建空链栈
public LinkStack() {
top = null;
}
// 已指定数据元素创建链栈,只有一个元素
public LinkStack(T element) {
top = new Node(element, null);
size++;
}
// 返回链栈的长度
public int length() {
return size;
}
// 进栈
public void push(T elemnt) {
// 让top指向新节点,新节点的next指向原来的top
top = new Node(elemnt, top);
size++;
}
// 出栈
public T pop() {
// 若当前为空栈,则返回null
if (size == 0) {
return null;
}
Node oldTop = top;
// 让top指向原栈顶的下一个节点
top = top.next;
// 释放原栈顶元素的引用
oldTop.next = null;
size--;
return oldTop.data;
}
// 获取栈顶元素
public T getTop() {
if (size == 0) {
return null;
}
return top.data;
}
// 判空
public boolean isEmpty() {
return size == 0;
}
// 清空栈
public void clear() {
top = null;
size = 0;
}
}
通过顺序存储实现的栈结构非常方便,因为只允许栈顶操作元素,所以不存在线性表中插入和删除时的移动元素的问题,但需要事先确定栈的存储大小!尽可能考虑周全,设计合适大小的数组来存储栈。当存在两个相同结构的栈时,在空间的使用上可以有一个技巧来实现存储空间的利用最大化–共享栈。
共享栈的设计思路:两个栈分别在数组的两端,添加元素时,分别向中间靠拢。当两个栈顶指针碰在一起,这个共享栈就被占满了。
判空条件: top1 == -1 && top2 == n
栈满条件: top1 + 1 == top2
public class BothStackShareMemory {
private Object[] array; //定义一个数组存储
private int stackSize; //栈长度
private int top1; //第一个栈的栈顶指针
private int top2; //第2个栈的栈顶指针
//初始化构建栈
public BothStackShareMemory() {
stackSize = 10;
array = new Object[stackSize];
top1 = -1;
top2 = stackSize; //都是空栈
}
//入栈
public boolean push(int stackNum, Object element) {
if (top1 + 1 == top2) {
System.out.println("栈满");
return false;
}
if (stackNum == 1) {
top1++;
array[top1] = element;
} else {
top2--;
array[top2] = element;
}
return true;
}
//出栈
public boolean pop(int stackNum, Object element) {
if (top1 == -1 || top2 == stackSize) {
System.out.println("栈为空");
return false;
}
if(stackNum == 1){
array[top1] = null;
top1--;
return true;
}else {
array[top2] = null;
top2++;
return true;
}
}
//获取栈顶元素
public Object peek(int i) {
if (i == 1) {
if(top1 == -1){
System.out.println("栈为空");
return null;
}
return array[top1];
} else {
if(top2 == stackSize){
System.out.println("栈为空");
return null;
}
return array[top2];
}
}
}
思路:遍历中缀表达式:
思路:遍历后缀表达式,遇数字就入栈,遇操作符则将栈顶数字出栈,进行运算,将结果再次入栈
分析:由于括号的匹配存在规则–左括号与最近一个右括号匹配,可以通过栈来实现
思路:对字符串遍历,遇到左括号就入栈,遇到右括号就与栈顶元素进行匹配。
public boolean isBalanced(String s) {
Stack<Character> stack = new Stack<Character>();
for(int i=0;i<s.length();i++){
if(s.charAt(i)=='(' || s.charAt(i)=='[' || s.charAt(i)=='{')
stack.push(s.charAt(i));
else if(s.charAt(i)==')'){
if(stack.isEmpty()) return false;
if(stack.pop()!='(') return false;
//在出栈操作之前应判断栈是否为空
} else if(s.charAt(i)==']'){
if(stack.isEmpty()) return false;
if(stack.pop()!='[') return false;
} else if(s.charAt(i)=='}'){
if(stack.isEmpty()) return false;
if(stack.pop()!='{') return false;
}
}
return stack.isEmpty();
}
思路:循环对数字进行求余数,将余数入栈,并对该数字整除,直至该数字为0 。依次将栈中元素取出,即该数字用二进制表示的字符串。
public String numChange(int number) {
java.util.Stack<String> stack = new Stack<String>();
while (number > 0) {
stack.push(number % 2 + "");
number = number / 2;
}
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.append(stack.pop());
}
return sb.toString();
}
处理用户请求
场景一:请求响应慢
用户在 客户端 进行操作,提交支付宝认证请求,后端收到该用户请求后,需要去请求支付宝轮询用户认证状态,当用户认证状态为已完成认证时,返回给前端已认证。认证过程中由于受到第三方的影响,时间可能会比较长,用户总不能一直等待吧?这种情况,需要在收到请求后,就给用户反馈,然后再 “依次” 处理用户请求。
二:请求量大
批量用户请求同时到达,如双十一、秒杀活动等,在同一时刻的用户请求可以达到每秒上万、上百万次,数据库压力过大,采用缓存机制可能效果不是非常明显。这种情况也需要在收到请求后,将用户请求组织起来,按照时间先后、会员非会员、优惠政策等依次处理用户请求。
打印机
一层楼计算机的打印任务都集中在一台打印机上,打印机对所有用户一视同仁,先到先服务, “依次” 处理打印请求。
文件操作
文件的读取和写入操作,都是顺序执行的,即 “依次” 读取和写入字符。
依次 符合队列的处理特性:先进先出。
队列:是特殊的线性表,只允许在一端进行插入操作,另一端进行删除操作的线性表。队列是一种 先进先出(FIFO) 的线性表
队头:允许删除的一端,front
队尾:允许插入的一端,rear
常用操作:判空、队列长度、入队、出队、清空等
两种方式:
队头固定不动
像生活中排队一样,一个人出队,后面的人都往前移动一个位置;新入队的人在尾部加入。
缺点:数组元素频繁的移动,时间复杂度高
队头不固定:
出队之后,队头向后移动一个位置;入队之后,队尾向后移动一个位置。
缺点:队头不断后移,队头之前空出大量位置,造成空间的浪费,并且容易导致队列满的情况。
循环队列
循环队列,即将头尾相接的顺序存储结构的队列。在队头不固定的情况中,当队尾到达了最后一个元素,再次移动时将队尾指针指向第一个位置,充分利用队头出队的空位置。
判空条件: front == rear
判满条件:(rear + 1) % QueueSize == front
队列长度:(rear - front + QueueSize) % QueueSize
单链表实现即可。队头 和 队尾两个指针,时间复杂度低。
出队:返回队头元素,头指针后移
入队:新建节点,将最后一个元素指向新节点,队尾指针后移指向新节点
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
queue.add(new Integer(1));
queue.add(new Integer(2));
queue.add(new Integer(3));
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.peek());
System.out.println(queue.poll());
System.out.println(queue.peek());
}
// 输出结果:1 1 2 2 3
打印机
打印任务按时间入队,依次出队完成打印任务。
图的广度优先遍历
思路:两个栈stack1和stack2,入队时向stack1中压入元素,出队时,从stack2中出栈元素。stack2中的元素从stack1中获取。
public class StackQueue<E> {
Stack<E> stack1 = new Stack<>();
Stack<E> stack2 = new Stack<>();
public void push(E node) {
stack1.push(node);
}
public E pop() {
if (stack2.empty()) {
while (!stack1.empty())
stack2.push(stack1.pop());
}
return stack2.pop();
}
public static void main(String[] args) {
StackQueue<Integer> sq = new StackQueue<>();
sq.push(new Integer(1));
sq.push(new Integer(2));
System.out.println(sq.pop());
sq.push(new Integer(3));
sq.push(new Integer(4));
System.out.println(sq.pop());
sq.push(new Integer(5));
System.out.println(sq.pop());
System.out.println(sq.pop());
System.out.println(sq.pop());
}
}
//1 2 3 4 5
思路:通过两个队列q1和q2来实现栈。入栈时,直接将元素放入q1中;出栈时,此时出栈元素应为q1队尾元素,将q1中元素依次出队放入q2,最后一个出队的元素就是需要出栈的元素。
public class QueueStack<E> {
Queue<E> q1 = new LinkedList<E>();// 入栈
Queue<E> q2 = new LinkedList<E>();// 出栈
// isEmpty()
public boolean isEmpty() {
return q1.isEmpty() && q2.isEmpty();
}
//入栈
public void push(E data) {
q1.add(data);
}
//出栈
public E pop() {
if (q1.size() == 1) {
return q1.poll();
} else {
while (q1.size() != 1) {
q2.add(q1.poll());
}
E tem = q1.poll();
while (!q2.isEmpty()) {
q1.add(q2.poll());
}
return tem;
}
}
public static void main(String[] args) {
QueueStack<Integer> qs = new QueueStack<>();
qs.push(new Integer(1));
qs.push(new Integer(2));
qs.push(new Integer(3));
qs.push(new Integer(4));
System.out.println(qs.pop());
qs.push(new Integer(5));
System.out.println(qs.pop());
System.out.println(qs.pop());
System.out.println(qs.pop());
System.out.println(qs.pop());
}
}
// 4 5 3 2 1
入栈顺序为:1,2,3,4,5,不可能的出栈顺序是:(C)
A. 5,4,3,2,1
B. 1,2,3,4,5
C. 3,4,5,1,2
D. 3,4,5,2,1