目录
1.栈的概念
栈是一种线性表,遵循“先进后出”的原则,简称LIFO(Last In First Out)。在栈顶进行数据的插入和删除。主要方法有三个:E push(E e): 元素e入栈; E pop():出栈;E peek():获取栈顶元素 。其他常用的方法还有int size():获取栈中有效元素的个数; boolean empty():判断栈是否为空 以及Stack():创建一个空栈。
简单使用一下上述方法:
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
//入栈
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
stack.push(6);
System.out.println("栈中的元素有"+stack);
//出栈
System.out.println("出栈的元素为"+stack.pop());
System.out.println("出栈的元素为"+stack.pop());
//查看栈顶元素
System.out.println("栈顶元素为"+stack.peek());
}
测试结果为:
2.栈的模拟实现
上面我们用java中自带的方法实现了一个栈,那么我们怎么自己模拟实现一个栈呢?这里我们首先要知道,栈是在栈顶进行元素的插入与删除的,所以我们在这里使用数组来实现栈,因为在数组的尾部进行元素的插入与删除也是非常方便的。
2.1栈的定义
private Object[] array;//定义一个数组
private int size;//当前栈中有效元素的个数
//无参构造方法:默认栈的大小
public MyStack() {
this(10);//注意:这里的this就是下面的MyStack
}
//有参构造方法
public MyStack(int capacity) {
this.array = new Object[capacity];
}
2.2入栈
我们根据下面的图理解,主要步骤也就是三步:因为入栈是在栈顶插入元素,也就是数组尾部插入元素,然后更新元素个数,最后判断数组是否需要扩容即可。
![](https://i-blog.csdnimg.cn/blog_migrate/def044752d7a069230a23baf8a5fc043.png)
代码实现:
public void push(E element){
array[size] = element;//尾插
size++;//更新节点数
if(array.length == size){//判断插入元素后数组的长度:是否需要扩容
array = Arrays.copyOf(array,array.length<<1);
}
}
2.3 出栈
代码实现:如果栈中没有元素则抛出异常。
public E pop(){
//边界条件:判断当前栈是否为空
if(!isEmpty()){
throw new NoSuchElementException("stack is empty");
}
//返回size-1位置上的值,也就是栈顶值
E val = (E)array[size-1];
size--;//更新元素个数
return val;
}
/**
* 判断当前栈是否为空
*/
private boolean isEmpty() {
if(size == 0){
return false;
}
return true;
}
2.4查看栈顶元素
查看栈顶元素的前提是栈不为空,否则抛出异常提示。
public E peek(){
//边界条件:判断当前栈是否为空
if(!isEmpty()){
throw new NoSuchElementException("stack is empty");
}
//返回size-1位置上的值,也就是栈顶值
E val= (E)array[size-1];
return val;
}
2.5重写toString方法
主要思想:遍历栈中的元素,当不为最后一个元素的时候添加逗号。
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("bottom[");
for (int i = 0; i < size; i++) {
sb.append(array[i]);
if(i < size-1){
sb.append(",");
}
}
sb.append("]top");
return sb.toString();
}
2.6 完整代码:MyStack
public class MyStack<E> {
private Object[] array;//定义一个数组
private int size;//定义一个默认的数组大小
//构造方法:默认的数组大小
public MyStack() {
this(10);//注意:这里的this就是下面的MyStack
}
//有参数传入自定义的数组大小
public MyStack(int capacity) {
this.array = new Object[capacity];
}
/**
* 入栈:利用数组实现入栈,相当于栈顶在数组的尾部,直接将元素值在数组中实现尾插,也就是size位置
*/
public void push(E element){
array[size] = element;//尾插
size++;//更新节点数
if(array.length == size){//判断插入元素后数组的长度:是否需要扩容
array = Arrays.copyOf(array,array.length<<1);
}
}
/**
* 出栈:在数组尾部返回size-1位置上的值
*/
public E pop(){
//边界条件:判断当前数组是否为空
if(!isEmpty()){
throw new NoSuchElementException("stack is empty");
}
//返回size-1位置上的值,也就是栈顶值
E val = (E)array[size-1];
size--;//更新元素个数
return val;
}
/**
* 判断当前数组是否为空
*/
private boolean isEmpty() {
if(size == 0){
return false;
}
return true;
}
/**
* 查看栈顶元素:在数组尾部返回size-1位置上的值
*/
public E peek(){
//边界条件:判断当前数组是否为空
if(!isEmpty()){
throw new NoSuchElementException("stack is empty");
}
//返回size-1位置上的值,也就是栈顶值
E val= (E)array[size-1];
return val;
}
/**
* 重写toString方法
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("bottom[");
for (int i = 0; i < size; i++) {
sb.append(array[i]);
if(i < size-1){
sb.append(",");
}
}
sb.append("]top");
return sb.toString();
}
}
2.7 测试类:MyStackTest
public class MyStackTest {
public static void main(String[] args) {
MyStack<Integer> stack = new MyStack<>();
//入栈
stack.push(1);
stack.push(2);
stack.push(3);
stack.push(4);
stack.push(5);
stack.push(6);
//出栈
System.out.println(stack.pop());
//查看此时的栈顶元素
System.out.println(stack.peek());
System.out.println(stack+" ");
}
}
3.相关OJ题
3.1 有效的括号
(1)问题描述:给定一个只包括 '(',')','{','}','[',']'的字符串s ,判断字符串是否有效。有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
(2)主要思路: 字符串中包含两种类型的符号,一种是左括号,另一种是右括号。我们可以不断的获取字符串中的每一个字符,判断该字符的类型:①如果第一个字符就是右括号则直接false,该字符串一定不是有效的;②如果是左括号我们就让它一直入栈,直到遇到右括号。此时我们就将栈中处于栈顶的左括号弹出,如果它们是匹配的一对括号,则返回true,否则返回false。根据这个特点,如果字符串遍历结束同时栈中的左括号全部出栈,栈为空,则此时的s一定是有效的字符串,否则不是有效字符串。
(3)代码实现
public boolean isValid(String s) {
//创建栈
Stack<Character> stack = new Stack<>();
//给出来的是字符串类型,要先转为字符
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
//让获取到的字符入栈:判断是不是左括号,如果是左括号的话才能入栈,直到遇见第一个右括号为止
if (ch=='[' || ch=='{' || ch=='('){
stack.push(ch);
}else{
//如果此时的符号是右括号,直接返回false
if(stack.isEmpty()){
return false;
}else{
//如果判断的是右括号:这里要写pop将栈顶的元素弹出,之前写的是peek,一直都是最上方的栈顶元素没有变化
char temp = stack.pop();
if(ch == ']' && (temp=='(' || temp == '{')){
return false;
}
if(ch == ')' && (temp=='[' || temp == '{')){
return false;
}
if(ch == '}' && (temp=='(' || temp == '[')){
return false;
}
}
}
}
//这里不能直接返回true,如果只有一个左括号的情况 [,for循环执行完走到这里是要判断栈中是否为空的。
if(stack.isEmpty()){
return true;
}else{
return false;
}
}
3.2 出栈入栈次序匹配
(1)问题描述:给定两个序列,判断第二个序列是不是第一个的出栈情况
(2)主要思路:序列s2使用一个指针i从第一个元素开始移动,遍历序列s1让其不断压入栈,直到s1的栈顶值和s2的指针指向的第一个值相等,此时s1弹出栈顶元素,s2的指针i向后移动。最终判断条件:如果s1的所有元素已经全部压入栈,但是栈中不为空,则s2一定不是s1的有效序列。
(3)代码实现
public boolean validateStackSequences(int[] pushed, int[] popped) {
//创建一个栈
Stack<Integer> stack = new Stack<>();
//i是s2的指针,从第一个值开始不断移动
int i = 0;
//遍历s1的值不断压入栈,直到s1的栈顶元素和s2的第一个值相等,则s1弹出栈顶元素
for (int val : pushed) {
stack.push(val);
//注意:用的是while循环不是if,同时此时要注意前提条件:栈里面不能为空
while (!stack.isEmpty() && popped[i] == stack.peek()){//s1的栈顶值和s2的指针指向的值相等
stack.pop();//弹出s1的栈顶值
i++;//s2的指针不断移动
}
}
//直到for循环结束也就是s1所有的值都已经压入完栈了,但是此时栈中的值非空,则说明s2不是s1对应的出栈情况
if(stack.isEmpty()){
return true;
}else{
return false;
}
}
3.3 求栈的最小值
(1)问题描述:求栈中的最小值:请设计一个栈,除了常规栈支持的pop与push函数以外,还支持min函数,该函数返回栈元素中的最小值。 执行push、pop和min操作的时间复杂度必须为(1)。
(2)主要思路:一个栈无法同时获取栈中最小值和栈顶值,因此使用两个栈完成。 第一个栈进行正常的入栈,可以获取栈顶值;第二个栈则只保存入栈过程中的最小值。
(3)代码实现
//创建两个栈
Stack<Integer> s1 = new Stack<>();
Stack<Integer> s2 = new Stack<>();
//入栈
public void push(int x) {
s1.push(x);//s1正常入栈
if(s2.isEmpty()){//如果s2是空的,当前值就是最小值直接入栈s2
s2.push(x);
}else{//此时s2中存在值
int temp = s2.peek();//先获取s2中的栈顶值
if(x > temp){//如果当前的值大于s2的栈顶值
s2.push(getMin());//将s2中的最小值入栈到时
}
else{
s2.push(x);//否则该值就是最小值入栈s2
}
}
}
//出栈
public void pop() {
s1.pop();
s2.pop();
}
//在s1里面查看栈顶元素
public int top() {
return (int)s1.peek();
}
//在s2中获取最小值
public int getMin() {
return (int) s2.peek();
}
3.4 逆波兰表达式求值
(1)问题描述: 给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 请你计算该表达式,返回一个表示表达式值的整数。比如:输入:tokens = ["2","1","+","3","*"],输出:9,解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9
(2)主要思想:感觉这个和前面的那个有效的括号思想有点类似,这个表达式里面也是包含两种不一样的符号,一个是数字,一个是操作运算符。那么我们也是不断的比那里字符串,判断该符号是数字还是运算符,如果是数字,就将该数字压入栈中;如果是运算符,就将栈中的数字弹出两次进行运算操作,计算完成之后再入栈,等待下一次的运算。最终栈中的元素就是计算得出的结果。
(3)代码实现
public int evalRPN(String[] tokens) {
//创建一个栈
Stack<Integer> stack = new Stack<>();
//遍历字符串
for (String str : tokens) {
//判断当前字符串是数字还是运算符号
if(isNum(str)){
//是数字,就将该字符串类型的数字转为整型数字压入栈
stack.push(Integer.parseInt(str));
}else{//说明是运算符,此时判断是哪种运算符
//此时从栈中连续弹出两个值
int num1 = stack.pop();
int num2 = stack.pop();
int result = 0;
switch (str) {
case "+":
result = num2+num1;
break;
case "-":
result = num2-num1;
break;
case "*":
result = num2*num1;
break;
case "/":
result = num2/num1;
break;
}
//将计算出来的最新的值压入栈中,等待下一次计算
stack.push(result);
}
}
//当整个字符串遍历结束,栈顶的元素值就是最终计算结果
return stack.peek();
}
/**
* 判断当前字符串是数字还是运算符号
* 尝试执行字符串转整型操作,不报错就返回true,否则catch返回false
*/
private boolean isNum(String str) {
try{
Integer.parseInt(str);
return true;
}catch(Exception e){
return false;
}
}
好啦~终于把栈的知识复习了一遍,下来要学习的是队列。终于到周五了,不会有人还在学习吧~