目录
栈
实现一个MyStack
1. push
2.pop
3.empty
4.peek
栈和链表的结合
括号匹配
栈的压入、弹出序列
最小栈
MinStack
push 编辑
pop
top
getMin
概念区分及思考:
队列
offer(入队列)
poll(出队列)
peek(获取队头元素)
empty(判断是否为空)
设计一个循环队列库
栈
栈(Stack) :一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈 顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据在栈顶。
Stack的方法都很简单,用代码写一下就懂了
public static void main(String[] args) {
Stack<Integer> s = new Stack();
s.push(1);
s.push(2);
s.push(3);
s.push(4);
System.out.println(s.size()); // 获取栈中有效元素个数---> 4
System.out.println(s.peek()); // 获取栈顶元素---> 4
s.pop(); // 4出栈,栈中剩余1 2 3,栈顶元素为3
System.out.println(s.pop()); // 3出栈,栈中剩余1 2 栈顶元素为3
if(s.empty()){
System.out.println("栈空");
}else{
System.out.println(s.size());
}
}
但是我们仅仅了解这一些还不够,我们还需要了解到Stack的源码。
但是很奇怪,为什么这个里面没有方法呢?栈到底是顺序表还是链表呢?
extends Vector<E> 这个里面暗藏玄机。
点进来,发现Stack没有成员变量,其实全部都是继承过来的。
所以可以知道:Stack是一个数组。
实现一个MyStack
栈的功能并不多,我们一个一个来。先创建一个栈:
public class MyStack {
public int[] elem;
public int usedSize;
public static final int DEFAULT_SIZE = 10;
public MyStack() {
this.elem = new int[DEFAULT_SIZE];
}
}
创建一个栈比较简单,创建一个数组就是创建一个栈,把栈的大小初始化为10,usedSize为使用的栈空间,同时也当做当前栈顶的元素的下标
1. push
public int push(int val) {
if(isFull()) { 如果栈已经满了,那么对栈空间扩容两倍
elem = Arrays.copyOf(elem,2*elem.length);
}
this.elem[usedSize] = val; 下标为usedSize的元素为push的值
usedSize++;
return val;
}
public boolean isFull() {
return usedSize == elem.length; 看看usedSize是否等于数组的长度
}
2.pop
public int pop() {
if(empty()) {
throw new MyEmptyStackException("栈为空!");
}
/*int ret = elem[usedSize-1];
usedSize--;
return ret;*/
return elem[--usedSize];
}
usedSize减一,这样就让原来的elem[usedSize]元素没有被指向,也就是弹出来了
public class MyEmptyStackException extends RuntimeException{
public MyEmptyStackException() {
}
public MyEmptyStackException(String message) {
super(message);
}
}
3.empty
public boolean empty() {
return usedSize == 0;
}
4.peek
public int peek() {
if(empty()) {
throw new MyEmptyStackException("栈为空!");
}
return elem[usedSize-1];
}
peek只获取栈顶的元素,并不会删除元素
同时在这里简单介绍一下中缀表达式转后缀表达式,了解即可
栈和链表的结合
逆序打印链表:
递归方式
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;
}
括号匹配
思路:把所有的左括号用栈存放,与又括号一一匹配:
if(ch2 == '[' && ch == ']' || ch2 == '(' && ch == ')' || ch2 == '{' && ch == '}') {
stack.pop();
}else{
return false;
}
但我们还要考虑到栈空了或者与右括号不匹配的情况,完整代码:
public boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(int i = 0; i < s.length();i++) {
char ch = s.charAt(i);
//1. 判断是不是左括号
if(ch == '(' || ch == '[' || ch == '{') {
stack.push(ch);
}else {
if(stack.empty()) {
//2. 遇到了右括号 但是栈为空,此时不匹配!
return false;
}
char ch2 = stack.peek();
//3。 此时 如果满足 这里面的任何一个匹配逻辑 都是匹配的
if(ch2 == '[' && ch == ']' || ch2 == '(' && ch == ')' || ch2 == '{' && ch == '}') {
stack.pop();
}else{
return false;
}
}
}
//4. 当字符串遍历完成了,但是栈不为空,说明左括号还在栈当中没有匹配完成
if(!stack.empty()) {
return false;
}
return true;
}
栈的压入、弹出序列
思路:
运用栈来实现还是比较简单的,将pushA数组的元素按照下标入栈后,再依次与popA里面的元素相比较,只要是依次能够匹配上就说明这是匹配的出栈序列。
public class Solution {
public boolean IsPopOrder(int [] pushA,int [] popA){
Stack<Integer> stack = new Stack<>();
int j = 0; j变量用来遍历popA数组
for(int i = 0 ; i < pushA.length ; i++){
stack.push(pushA[i]);
while(j < popA.length && !stack.empty() && stack.peek() == popA[j]){
stack.pop();
j++;
}
}
return stack.empty();
}
}
最小栈
要实现minStack,仅仅用一个栈是不够的,需要两个栈:
第一个栈的元素正常进出,第二个栈仅仅进出比上一个元素小的元素。
MinStack
class MinStack { Stack<Integer> stack1; Stack<Integer> minStack; public MinStack() { stack1 = new Stack<>(); minStack = new Stack<>(); }
新建两个栈,第一个栈存放所有元素,第二个栈仅存放最小的元素
push
先让需要push的元素进入到stack1里面去,如果minStack为空那么就push到minStack里面去,如果不为空就和minStack的顶端元素相比较,只有小于等于才push到里面去,代码的实现是很简单的:
public void push(int val) { stack1.push(val); if(minStack.empty()){ minStack.push(val); }else{ if(minStack.peek()>=val){ minStack.push(val); } } }
pop
先让stack1pop一个元素出来,如果minStack的元素和这个元素相等,那么minStack的元素也就需要被pop
public void pop() { if(!minStack.empty()){ int x = stack1.pop(); if(minStack.peek()==x){ minStack.pop(); } } }
top
需要注意的是,该top返回的是stack1的元素
public int top() { if(!stack1.empty()){ return stack1.peek(); } return -1; }
getMin
获取的是最小元素,但minStack中的元素就是最小元素,也即返回minStack中的顶元素
public int getMin() { if(minStack.empty()){ return -1; } return minStack.peek(); }
概念区分及思考:
栈、虚拟机栈、栈帧有什么区别呢?
栈:是一种先进后出的数据结构。
虚拟机栈:是JVM的一块内存空间。
栈帧:是在调用函数的过程中,在java虚拟机栈上开辟的一块内存。
目前实现的栈,底层是数组实现,所以也可以叫做顺序栈。请问,由单链表来实现栈是否可以?(链式栈)
但是,双链表就可以完美的实现栈的所有功能,并且时间复杂度为O(1)。
但栈用数组来完成仍然是最优解。
队列
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 入队列:进行插入操作的一端称为队尾(Tail/Rear) 出队列:进行删除操作的一端称为队头 (Head/Front)。
java当中的队列基本上就是双向链表来实现的
但是我们为了加大难度,我们通过单链表来实现一个队列,并且时间复杂度为O(1)。
实现方法就是:通过给队尾加上一个tail,这样就可以让出入队时间复杂度都为O(1).
所以基础的构造就是:
static class ListNode{
public int val;
public ListNode next;
public ListNode(int val) {
this.val = val;
}
}
public ListNode head;
public ListNode tail;
public int Usedsize = 0;//记录队列的大小
offer(入队列)
在这个我们自己实现的队列中,我们从尾入元素,从头出元素。也就类似于单链表里面的尾插法,从尾插入一个元素,Usedsize++。但我们仍需要注意链表如果为空需要分情况讨论。
public void offer(int val){
ListNode node = new ListNode(val);
if(head == null){
head = node;
tail = node;
}else{
tail.next = node;
tail = tail.next;
}
Usedsize++;
}
poll(出队列)
只需要把head节点往后移,再返回之前的头结点的val值就能完成,但仍需要注意head为空的情况
public int poll(){
if(head == null){
return -1; 这里抛异常会更好
}
int ret = head.val;
head = head.next;
if(head == null){ 此时head已经是原head之后的节点了;如果head为空那么tail也需要置空
tail = null;
}
Usedsize--;
return ret;
}
peek(获取队头元素)
返回队头的元素就行了
public int peek(){
if(head == null){
return -1;
}
return head.val;
}
empty(判断是否为空)
public boolean empty(){
return Usedsize == 0;
}
设计一个循环队列库
实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。 环形队列通常使用数组实现。
在这个图中,内圈的是下标,外圈的是存放的元素。这就是一个数组,只不过不同的是首尾相连,变成了一个循环的数组。
我们采用rear表示队尾,front表示队头。
问题:rear怎么从7位置走到0位置?
rear = (rear+1) % elem.length
我们先把基础的参数和构造方法写出来,构造数组来完成
public int[] elem; public int front; public int rear; public MyCircularQueue(int k) { elem = new int[k]; }
enQueue(入队):
public boolean deQueue() { if(isEmpty()){ return false; } front = (front+1) % elem.length; 在没有循环的时候,front++就能满足要求,有循环的时候只有这样才能完成 return true; }
isEmpty(判断是否为空):
public boolean isEmpty() { if(front == rear){ 循环的时候,如果front和rear就代表这个循环队列为空 return true; } return false; }
deQueue(出队):
public boolean deQueue() { if(isEmpty()){ return false; } front = (front+1) % elem.length; 和入队是一样的逻辑 return true; }
isFull(判断是否满了):
public boolean isFull() { if((rear+1) % elem.length == front){ 如果front和当前的rear有这样的关系就说明满了 return true; } return false; }
Front
: 从队首获取元素。如果队列为空,返回 -1 。Rear
: 获取队尾元素。如果队列为空,返回 -1 。public int Front() { if(isEmpty()){ return -1; } return elem[front]; } public int Rear() { if(isEmpty()){ return -1; } int index = rear == 0 ? elem.length-1 : rear-1; 注意这里是一个三目操作符 return index; }
完整代码:
class MyCircularQueue {
public int[] elem;
public int front;
public int rear;
public MyCircularQueue(int k) {
elem = new int[k];
}
public boolean enQueue(int value) {
if(isFull()){
return false;
}
elem[rear] = value;
rear = (rear+1) % elem.length;
return true;
}
public boolean deQueue() {
if(isEmpty()){
return false;
}
front = (front+1) % elem.length;
return true;
}
public int Front() {
if(isEmpty()){
return -1;
}
return elem[front];
}
public int Rear() {
if(isEmpty()){
return -1;
}
int index = rear == 0 ? elem.length-1 : rear-1;
return index;
}
public boolean isEmpty() {
if(front == rear){
return true;
}
return false;
}
public boolean isFull() {
if((rear+1) % elem.length == front){
return true;
}
return false;
}
}
栈和队列到此就结束了,总体来说比较简单,下一章就是二叉树哦!