内容概览:
- 栈和栈的应用:撤销操作和系统栈
- 栈的基本实现
- 栈的另外一个应用:括号匹配
- 关于Leetcode的更多说明
- 数组队列
- 循环队列
- 循环队列的实现
- 数组队列和循环队列的比较
2-1.栈(Stack)
- 栈也是一种线性结构
- 相比数组,栈对应的操作是数组的子集
- 只能从一端添加元素,也只能从一端取出元素
- 这一端称为栈顶
- 栈是一种先进后出的数据结构
- Last In First Out(LIFO)
- 在计算机的世界里,栈拥有着不可思议的作用
栈的应用:
- 1)无处不在的undo操作(撤销)
- 例子:依次输入 "我爱"“中华”,"我爱"和“中华”会依次存入栈中,当我发现,我想写的是“我爱中国”,则位于栈顶的“中华”被执行undo操作后,在栈顶销毁,然后重新输入“中国”,存入栈顶。
- 2)程序调用的系统栈
函数A、B、C依次执行的顺序如箭头所示
首先执行A,到A2(A的第二行)跳到函数B(),然后执行到B2,又跳到函数C
当函数C执行完后,系统会检查目前处于栈顶的是哪个位置,发现是执行到了B2这一行
B函数继续执行,执行完后B2从栈顶销毁,系统再检查栈中是否还有元素,发现A2,同B2的流程
A最终执行完,发现栈中没有元素,这说明程序执行完了!
2-2.栈的实现
Stack<E> 五种基本操作
- void push() 添加一个元素(入栈)
- E pop() (出栈)
- E peep() 看一下栈顶的元素是谁
- int getSize() 看一下栈里有多少元素
- boolean isEmpty() 看栈是否为空
定义一个栈接口,里面写它的抽象方法不带修饰符的
public interface Stack<E> {
int getSize();//返回栈的元素个数
boolean isEmpty();//返回栈是否为空
void push(E e);//入栈
E pop();//出栈
E peek();//查看栈末尾的元素
}
新建一个ArrayStack类实现Stack<E>接口,通过之前的动态数组来搭建栈
public class ArrayStack<E> implements Stack<E> {
Array<E> array;
//带参构造函数
public ArrayStack(int capacity){
array =new Array<>(capacity);
}
//无参构造
public ArrayStack(){
array =new Array();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
//进栈,在末尾插入元素
@Override
public void push(E e){
array.addLast(e); }
//出栈,在末尾移除元素
@Override
public E pop(){
return array.removeLast();
}
//看看队列末尾的元素
@Override
public E peek(){
return array.getLast();
}
@Override
public String toString(){
StringBuilder res =new StringBuilder();
res.append("Stack:");
res.append("[");
for(int i=0;i<array.getSize();i++){
res.append(array.get(i));
if(i!=array.getSize()-1)
res.append(",");
}
res.append("]top");//这里是栈顶
return res.toString();
}
}
MainActivity来实现栈
public class Main {
public static void main(String[] args) {
ArrayStack<Integer> stack=new ArrayStack<>();
for(int i=0;i<5;i++){
stack.push(i);//入栈
System.out.println(stack);
}
stack.pop();//出栈
System.out.println(stack);
}
}
实现结果:
Stack:[0]top
Stack:[0,1]top
Stack:[0,1,2]top
Stack:[0,1,2,3]top
Stack:[0,1,2,3,4]top
Stack:[0,1,2,3]top
栈的时间复杂度分析
ArrayStack<E>
- void push() 添加一个元素(入栈) O(1)均摊
- E pop() (出栈) O(1)均摊
- E peep() 看一下栈顶的元素是谁 O(1)
- int getSize() 看一下栈里有多少元素 O(1)
- boolean isEmpty() 看栈是否为空 O(1)
2-3.栈的另外一个应用:括号的匹配
题目:给定一个只包含'(',')','[',']',‘{’,'}'的字符串,判断字符串是否有效
括号必须以正确的顺序关闭,"()","{}()[]"都是有效的,但是"[}”和“([)]”不是
假设:字符串s为{[()]},依次将左侧的括号压入栈里,直到第四个括号是不是右侧的小括号其第三个是左侧的小括号(匹配出栈)
依次类推,所以匹配有效,最终栈为空
当不是有效的字符串
到达第三个括号时此时是右侧的中括号和栈顶的元素并不是左侧的中括号,不匹配,不是有效字符串
栈顶元素反映了在嵌套的层次关系中,最近的需要匹配的元素
import java.util.Stack;
public class Solution {
/**
*有效括号问题
* 判断这个字符串是不是有效的字符串
*
*/
public boolean isValid(String s){
Stack<Character> stack=new Stack<>();
for (int i=0;i<s.length();i++){
char c=s.charAt(i);//返回指定索引位置的char值
if (c=='('||c=='['||c=='{')
stack.push(c);
else{
if(stack.isEmpty())//如果栈为空
return false;
char topChar=stack.pop();//如果栈不为空,拿出栈顶元素
if(c==')'&&topChar!='(')//如果此时的元素是右边的小括号,而栈顶元素不是,则返回false
return false;
if(c==']'&&topChar!='[')//如果此时的元素是右边的中括号,而栈顶的元素不是false
return false;
if (c=='}'&&topChar!='{')
return false;
}
}
return stack.isEmpty();//当所有括号匹配,栈变成空时,则返回true
}
//测试用例
public static void main(String[] args){
System.out.println((new Solution()).isValid("[]{}()"));
System.out.println((new Solution()).isValid("[{]}()"));
}
测试结果:
true
false
2-4.数组队列(Queue)
- 队列也是一种线性结构
- 相比数组,队列对应的操作是数组的子集
- 只能从一端(队尾)添加元素,只能从另一端(队首)取出元素
- 队列是一种先进先出的数据结构(先到先得)
- First In First Out(FIFO)
队列的实现
Queue<E>
- void enqueue(E) 在队的末尾添加一个元素
- E dequeue() 在队的末尾删除一个元素
- E getFront 获取队首的元素
- int getSize() 获取队列的长度
- boolean isEmpty() 队列是否为空
创建Queue接口支持泛型
public interface Queue<E> {
int getSize();//得到队列的大小
boolean isEmpty();//判断队列是否为空
void enqueue(E e);//给队列末尾插入一个元素
E dequeue();//从队列首部取出的一个元素
E getFront();//获取队列的第一个元素
}
创建ArrayQueue实现Queue接口
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
//带参构造函数
public ArrayQueue(int capacity) {
array = new Array<>(capacity);
}
public ArrayQueue(){
array=new Array<>();
}
@Override
public int getSize(){
return array.getSize();
}
@Override
public boolean isEmpty(){
return array.isEmpty();
}
public int getCapacity(){
return array.getCapacity();
}
//添加一个元素
@Override
public void enqueue(E e){
array.addLast(e);
}
//从队列头拿出一个元素
@Override
public E dequeue(){
return array.removeFirst();
}
//查看队首是哪个元素
@Override
public E getFront(){
return array.getFirst();
}
@Override
public String toString(){
StringBuilder res=new StringBuilder();
res.append("Queue:");
res.append("front[");
for(int i=0;i<array.getSize();i++){
res.append(array.get(i));
if(i!=array.getSize()-1)
res.append(',');
}
res.append("]tail");
return res.toString();
}
public static void main(String[] arg){
ArrayQueue<Integer> queue=new ArrayQueue<>();
for(int i=0;i<10;i++){
queue.enqueue(i);
System.out.println(queue);
//每插入队列3个元素,取出一个元素
if(i%3==2){//0、1、2 2对3取余为2
queue.dequeue();
System.out.println(queue);
}
}
}
}
输出结果
Queue:front[0]tail
Queue:front[0,1]tail
Queue:front[0,1,2]tail
Queue:front[1,2]tail
Queue:front[1,2,3]tail
Queue:front[1,2,3,4]tail
Queue:front[1,2,3,4,5]tail
Queue:front[2,3,4,5]tail
Queue:front[2,3,4,5,6]tail
Queue:front[2,3,4,5,6,7]tail
Queue:front[2,3,4,5,6,7,8]tail
Queue:front[3,4,5,6,7,8]tail
Queue:front[3,4,5,6,7,8,9]tail
数组队列的时间复杂度分析
2-5.循环队列
数组队列的问题:弥补数组队首出队时:时间复杂度O(n)
每次删除队首时,后面的元素都必须向前挪一位,这样每次造成时间复杂度为O(n)
有没有办法使后面的元素不用挪,只有改变队首的位置即可,队首变成第二个元素的位置
capacity中,浪费一个空间
- front==tail 队列为空
- (tail+1)%c==front 队列为满
自定义LoopQueue接口
public interface Queue<E> {
int getSize();//得到队列的大小
boolean isEmpty();//判断队列是否为空
void enqueue(E e);//给队列末尾插入一个元素
E dequeue();//从队列首部取出的一个元素
E getFront();//获取队列的第一个元素
}
新建一个LoopQueue实现接口
public class LoopQueue<E> implements Queue<E> {
private E[] data;
private int front,tail;//队首,队尾
private int size;
public LoopQueue(int capacity){
data=(E[])new Object[capacity+1];//浪费一个单位
front=0;
tail=0;
size=0;
}
public LoopQueue(){
this(10);
}
public int getCapacity(){
return data.length-1;//多出一个单位放tail
}
@Override
public boolean isEmpty() {
return front==tail;//判断队列是否为空
}
@Override
public int getSize(){
return size;
}
}
扩容操作
//扩容
private void resize(int newCapacity){
E[] newData=(E[])new Object[newCapacity+1];//新建一个新的队列,因为要浪费一个单位
for(int i=0;i<size;i++)
newData[i]=data[(i+front)%data.length];//front的队首,%的目的防止长度超出data。length
data=newData;
front=0;
tail=size; }
入对操作:
//进队操作
@Override
public void enqueue(E e){
if((tail+1)%data.length==front)//判断队列是否为满
resize(getCapacity()*2);//
System.out.println("循环数组实际长度为:"+data.length);
data[tail]=e;
tail=(tail+1)%data.length;//防止越界
size++; }
出队操作:
//出队操作
@Override
public E dequeue(){
if (isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue");
E ret=data[front];//保存队首的元素
data[front]=null;
front=(front+1)%data.length;
size--;
//当前是否要缩容,防止振荡时间复杂度
if (size==getCapacity()/4 && getCapacity()/2!=0)
resize(getCapacity()/2);
return ret;
}
得到队首的元素
@Override
public E getFront(){
if (isEmpty())
throw new IllegalArgumentException(" Queue is empty");
return data[front];
}
重写toString方法
@Override
public String toString(){
StringBuilder res=new StringBuilder();
res.append("Queue:");
res.append(String.format("Queue:size=%d,capacity=%d\n",size,getCapacity()));
res.append("front[");
for(int i=front;i!=tail;i=(i+1)%data.length){//不能去tail的位置,循环队列
res.append(data[i]);
if((i+1)%data.length!=tail)//不是队列中最后一个元素
res.append(',');
}
res.append("]tail");
return res.toString();
}
测试用例
public static void main(String[] arg){
LoopQueue<Integer> queue=new LoopQueue<>();
for(int i=0;i<10;i++) {
queue.enqueue(i);
System.out.println(queue);
//每插入队列3个元素,取出一个元素
if (i % 3 == 2) {//0、1、2 2对3取余为2
queue.dequeue();
System.out.println(queue);
}
}
}
循环队列是时间复杂度
解决了在数组中出队O(n)的均摊时间复杂度,变成O(1);
2-6.循环队列和数组队列的比较
写一个testQueue的方法,让两种队列分别进行opCount次进队出队操作,记录消耗的时间
//测试使用q运行opCount个enqueue和dequeue操作两种队列需要花费的时间,单位秒
private static double testQueue(Queue<Integer> q,int opCount){
long startTime=System.nanoTime();//计算当时时间,单位纳秒
//。。。。。你要进行的操作
long endTime=System.nanoTime();//计算结束时间,单位纳秒
return (endTime-startTime)/1000000000.0;//转换单位成秒
}
主函数:入队10W个,出队10W个
public static void main(String[] args) {
int opCount=100000;
//数组队列的操作
ArrayQueue<Integer> arrayQueue=new ArrayQueue<>();
double time1=testQueue(arrayQueue,opCount);
System.out.println("ArrayQueue Time="+time1+"s");
//循环队列的操作
LoopQueue<Integer> loopQueue=new LoopQueue<>();
double time2=testQueue(arrayQueue,opCount);
System.out.println("LoopQueue Time="+time2+"s");
}
具体操作:
//。。。。。你要进行的操作
//生成随机数
Random random=new Random();
for(int i=0;i<opCount;i++){
//入队插入随机数
q.enqueue(random.nextInt(Integer.MAX_VALUE));//0-Integer最大值
}
for(int i=0;i<opCount;i++){
//出队操作
q.dequeue();
}
测试结果:
ArrayQueue Time=5.405521306s
LoopQueue Time=0.703383811s