目录
栈
一、栈的基本概念
1、栈的定义
栈:一种特殊的线性表,其 只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。压栈:栈的插入操作叫做进栈/压栈/入栈, 入数据在栈顶。出栈:栈的删除操作叫做出栈。 出数据在栈顶。
2、栈的基本操作
Stack():初始化一个空栈
E push(E e) :将e入栈,并返回e
E pop():将栈顶元素出栈并返回。
E peek():获取栈顶元素
int size() :获取栈中有效元素个数
boolean empty():检测栈是否为空
二、栈的顺序存储结构
1、基于顺序表实现栈
采用顺序存储的栈称为顺序栈,它利用一组地址连续的存储单元存放自栈底到栈顶的数据元素,我们创建一个数组来实现,同时设置一个usedSize表示数组里存储的有效数据 , 然后在构造方法中默认设置一个长度为10的数组. 栈的顺序存储结构可描述为以下代码:
2、判断栈是否为空
如果栈是空的的话 那么有效数据usedSize为0 则为以下代码表示。
public boolean isEmpty(){
return usedSize==0;
}
3、判断栈是否已经满
此时也就是判断有效数据是否跟数组长度相等, 如果相等那么栈为满.
public boolean isFull(){
return usedSize == elem.length;
}
4、实现入栈
栈的操作是在一端进行的,所以我们选择在顺序表的尾部进行操作:入栈用尾插来实现、出栈用尾删来实现、取栈顶元素就是取尾部元素。
注意:我们在用顺序表来实现栈的时候选取的是在顺序表的尾部来进行的,但这并不是说在顺序表的头部就不能实现。只是在头部实现的时候,不管是头插还是头删都要进行元素的搬运,时间复杂度太高,所以不选取。
入栈也就是在数组上添加元素 添加完后 要记得对有效数据usedSize进行++操作 .如果栈满了 那么我们需要对数组进行扩容 ,本文是对数组进行二倍扩容
public void push(int val){
if (isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize]=val;
usedSize++;
}
5、实现出栈
如果栈是空的我们抛出异常处理 我们创一个异常
public class EmptyException extends RuntimeException {
public EmptyException() {
}
public EmptyException(String message) {
super(message);
}
}
顺序表出栈就是减少一个有效元素
public int pop(){
if (isEmpty()){
throw new EmptyException("栈是空的!");
}
usedSize--;
return elem[usedSize];
}
6、查看栈顶元素
因为usedSize是有效数据 , 所以usedSize-1 就是数组最后一个元素
public int peek(){
if (isEmpty()){
throw new EmptyException("栈是空的!");
}
return elem[usedSize-1];
}
7、完整代码
import java.util.Arrays;
import java.util.Stack;
public class MyStack {
public int[] elem;
public int usedSize;
public MyStack(){
this.elem = new int[10];
}
public void push(int val){
if (isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize]=val;
usedSize++;
}
public boolean isFull(){
return usedSize == elem.length;
}
public int pop(){
if (isEmpty()){
throw new EmptyException("栈是空的!");
}
usedSize--;
return elem[usedSize];
}
public boolean isEmpty(){
return usedSize==0;
}
public int peek(){
if (isEmpty()){
throw new EmptyException("栈是空的!");
}
return elem[usedSize-1];
}
}
三、栈的链式存储结构
1.基于链表实现栈
用链表来实现栈:在用链表实现栈的时候,我们操作的一段选取头端。用头插表示入栈,用头删来表示出栈,取栈顶元素就是取链表的head的值。
下边是基于链表实现的栈:
public class MyStackForLinkedList {
// 先创建一个链表
class Node{
public int val;
public Node next;
public Node(int val){
this.val = val;
}
}
private Node head = null;
2.实现入栈
用链表实现入栈也就是用链表实现头插 如果是空链则将头节点设置为newNode 如果头不为空 则新节点的next指向头节点 然后就将头节点head设置为newNode节点
public void push(int val) {
// 将要插入的元素放在链表里边
Node newNode = new Node(val);
// 空链表
if (head == null) {
head = newNode;
return;
}
// 处理一般情况
newNode.next = head;
head = newNode;
}
3、实现出栈
我们将返回类型设置为Integer 是因为 链表为空时返回 null 如果链表只有一个元素时 只需将头节点置空即可 因为我们需要返回一个值 所以设置ret保存链表的值
如果是其他情况: 我们先记录下头节点的值 留在最后返回 然后 让头节点往后走即可 便可实现出栈
public Integer pop() {
if (head == null) {
return null;
}
// 链表之中只有一个元素
if (head.next == null) {
int ret = head.val;
head = null;
return ret;
}
// 一般情况的处理
int ret = head.val;
head = head.next;
return ret;
}
4、查看栈顶元素
链表为空返回null 链表不为空时头节点便是栈顶元素 所以返回头节点元素即可
public Integer peek() {
// 特殊情况:要是链表是空的,没得取,返回null
if (head == null) {
return null;
}
return head.val;
}
5、完整代码
public class MyStackForLinkedList {
// 先创建一个链表
class Node{
public int val;
public Node next;
public Node(int val){
this.val = val;
}
}
private Node head = null;
public void push(int val) {
Node newNode = new Node(val);
// 特殊情况的处理,空链表
if (head == null) {
head = newNode;
return;
}
// 处理一般情况
newNode.next = head;
head = newNode;
}
public Integer pop() {
if (head == null) {
return null;
}
if (head.next == null) {
int ret = head.val;
head = null;
return ret;
}
int ret = head.val;
head = head.next;
return ret;
}
public Integer peek() {
if (head == null) {
return null;
}
return head.val;
}
}
队列
一、队列的基本概念
1、队列的定义
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头(Head/Front)
队头(Front):允许删除的一端,又称队首。
队尾(Rear):允许插入的一端。
空队列:不包含任何元素的空表
2、队列的常见基本操作
方法 | 入队列 |
boolean offffer(E e)
| 入队列 |
E poll() | 出队列 |
peek() | 获取队头元素 |
int size() | 获取队列中有效元素个数 |
boolean isEmpty() | 检测队列是否为空 |
二、队列的顺序存储结构
1、基于顺序表实现队列
采用顺序存储的队列称为顺序队列,它利用一组地址连续的存储单元存放自队尾到队头的数据元素,我们创建一个数组来实现,同时设置一个usedSize表示数组里存储的有效数据 , 然后在构造方法中默认设置一个长度为10的数组. 队列的顺序存储结构可描述为以下代码:
public int[] elem;
public int usedSize;
public MyQueue(){
this.elem = new int[10];
}
2、判断队列是否为空
public boolean isEmpty(){
return usedSize==0;
}
3、判断队列是否为满
public boolean isFull(){
return usedSize == elem.length;
}
4、实现入队列
public void offer(int val){
if (isFull()){
elem = Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize] = val;
usedSize++;
}
5、实现出队列
出队列因为是先进先出 所以需要从头出 也就是删除下标为0的点 将数组整体向前移动即可
返回的ret为删除的数据
public int poll(){
if (isEmpty()){
return -1;
}
int ret = elem[0];
for (int i = 0; i <elem.length; i++) {
elem[i]=elem[i+1];
}
usedSize--;
return ret;
}
6、获取队头元素
对头也就是数组的头 所以直接返回0下标的数据即可
public int peek(){
if (isEmpty()){
return -1;
}
return elem[0];
}
对于数组来说,入队列和出队列操作都相对简单,但是可能会造成空间大量浪费
三、队列的链式存储结构
1、基于链表实现队列
在这个我们自己实现的队列中,我们从尾入元素,从头出元素。也就类似于单链表里面的尾插法,从尾插入一个元素,Usedsize++。但我们仍需要注意链表如果为空需要分情况讨论。以下为队列的基本实现
class Node {
public int val;
public Node next;
public Node(int val){
this.val = val;
}
}
public Node front;
public Node rear;
public int usedSize;
2、判断队列是否为空
因为设置了usedSize 当其为0的时候 队列为空
public boolean isEmpty(){
return usedSize==0;
}
3、实现入队列
如果队列为空 那么将队头和队尾都指向node 如果不为空 那么将尾节点的next指向node 然后让尾节点向后走 最后usedSize++.
public void offer(int val){
Node node = new Node(val);
if (front==null){
front = node;
rear = node;
}else {
rear.next = node;
rear = rear.next;
}
usedSize++;
}
4、实现出队列
如果队列不为空 头节点向后走 重要的是走完后 需要判断一下头节点是否为空
public int poll(){
if (isEmpty()) {
return -1;
}
int ret = front.val;
front = front.next;
if (front == null){
rear = null;
}
usedSize--;
return ret;
}
5、获取队头元素
返回队头元素即可
public int peek(){
if(front == null){
return -1;
}
return front.val;
}
6、完整代码
public class MyQueue {
class Node {
public int val;
public Node next;
public Node(int val){
this.val = val;
}
}
public Node front;
public Node rear;
public int usedSize;
public void offer(int val){
Node node = new Node(val);
if (front==null){
front = node;
rear = node;
}else {
rear.next = node;
rear = rear.next;
}
usedSize++;
}
public int poll(){
if (isEmpty()) {
return -1;
}
int ret = front.val;
front = front.next;
if (front == null){
rear = null;
}
usedSize--;
return ret;
}
public int peek(){
if(front == null){
return -1;
}
return front.val;
}
public boolean isEmpty(){
return usedSize==0;
}
}
设计一个循环队列
可以参考链接中的文章
基于Java数组实现循环队列_²º¹⁷旧人不必等的博客-CSDN博客
相关oj题
逆波兰表达式
一、逆波兰表示法
逆波兰表达式又叫做后缀表达式。逆波兰表示法是波兰逻辑学家J・卢卡西维兹(J・ Lukasiewicz)于1929年首先提出的一种表达式的表示方法 [1] 。后来,人们就把用这种表示法写出的表达式称作“逆波兰表达式”。逆波兰表达式把运算量写在前面,把算符写在后面。
中缀表达式:就是我们平时用的表达式,比如 1+((2+3)*4)-5这种表达式
比如:“1+((2+3)*4)-5”转换为后缀表达式是”1 2 3 + 4 * + 5 -”,可以发现后缀表达式里没有括号。
具体可以用这样的方法转换:
这里我们先把中缀表达式写为这样:((1+((2 + 3)*4))-5)在表达式里每一步运算都加上括号,颜色相同的括号相对应
然后我们将对应的操作符(运算符)挪到对应的括号后面,如下:
然后我们去掉括号就得到了后缀表达式:1 2 3 + 4 * + 5 -
我们以此题为例
我们首先创建一个evalRPN方法 并创建一个栈
然后我们用for each 方法去遍历这个字符型数组 此时我们需要判断所遍历的字符是否为有效的算符 所以我们创建一个 isOperation方法来进行判断 如果是返回ture 不是则返回false,此时比较两个类型我们用equals方法 更为合适
此时如果我们遍历的符号不是有效运算符的话那我们需要将他插入栈中 但是此时我们的x是string类型 所以我们需要用Interger.setInt(x)进行转换
然后如果我们遍历的符号是有效的运算符的话 ,根据我们最开始的了解对后缀表达式进行运算我们设置num1,num2来存储从栈取出来的值 我们设置num2为运算符号右边的数值,因为栈是先进后出的, 所以我们先取出num2的值,再取出num1的值. 然后我们用swith语句进行判断 该x为哪个有效字符 并进行相应的运算,并将这个运算值插入栈中 继续运算(此时需要注意的是num1-num2的顺序不能变)
经过以上的代码我们已经完成运算 , 因为运算完会将值插入栈中,所以最后我们返回stack.pop.
完整代码:
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for (String x : tokens) {
if (!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();
}
private boolean isOperation(String x) {
if (x.equals("+") || x.equals("-") || x.equals("*") || x.equals("/")) {
return true;
}
return false;
}
}
最小栈
设计一个支持 push
,pop
,top
操作,并能在常数时间内检索到最小元素的栈。
1 . 先实现两个栈
我们创建两个栈 一个普通栈 一个最小栈
Stack<Integer> stack;
Stack<Integer> minstack;
public MinStack() {
stack = new Stack<>();
minstack = new Stack<>();
}
2 . 实现入栈
我们先将val值入栈到普通栈stack 然后去判断如果最小栈是空的话 那么将val也入栈 ,而如果最小栈不是空栈的话 那么我们就需要判断 最小栈的栈顶的值与要入栈的值 谁大谁小 我们判断需要peek去看 而不是pop取出一个值 当入栈的值小于最小栈栈顶的值时 我们对最下栈实行入栈操作
public void push(int val) {
stack.push(val);
if (minstack.empty()){
minstack.push(val);
}else {
if(val <= minstack.peek()) {
minstack.push(val);
}
}
}
3 . 实现出栈
在普通栈不为空的时候 我们需要记录一下出栈的值 然后用这个值和最小栈作比较 如果相等则最小栈也进行出栈操作(因为我们入栈的时候 如果入栈的值大于最小栈的值 那么最小栈是不进行入栈操作的 所以出栈的时候 我们需要判断普通栈出栈的值是否与最小栈的栈顶值相等)
public void pop() {
if (!stack.empty()){
Integer i = stack.pop();
if(i.equals(minstack.peek())){
minstack.pop();
}
}
}
4 . 获取堆栈顶部的元素
栈不为空时直接返回栈顶元素
public int top() {
if(!stack.empty()){
return stack.peek();
}
return -1;
}
5.获取堆栈中的最小元素
直接返回最小栈栈顶元素即可
public int getMin() {
return minstack.peek();
}
6 . 完整代码
import java.util.Stack;
class MinStack {
Stack<Integer> stack;
Stack<Integer> minstack;
public MinStack() {
stack = new Stack<>();
minstack = new Stack<>();
}
public void push(int val) {
stack.push(val);
if (minstack.empty()){
minstack.push(val);
}else {
if(val <= minstack.peek()) {
minstack.push(val);
}
}
}
public void pop() {
if (!stack.empty()){
Integer i = stack.pop();
if(i.equals(minstack.peek())){
minstack.pop();
}
}
}
public int top() {
if(!stack.empty()){
return stack.peek();
}
return -1;
}
public int getMin() {
return minstack.peek();
}
}
有效的括号:
这个题会给人一个错觉,计算左右括号的数量,然后判断每种左右括号相同,就是有效括号了吧。其实并不是,比如 [{]},明显是各种左右括号都相等,但却不是有效的,因为有效还得跟位置有关系 所以我们仔细去看,却发现了一个特点,跟栈先入后出的特别非常吻合,即若遇到左括号入栈,遇到右括号时将对应栈顶左括号出栈即可,所以只需遍历完所有括号后 stack 仍然为空,这就说明括号是有效的。
我们思路如下
当遇到一个左括号时,在后续遍历中希望有一个相同类型的右括号将其闭合。由于后遇到的左括号要先闭合,因此我们可以将这个左括号放入栈顶。
当遇到一个右括号时,我们则需要将一个相同类型的左括号闭合。此时,我们可以取出栈顶的左括号并判断它们是否是相同类型的括号。如果不是相同的类型,或者栈中并没有左括号,那么字符串s无效,返回false即可。注意到有效字符串的长度得为偶数,如果长度为奇数,直接返回false,就不需要再走后续的遍历判断了。
完整代码
import java.util.Stack;
class Solution {
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
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 (stack.empty()){
return false;
}
char ch = stack.peek();
if (ch == '(' && s.charAt(i)==')' || ch == '{' && s.charAt(i)=='}'|| ch == '[' && s.charAt(i)==']'){
stack.pop();
return true;
}else {
return false;
}
}
}
if (!stack.empty()){
return false;
}
return true;
}
}
栈的压入、弹出序列
栈的压入、弹出序列_牛客题霸_牛客网 (nowcoder.com)
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列
思路如下:
- 建立一个辅助栈
- 当栈为空或者栈的栈顶元素不为弹出序列需要的弹出元素时,将压入序列继续压入栈直到栈顶为弹出元素
- 当栈顶元素为弹出元素时,则弹出该元素
- 当压入序列的所有元素都压入栈时,依然找不到匹配的弹出元素,则第二个序列不是弹出序列
import java.util.*;
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA) {
Stack<Integer> stack = new Stack<>();
int j = 0;
for (int i = 0; i < pushA.length; i++) {
stack.push(pushA[i]);
while ( stack.peek() == popA[j]){
stack.pop();
j++;
}
}
return stack.empty();
}
}
总结
本文详细介绍了栈和队列相关知识和实现,还有一部分oj题的讲解 ,如有不足,还请指正.