一、什么是栈?
栈:是一种“操作受限”的线性表,只允许在一端插入和删除数据,特点是先进后出(FILO)
二、为什么需要栈?
1、任何数据结构都是对特定应用场景的抽象,数组和链表随人使用起来更加灵活,但却暴露了几乎所有操作,难免引发错误操作风险
2、所以当某些数据集合只涉及在某端插入和删除数据,且满足先进后出特性时,那么应该首选择栈这种数据结构
三、实现栈:
栈是一种操作受限的数据结构,可以用数组和链表实现
无论是顺序栈还是链式栈。入栈,出栈只涉及栈顶个别数据的操作,所以时间复杂度都是0(1)
顺序栈实现 :
package dataStruct.stack;
/*
* 栈数组实现
* @author chao
*/
public class StackOfArray {
Object[] items;
int n;
int count;
public int size() {
return count;
}
public StackOfArray(int n) {
this.items = new Object[n];
this.n = n;
this.count = 0;
}
public void ensureCapacitity(int n) {
Object[] temp = this.items;
items = new Object[n * 2 + 1];
for (int i = 0; i < temp.length; i++) {
items[i] = temp[i];
}
}
public boolean push(Object ele) {
if (count == n) {
ensureCapacitity(n);
}
;
items[count] = ele;
count++;
return true;
}
public Object pop() {
if (count == 0)
return false;
Object temp = items[count - 1];
count--;
return temp;
}
}
链式栈 :
package dataStruct.stack;
/**
* 栈链表实现
* @author chao
*/
public class StackOfSinglyLinked {
//头节点
private Node head;
//有多少个节点
private int size;
public boolean isEmpty() {
return size() == 0;
}
public void push(Object ele) {
//新增加的节点作为链表头节点
Node oldNode = head;
head = new Node(ele, oldNode);
size++;
}
public Object pop() {
if (head == null) {
return -1;
}
Object value = head.ele;
head = head.next;
size--;
return value;
}
public void clear() {
this.head = null;
size = 0;
}
public int size() {
return size;
}
public String toString() {
if (size <= 0) {
return "[]";
}
Node p = head;
StringBuilder sb = new StringBuilder(size);
sb.append("]");
while (p != null) {
sb.append(p.ele);
if (p.next != null) {
sb.append(",");
} else {
sb.append("[");
}
p = p.next;
}
return sb.reverse().toString();
}
// 链表节点
private static class Node {
Object ele;
Node next;
public Node(Object ele, Node next) {
this.ele = ele;
this.next = next;
}
}
}
四、栈的应用
1、栈在函数调用中的应用
操作系统给每个线程分配了一块独立的内存空间,这块内存被组织成“栈”这种结构,用来存储函数调用时的临时变量。每进入一个函数,就会将其中的临时变量作为栈帧入栈,当被调用函数执行完成,返回之后,将这个函数对应的栈帧出栈。
2、栈在表达式求值中的应用(比如:34+13*9+44-12/3)
利用两个栈,其中一个用来保存操作数,另一个用来保存运算符。我们从左向右遍历表达式,当遇到数字,我们就直接压入操作数栈;当遇到运算符,就与运算符栈的栈顶元素进行比较,若比运算符栈顶元素优先级高,就将当前运算符压入栈,若比运算符栈顶元素的优先级低或者相同,从运算符栈中取出栈顶运算符,从操作数栈顶取出2个操作数,然后进行计算,把计算完的结果压入操作数栈,继续比较
3、栈在括号匹配中的应用(比如:{}{[()]()})
用栈保存为匹配的左括号,从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式。
当所有的括号都扫描完成之后,如果栈为空,则说明字符串为合法格式;否则,说明未匹配的左括号为非法格式。
package dataStruct.stack;
import java.util.HashMap;
import java.util.Map;
/*
* 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串
*/
public class Solution {
public static void main(String[] args) {
Solution so = new Solution();
System.out.println( so.isValid("]"));
}
Map<Character, Character> map;
public Solution() {
this.map = new HashMap<>();
map.put('}', '{');
map.put(')', '(');
map.put(']', '[');
}
public boolean isValid(String str) {
StackOfSinglyLinked stack = new StackOfSinglyLinked();
char[] array = str.toCharArray();
for (int i = 0; i < array.length; i++) {
char p = array[i];
//从左到右一次扫描字符串,当扫描到左括号时,则将其压入栈中;
if (map.containsKey(p)) {
//当扫描到右括号时,从栈顶取出一个左括号,如果能匹配上,则继续扫描剩下的字符串。
Object pop = stack.pop();
//如果扫描过程中,遇到不能配对的右括号,或者栈中没有数据,则说明为非法格式
if (pop != map.get(p)) {
return false;
}
} else {
stack.push(p);
}
}
return stack.isEmpty();
}
}
4.如何实现浏览器的前进后退功能?
我们使用两个栈X和Y,我们把首次浏览的页面依次压如栈X,当点击后退按钮时,再依次从栈X中出栈,并将出栈的数据一次放入Y栈。当点击前进按钮时,我们依次从栈Y中取出数据,放入栈X中。当栈X中没有数据时,说明没有页面可以继续后退浏览了。当Y栈没有数据,那就说明没有页面可以点击前进浏览了。
package dataStruct.stack;
/**
* 栈实现浏览器后退前进功能
* @author chao
*
*/
public class SampleBrowser {
String currentPage; //当前页面
StackOfSinglyLinked back;
StackOfSinglyLinked forward;
public SampleBrowser() {
this.back = new StackOfSinglyLinked();
this.forward = new StackOfSinglyLinked();
}
//模拟打开一个页面
public void open(String url) {
//如果当前页面不为空,也就是之后再次打开页面就把之前的页面放入back栈中
if(currentPage!=null) {
back.push(this.currentPage);
forward.clear();
}
showUrl(url,"open");
}
public void showUrl(String url,String prefix) {
System.out.println(prefix+"page: "+url);
this.currentPage=url;
}
public String goBack() {
if(canGoBack()) {
//回退页面的时候,将当前打开的页面放入forward栈中
forward.push(currentPage);
//弹出要回退到的页面
String preUrl = (String) back.pop();
//打开要回退到的页面
showUrl(preUrl, "back");
return preUrl;
}
System.out.println("Cannot go back, no pages behind.");
return null;
}
public String goForward() {
if(canGoForward()) {
//前进页面的时候,将当前打开的页面放入back栈中
back.push(currentPage);
//弹出要前进的页面
String afterUrl = (String) forward.pop();
//打开要前进的页面
showUrl(afterUrl, "forward");
return afterUrl;
}
System.out.println("Cannot go forward, no pages forward.");
return null;
}
public boolean canGoBack() {
return back.size()>0;
}
public boolean canGoForward() {
return forward.size()>0;
}
}
思考
1、我们在讲栈的应用时,讲到用函数调用栈来保存临时变量,为什么函数调用要用“栈”来保存临时变量呢?用其他数据结构不行吗?
函执调用的执行顺序符合栈的后进先出特点,局部变量的生命周期应该和函数一致,随着方法的调用结束,会自动释放栈帧内存,所以用栈保存临时变量
2、我们都知道,JVM 内存管理中有个“堆栈”的概念。栈内存用来存储局部变量和方法调用,堆内存用来存储 Java 中的对象。那 JVM 里面的“栈”跟我们这里说的“栈”是不是一回事呢?如果不是,那它为什么又叫作“栈”呢?
内存中的堆栈和数据结构堆栈不是一个概念,可以说内存中的堆栈是真实存在的物理区,数据结构中的堆栈是抽象的数据存储结构。
内存空间在逻辑上分为三部分:代码区、静态数据区和动态数据区,动态数据区又分为栈区和堆区。
代码区:存储方法体的二进制代码。高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)控制代码区执行代码的切换。
静态数据区:存储全局变量、静态变量、常量,常量包括final修饰的常量和String常量。系统自动分配和回收。
栈区:存储运行方法的形参、局部变量、返回值。由系统自动分配和回收。
堆区:new一个对象的引用或地址存储在栈区,指向该对象存储在堆区中的真实数据。
摘自评论