文章目录
一、栈
1、栈的定义与功能
- 栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。
- 栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
- 压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
- 出栈:栈的删除操作叫做出栈。出数据在栈顶。
方法 | 功能 |
---|---|
Stack() | 构造一个空的栈 |
E.push(E e) | 将e入栈,并返回e |
E.pop() | 将栈顶元素出栈并返回 |
E.peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素个数 |
boolean empty() | 检测栈是否为空 |
public class TestDemo {
public static void main(String[] args) {
Stack<Integer> stack=new Stack<>();
stack.push(12);
stack.push(24);
stack.push(48);
stack.push(36);
int val=stack.peek();//获取栈顶元素但不删除
System.out.println("栈顶元素是:"+val);//36
int val2=stack.pop();//删除栈顶元素
System.out.println("被删除的栈顶元素是:"+val2);
if(stack.empty()){
System.out.println("栈空");
}else{
System.out.println("栈的有效元素个数是:"+stack.size());//获取栈中有效的元素个数
}
}
}
2、模拟实现一个栈
从上图中可以看到, Stack继承了Vector ,Vector和ArrayList类似,都是动态的顺序表,不同的是Vector是线程安 全的。
【1】、源码中的capacityIncrement
扩容的源码中capacityIncrement
的意思:
【2】、初始化栈
- 1、用一个数组代表栈,这里选择元素为基本数据类型的数组。
- 2、用一个
usedSize
变量代表当前存储的有效的数据个数,也可以表示当前可以存放的数据元素的下标。 - 3、设置一个栈的初始大小
DEFAULT_CAPACITY
。 - 4、设置一个构造方法可以初始化数组。
public class MyStack {
/**
* usedSize:表示当前存储的有效的数据个数,
* 也可以表示当前可以存放的数据元素的下标
*/
public int[] elem;
public int usedSize;
public static final int DEFAULT_CAPACITY=10;
//初始栈的大小,类似于源码当中的capacityIncrement
public MyStack(){
elem=new int[DEFAULT_CAPACITY];
}
}
【3】、压栈
- 1、压栈之前要判断栈是否满了
- 2、要设置一个栈的初始大小,才能比较栈是否满了
- 3、栈满了,就要进行二倍扩容,使用
Arrays.copyOf
/**
* 判断栈当前是否是满的
* @return
*/
public boolean isFull(){
if(usedSize==elem.length){
return true;
}
return false;
}
/**
* 压栈
* @param val 要入栈的元素值
*/
public void push(int val){
/**
* 1、isFull()判断栈是否是满的
* */
if(isFull()){
//栈满了,就进行二倍扩容
elem=Arrays.copyOf(elem,2*elem.length);
}
//存放到当前下标,同时usedSize需要自增
elem[usedSize]=val;
usedSize++;
}
【4】、出栈
如果元素是引用数据类型的出栈,则思路为:
elem[usedSize-1]=null;
usedSize--;
但由于我们定义的数组的元素是int类型,因此思路为:
直接让usedSize--
,这样新的数据添加进来就会覆盖应该被删除的数据。
- 1、在出栈之前也要判断栈是否为空
- 2、如果栈为空,则抛出一个异常
/**
* 判断是否为空
* @return
*/
public boolean isEmpty(){
return usedSize==0;
}
重新new一个class,写一个异常:
//运行时异常,则继承RuntimeException
public class EmptyStackException extends RuntimeException{
//加上构造方法
public EmptyStackException(){
}
public EmptyStackException(String msg){
super(msg);
}
}
/**
* 删除栈顶元素,
* 分析:这里不是引用数据类型,而是基本数据类型
* 删除就是直接让usedSize--,这样新的数据添加进来就会覆盖应该被删除的数据
* @return
*/
public int pop(){
if(isEmpty()){
throw new EmptyStackException("栈为空了!");
}
int oldVal=elem[usedSize-1];
usedSize--;
return oldVal;
}
【5】、获取栈顶元素
- 1、判断是否为栈是否为空
- 2、如果栈不空的话就返回栈顶元素的值
public int peek(){
if(isEmpty()){
throw new EmptyStackException("栈为空了,无法获取栈顶元素");
}
return elem[usedSize-1];
}
【6】获取元素个数
public int getUsedSize(){
return usedSize;
}
【7】源码
源码一:
import java.util.*;
/**
* @author Susie-Wen
* @version 1.0
* @description:模拟实现一个栈
* @date 2022/7/6 19:38
*/
public class MyStack {
/**
* usedSize:表示当前存储的有效的数据个数,
* 也可以表示当前可以存放的数据元素的下标
*/
public int[] elem;
public int usedSize;
public static final int DEFAULT_CAPACITY=10;
//初始栈的大小,类似于源码当中的capacityIncrement
public MyStack(){
elem=new int[DEFAULT_CAPACITY];
}
/**
* 压栈
* @param val 要入栈的元素值
*/
public void push(int val){
/**
* 1、isFull()判断栈是否是满的
* */
if(isFull()){
//栈满了,就进行二倍扩容
elem=Arrays.copyOf(elem,2*elem.length);
}
//存放到当前下标,同时usedSize需要自增
elem[usedSize]=val;
usedSize++;
}
/**
* 判断栈当前是否是满的
* @return
*/
public boolean isFull(){
if(usedSize==elem.length){
return true;
}
return false;
}
/**
* 删除栈顶元素,
* 分析:这里不是引用数据类型,而是基本数据类型
* 删除就是直接让usedSize--,这样新的数据添加进来就会覆盖应该被删除的数据
* @return
*/
public int pop(){
if(isEmpty()){
throw new EmptyStackException("栈为空了!");
}
int oldVal=elem[usedSize-1];
usedSize--;
return oldVal;
}
/**
* 判断是否为空
* @return
*/
public boolean isEmpty(){
return usedSize==0;
}
public int peek(){
if(isEmpty()){
throw new EmptyStackException("栈为空了,无法获取栈顶元素");
}
return elem[usedSize-1];
}
public int getUsedSize(){
return usedSize;
}
}
源码二:
public class MyStack {
int[] array;
int size;
public MyStack(){
array = new int[3];
}
public int push(int e){
ensureCapacity();
array[size++] = e;
return e;
}
public int pop(){
int e = peek();
size--;
return e;
}
public int peek(){
if(empty()){
throw new RuntimeException("栈为空 ,无法获取栈顶元素"); }
return array[size-1];
}
public int size(){
return size;
}
public boolean empty(){
return 0 == size;
}
private void ensureCapacity(){
if(size == array.length){
array = Arrays.copyOf(array, size*2); }
}
}
3、栈插入的时空复杂度
栈的底层就是一个数组。
1、对于栈的顺序存储:
入栈的时间复杂度:O(1)
出栈的时间复杂度:O(1)
2、假设单向链表尾插法插入数据:
入栈->尾插法:O(N)
出栈->尾插法:O(N)
3、假设单向链表头插法插入数据:
入栈->头插法:O(1)从头结点插入,不需要遍历列表
出栈->头插法:O(1)删除头结点
因此如果栈使用单向链表的形式存储,则需要使用头插法。
4、假设双向链表尾插法插入数据:
入栈->尾插法:O(1)
出栈->尾插法:O(1)
5、假设双向链表头插法插入数据:
入栈->尾插法:O(1)
出栈->尾插法:O(1)
4、例题
【1】、不可能的出栈顺序
有一道题目:一个栈的输入顺序是ABCDEF,那么不可能出现的出栈顺序是什么()
A、DCBAEF
B、ACBEDF
C、DEFBCA
D、CDBAFE
这道题的答案是C。分析:这些题一般都默认在进栈的过程当中可以出栈。
【2】、中缀转后缀表达式
1、中缀表达式转后缀表达式。
中缀表达式:a+b*c+(d*e+f)*g
整个后缀表达式当中没有括号,但是仍然可以区分优先级。
例题一:
中缀表达式:2+(3*5)
后缀表达式:235*+
例题二:
中缀表达式:(2+3)*5
后缀表达式:23+5*
【3】、括号匹配
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。
有效字符串需满足:
左括号必须用相同类型的右括号闭合。 左括号必须以正确的顺序闭合。示例 1:
输入:s = “()” 输出:true示例 2:
输入:s = “()[]{}” 输出:true示例 3:
输入:s = “(]” 输出:false示例 4:
输入:s = “([)]” 输出:false示例 5:
输入:s = “{[]}” 输出:true提示:
仅由括号 ‘()[]{}’ 组成
分析:
1、可能出现的情况
- (【)】:左右字符不匹配
- ()):右括号多
- (():左括号多
2、使用栈来解决
如果是左括号就入栈,右括号就出栈。
当出栈的时候可能出现的情况:
【1】当匹配成功: 满足以下两点
- 1、字符串遍历完成
- 2、栈为空
【2】当不匹配的情况:
- 1、右括号多:当前下标是右括号,但是栈为空
- 2、左括号多:字符串遍历完成,但是栈仍然不为空
解法一
class Solution {
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{
//到这步说明是右括号
//此时要判断栈是否为空
if(stack.empty()){
//栈空,说明右括号多!
return false;
}
char top=stack.peek();
if(ch==')' && top=='(' || ch==']' && top=='[' || ch=='}' && top=='{'){
stack.pop();//说明当前的左右括号匹配,因此左括号出栈
}else{
return false;//说明左右括号不匹配
}
}
}
if(stack.empty()){
return true;
}else{
return false;//左括号多
}
}
}
时间复杂度:O(n)
空间复杂度:O(n)
解法二
分析:解法一和解法二都是使用栈,但是解法二看上去更加简洁,因为解法二是将所有的右括号访入栈当中,而解法一是将所有的左括号进栈。
class Solution {
public boolean isValid(String s) {
Stack<Character>stack = new Stack<Character>();
for(char c: s.toCharArray()){
if(c=='(')stack.push(')');
else if(c=='[')stack.push(']');
else if(c=='{')stack.push('}');
else if(stack.isEmpty()||c!=stack.pop())return false;
}
return stack.isEmpty();
}
}
ToCharArray( )
的用法,将字符串对象中的字符转换为一个字符数组。
详解释就是:字符串转换成字符数组后,每个字符的ASC码与字符T的ASC码进行二进制异或运算。
解法三
class Solution {
public boolean isValid(String s) {
int length = s.length() / 2;
for (int i = 0; i < length; i++) {
s = s.replace("()", "").replace("{}", "").replace("[]", "");
}
return s.length() == 0;
}
}
分析:这个思路很好,不需要栈的复杂的解法,使用的是java当中的方法replace
为了搞清楚规律,我随便测试了一组数据:
public class Test01 {
public static void main(String[] args) {
String s="(}(([[[((]]]()[[[]]))){{{{}}}";
System.out.println("原始的s:"+s);
s=s.replace("()","");
System.out.println("新的替换了()的s:"+s);
s=s.replace("[]","");
System.out.println("新的替换了[]的s:"+s);
s=s.replace("{}","");
System.out.println("新的替换了{}的s:"+s);
}
}
从上面的执行结果可以看出,每次replace都会将一组满足条件的括号删除,因此可以考虑写一个循环。
- 循环只要变量字符串的前一半字符就可以了,因为如果括号匹配的话,那么一半刚好是所有左括号。
【4】、递归实现单链表逆序打印
/**
* 递归实现单链表逆序打印
* @param head
*/
public void printList(ListNode head){
if(head==null){
//单链表无元素
return;
}
if(head.next==null){
//说明此时单链表只有一个元素
System.out.println(head.val+" ");
return;
}
//单链表至少两个结点
printList(head.next);//循环结束之后从这里继续执行
System.out.println(head.val+" ");
}
【5】非递归实现单链表逆序打印
上面的递归其实就是模拟栈的实现,因此非递归的做法就是使用栈来逆序打印单链表:
- 利用了栈的先进后出的特点
/**
* 非递归实现单链表逆序打印
*/
public void printList2(){
Stack<LinkNoed> Stack=new Stack<>();
LinkNode cur=head;
while(cur!=null){
stack.push(cur);
cur=cur.next;
}
while(!stack.empty()){
LinkNode top=stack.pop();
System.out.println(top.val+" ");
}
}
两种方式的源码:
// 递归方式
void printList(Node head){
if(null != head){
printList(head.next);
System.out.print(head.val + " ");
}
}
// 循环方式
void printList(Node head){
if(null == head){
return;
}
Stack<Node> s = new Stack<>();
// 将链表中的结点保存在栈中
Node cur = head;
while(null != cur){
s.push(cur);
cur = cur.next;
}
// 将栈中的元素出栈
while(!s.empty()){
System.out.print(s.pop().val + " ");
}
}
【5】、逆波兰表达式求值
根据 逆波兰表示法,求表达式的值。
- 有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
注意 两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。- 示例 1:
输入:tokens = [“2”,“1”,“+”,“3”,“*”] 输出:9 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) *3) = 9- 示例 2:
输入:tokens = [“4”,“13”,“5”,“/”,“+”] 输出:6 解释:该算式转化为常见的中缀算术表达式为:(4 + (13/ 5)) = 6- 示例 3:
输入:tokens = [“10”,“6”,“9”,“3”,“+”,“-11”,““,”/“,””,“17”,“+”,“5”,“+”]
输出:22
解释:该算式转化为常见的中缀算术表达式为: ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
思路:
- 第一个数组是push数组,表示入栈次序。第二个数组是pop数组,表示出栈序列。
- 定义两个变量i和j,分别遍历push和pop数组,push依次入栈,如果i和j对应的数字相同,则把栈当中的元素出栈,否则就让push入栈。
- 结果:当j遍历完了数组,并且栈也是空的,则说明pop数组就是push数组的出栈序列。
- 结果:当j遍历完了数组,但是栈里面仍然存在元素,则pop数组不是push数组的出栈序列。
- 定义一个方法
isOperation
来判断是否是除了加减乘除之外的有效字符。 - 由于是字符串比较因此必须使用
equals
。 - 最开始定义栈的时候,栈里面的元素要变成
Integer
,因为最后要把数字相加减,而不要定义成String
注意下列代码当中的num1和num2的顺序不能改变,因为num2先被pop出来。此外,自定义函数如果是加减乘除符号的话return的是true而非false。
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack=new Stack<>();
for(String x:tokens){
//要把所有的数字全都放入数字当中
if(!isOperation(x)){
//!isOperation(x):表示字符不是加减乘除
stack.push(Integer.parseInt(x));//把字符串转变成整数再放到栈中
}else{
//弹出两个元素
int num2=stack.pop();
int num1=stack.pop();
switch(x){
case "+" :
stack.push(num1+num2);
break;
case "-" :
stack.push(num1-num2);
break;
case "*" :
stack.push(num1*num2);
break;
case "/" :
stack.push(num1/num2);
break;
}
}
}
return stack.pop();
}
//定义一个方法来判断是否是除了加减乘除之外的有效字符
//由于是字符串比较因此必须使用equals
private boolean isOperation(String opera){
if(opera.equals("+") || opera.equals("-") || opera.equals("*") || opera.equals("/")){
return true;
}
return false;
}
}
【6】、栈的压入、弹出序列
描述
- 输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。
- 0<=pushV.length == popV.length <=1000
- -1000<=pushV[i]<=1000
- pushV 的所有数字均不相同
- 示例1
输入: [1,2,3,4,5],[4,5,3,2,1]
返回值: true
说明: 可以通过push(1)=>push(2)=>push(3)=>push(4)=>pop()=>push(5)=>pop()=>pop()=>pop()=>pop() 这样的顺序得到[4,5,3,2,1]这个序列,返回true- 示例2
输入: [1,2,3,4,5],[4,3,5,1,2]
返回值: false
说明:
由于是[1,2,3,4,5]的压入顺序,[4,3,5,1,2]的弹出顺序,要求4,3,5必须在1,2前压入,且1,2不能弹出,但是这样压入的顺序,1又不能在2之前弹出,所以无法形成的,返回false
import java.util.*;
import java.util.ArrayList;
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
if(pushA.length==0 || popA.length==0){
return false;
}
Stack<Integer> stack=new Stack<>();
int j=0;
for(int i=0;i<pushA.length;i++){
stack.push(pushA[i]);
while(j<popA.length && !stack.empty() &&
stack.peek()==popA[j]){//把栈里面一样的都弹出去
//这里如果是引用类型则不能使用==,要使用equals
stack.pop();
j++;
}
}
return stack.empty();
}
}