概述
计算机科学中,stack是一种线性的数据结构,只能在其一端添加数据和移除数据。习惯来说,可以移动的一端称为栈顶,另外一端不能操作的数据称为栈底,就如同子弹弹夹或者生活中的书
可以理解为后进先出
链表实现栈
先定义一个接口类,里面提供栈里面所需要的操作,再听过implement接口,便于方法的封装管理
public interface Stack <E>{
boolean push(E value);
E pop();
E peek();
boolean isEmpty();
boolean isFull();
}
关于泛型
在上述代码中,Stack
接口使用了泛型 <E>
,其中 E
是一个类型参数,代表栈中元素的类型。泛型的作用如下:
-
类型安全:通过使用泛型,
Stack
接口可以确保栈中存储的元素类型是相同的,从而避免在运行时出现类型不匹配的问题。 -
代码重用:使用泛型,
Stack
接口可以用于任何类型的元素,而不需要为每种类型创建一个新的栈实现。这使得代码更加通用和可重用。 -
提高代码的可读性:泛型提供了更好的代码可读性,因为它们允许在代码中明确指定栈中元素的类型。
-
减少强制类型转换:使用泛型可以减少源代码中的强制类型转换,从而减少了出错的机会,并提高了代码的可读性。
-
泛型方法:如果
Stack
接口中包含方法,这些方法也可以使用泛型,使得方法能够处理多种类型的参数。 -
泛型擦除:Java编译器会在编译时将泛型类型参数替换为它们的边界(如果没有指定边界,则默认为
Object
),这个过程称为泛型擦除。这保证了泛型代码可以与不使用泛型的旧代码兼容。
通过使用泛型,Stack
接口可以提供更安全、灵活和可维护的代码。
我们来看栈的相关代码
public class LinkListStack <E>implements Stack<E>,Iterable<E> {
public LinkListStack(int capacity) {
this.capacity = capacity;
}
private int capacity;
private int size;
private Node<E> head=new Node<E>(null,null);
static class Node<E>{
E value;
Node next;
public Node(E value, Node next) {
this.value = value;
this.next = next;
}
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p=head.next;//设置栈的头结点
@Override
public boolean hasNext() {
return p!=null;
}
@Override
public E next() {
E Val=p.value;
p=p.next;
return Val;
}
};
}
@Override
public boolean push(E value) {
if(isFull()){
return false;
}
head.next=new Node<E>(value,head.next);
size++;
return true;
}
@Override
public E pop() {
if(isEmpty()){
return null;
}
Node<E> first=head.next;
head.next=first.next;
return first.value;
}
@Override
public E peek() {
if (isEmpty()){
return null;
}
return (E) head.next.value;
}
@Override
public boolean isEmpty() {
return head.next == null;
}
@Override
public boolean isFull() {
return size==capacity;
}
public static void main(String[] args) {
LinkListStack<Integer> linkListStack=new LinkListStack<>(10);
linkListStack.push(1);
linkListStack.push(2);
linkListStack.push(3);
linkListStack.push(4);
linkListStack.pop();
linkListStack.push(5);
linkListStack.push(6);
for(Integer value:linkListStack){
System.out.println(value);
}
}
}
和之前学习的几种数据结构一样,我们通过定义一个栈的类,在里面再通过static对于内部类node进行封装。
Node
类被设计为 LinkListStack
的一个组成部分,用于表示栈中的节点。由于 Node
类不需要访问 LinkListStack
的实例变量或方法,因此它可以被声明为 static
,从而提高内存效率和编译时的优化。我们再来巩固一下static关键字的好处
-
内存效率:
static
内部类不会持有对外部类的引用,因此不会增加外部类实例的内存占用。 -
独立性:
Node
类可能被设计为可以独立于LinkListStack
类使用,或者在其他上下文中重用。 -
访问控制:
static
内部类可以有不同的访问修饰符,例如public
或private
,这可以限制外部类对内部类的访问。 -
避免不必要的依赖:如果
Node
类不需要访问LinkListStack
类的实例变量或方法,那么将其声明为static
可以避免这种不必要的依赖。 -
编译时的优化:由于
static
内部类不依赖于外部类的实例,编译器可以在不涉及外部类的情况下优化static
内部类的代码。
@Override
public boolean isEmpty() {
return head.next == null;
}
@Override
public boolean isFull() {
return size==capacity;
}
在上面这两个判断是否为空或者满的方法中,逻辑就是返回头结点的下一个是否为空和栈的元素个数是否和容量相同,不难理解
@Override
public boolean push(E value) {
if(isFull()){
return false;
}
head.next=new Node<E>(value,head.next);
size++;
return true;
}
再添加操作中,我们先判断栈是否满了,满了的话无法完成添加操作,没有满的话则通过创建该节点,将该节点指向原来头结点的指向,再将头结点指向该节点
@Override
public E pop() {
if(isEmpty()){
return null;
}
Node<E> first=head.next;
head.next=first.next;
return first.value;
}
@Override
public E peek() {
if (isEmpty()){
return null;
}
return (E) head.next.value;
}
再来看移除和返回栈顶的操作,移除方法就是将头结点指向下下个节点,返回操作直接返回头结点指向的值就行。
我们再来看实现迭代器的遍历操作
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p=head.next;//设置栈的头结点
@Override
public boolean hasNext() {
return p!=null;
}
@Override
public E next() {
E Val=p.value;
p=p.next;
return Val;
}
};
}
再便利操作中,当查询到节点的next为空时,代表已经遍历完了栈中所有的元素,可以结束遍历
数组实现栈
我们设置一个top指针,指向下一个加入栈中的元素在数组中存储对应的位置
我们可以利用top指针完成isempty和isfull的判断,不难理解,如下
@Override
public boolean isEmpty() {
return top == 0;
}
@Override
public boolean isFull() {
return top==array.length;
}
我们来看其他方法
添加方法只需要再判断是否为空的基础上将传入的值赋值给top,同时top完成自增
移除方法则是将top--即可,这样子再下一次添加操作中会直接覆盖移除位置的值
返回栈顶的方法不难理解,整体代码如下
@Override
public boolean push(E value) {
if(isFull()){
return false;
}
array[top++] = value;
return true;
}
@Override
public E pop() {
if(isEmpty()){
return null;
}
E value = array[top-1];
top--;
return value;
}
@Override
public E peek() {
if(isEmpty()){
return null;
}
return array[top-1];
}
关于迭代器遍历的方法,当p=0的时候说明遍历结束,然后按照返回值和完成p的递减即可
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p=top;
@Override
public boolean hasNext() {
return p>0;
}
@Override
public E next() {
E value=array[p-1];
p--;
return value;
}
};
}
应用一:有效的括号
我们来看一道题目木
我们来分析一下思路,我们可以将括号分为左括号和有括号部分,如果是左括号就将对应的右括号存入栈里面,再输入式子中的左括号全部处理完成之后再将依次读取的右括号与从栈顶拿出来的右括号判断是否相等,然后移除栈顶的右括号再次进行读取判断,如果最后栈里面还有元素,说明括号不正确。
注意,如果式子是以右括号开头,依然是不正确的,需要补充判断
我们来看代码
public class E01Leetcode20 {
public boolean isVaild(String s) {
ArrayStack<Character> stack = new ArrayStack<>(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '(') {
stack.push(')');
} else if (c == '[') {
stack.push(']');
} else if (c == '{') {
stack.push('}');
} else {
if (stack!=null &&c == stack.peek()) {
stack.pop();
} else {
return false;
}
}
}
return stack.isEmpty();
}
}
后缀表达式求值
思路如下:
遇到数字压入栈
遇到运算符,就从栈弹出两个数字做运算,将结果压入栈
遍历结束,栈中剩下的数字就是结果
将栈顶的值返回即为结果
我们来看代码
public class E02Leetcode150 {
public int evalRPN(String[] tokens) {
LinkedList<Integer> stack = new LinkedList<>();
for (String t : tokens) {
switch (t) {
case "+"->{
Integer b=stack.pop();
Integer a=stack.pop();
stack.push(a+b);
}
case "-"->{
Integer b=stack.pop();
Integer a=stack.pop();
stack.push(a-b);
}
case "*"->{
Integer b=stack.pop();
Integer a=stack.pop();
stack.push(a*b);
}
case "/"->{
Integer b=stack.pop();
Integer a=stack.pop();
stack.push(a/b);
}
default -> {//数字
stack.push(Integer.parseInt(t));
}
}
}
return stack.pop();
}
}
将中缀表达式转换为后缀表达式
类似于实现这样的效果,思路如下,先通过StringBuilder创建一个存储字符串的对象,在用过链表Linkedlist来实现栈,栈里面用来存储运算符
1,遇到非运算符,直接拼串
2,遇到+ - * /
--它的优先级比栈顶运算符高,入栈
--否则把栈里面优先级>=它的都出栈,它再入栈
3,遍历完成,栈里剩余运算符依次出栈
4,带()
--左括号直接入栈,左括号先设置为0
--右括号就把栈里到左括号为止的所有运算符出栈
代码如下
public class E03InfixToSuffix {
public static void main(String[] args) {
}
static int priority(char c){
return switch (c){
case '+','-' -> 1;
case '*','/' -> 2;
case '(' -> 0;
default -> throw new IllegalArgumentException("不合法的运算符"+c);
};
}
static String infixToSuffix(String exp){
LinkedList<Character> stack = new LinkedList<>();
StringBuilder sb = new StringBuilder();
for(int i=0; i<exp.length(); i++){
char c=exp.charAt(i);
switch (c){
case'*','/','+','-'->{
if(stack.isEmpty()){
stack.push(c);
}else {
if(priority(c)>priority(stack.peek())){
stack.push(c);
}else {
while(!stack.isEmpty() && priority(stack.peek())>=priority(c)){
sb.append(stack.pop());
}
stack.push(c);
}
}
}
case'('->{
stack.push(c);
}
case')'->{
while (!stack.isEmpty()&&stack.peek()!='('){
sb.append(stack.pop());
}
stack.pop();
}
default -> {
sb.append(c);
}
}
}
while(!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.toString();
}
}