栈 (Stack)
栈是一种线性结构;相比数组,栈对应的操作是数组的子集;只能从一端加元素,也只能从一端取出元素;这一端称为栈顶。
先进后出的数据结构:
栈的应用:
-
无处不在的撤销(ctrl+z)
-
程序调用的系统栈
计算机执行A方法时,执行到第二行,发现要先去执行方法B,此时系统就要先记录一个信息A2(表示方法A执行到了第二行,就去执行B了);同理,执行B方法的时候,执行到第二行时,发现又要去执行C方法,又在栈中记录B2;然后程序去执行C,执行完后,程序就要去看一下系统栈了,发现栈顶是B2,取出B2,然后顺着B2之前执行到的位置往下执行,执行完B后;程序又去栈中查看栈顶元素,发现是A2,然后又执行A2,执行完A后,然后就又去栈中查看,发现已经没有元素了,说明已经执行完了。
栈的实现(数组的方式)
Stack<E>
- void push(E) 添加元素。入栈 。 时间复杂度O(1) 如果触发了resize,均摊之后,依然是O(1)
- E pop() 取出元素 。 出栈。 O(1) 如果触发了resize,均摊之后,依然是O(1)
- E peek() 查看栈顶元素是什么 O(1)
- int getSize() 栈里几个元素 O(1)
- boolean isEmpty() 栈是否为空 o(1)
使用上一篇,编写的动态数组类实现栈,当然也能使用其他数据结构,比如链表。
栈接口:
public interface Stack<E> {
int getSize();
boolean isEmpty();
void push(E e);
E pop();
E peek();
}
实现:
public class ArrayStack<E> implements Stack<E> {
private 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 stringBuilder=new StringBuilder();
stringBuilder.append("Stack:");
stringBuilder.append("[");
for (int i=0;i<array.getSize();i++){
stringBuilder.append(array.get(i));
if (i!=array.getSize()-1)
stringBuilder.append(",");
}
//top表示右边是栈顶
stringBuilder.append("]top");
return stringBuilder.toString();
}
}
栈的具体应用
括号匹配——编译器
https://leetcode-cn.com/problemset/all/
题库的第二十题。
import java.util.Stack;
/**
* 有效的括号
* 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
*
* 有效字符串需满足:
*
* 左括号必须用相同类型的右括号闭合。
* 左括号必须以正确的顺序闭合。
* 注意空字符串可被认为是有效字符串。
*/
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);
if (c=='(' || c=='{' || c=='[')
stack.push(c);
else {
if (stack.isEmpty())
return false;
Character pop = stack.pop();
if (c==')' && pop!='(')
return false;
if (c=='}' && pop!='{')
return false;
if (c==']' && pop!='[')
return false;
}
}
//匹配完之后,如果栈里还有元素,没人和它匹配,也是不行的
return stack.isEmpty();
}
}
队列(Queue)
队列也是一种线性结构;
相比数组,队列对应的操作是数组的子集;
只能从一端(队尾)添加元素,只能从另一端(队首)取出元素;先进先出的数据结构;
队列的实现(数组方式)
Queue<E>
- void enqueue(E) 入队 O(1) 均摊
- E dequeue() 出队 O(n) 性能消耗大
- E getFront() 查看队首元素 O(1)
- int getSize() 获取队列的元素个数 O(1)
- boolean isEmpty() 队列是否为空 O(1)
队列接口:
public interface Queue<E> {
void enqueue(E e);
E dequeue();
E getFront();
int getSize();
boolean isEmpty();
}
队列实现:
public class ArrayQueue<E> implements Queue<E> {
private Array<E> array;
public ArrayQueue(){
array=new Array<>();
}
public ArrayQueue(int capacity){
array=new Array<>(capacity);
}
/**
* 入队
* @param e
*/
@Override
public void enqueue(E e) {
array.addLast(e);
}
/**
* 出队
* @return
*/
@Override
public E dequeue() {
return array.removeFirst();
}
/**
* 查看队首元素
* @return
*/
@Override
public E getFront() {
return array.getFirst();
}
/**
* 获取队列的元素个数
* @return
*/
@Override
public int getSize() {
return array.getSize();
}
/**
* 队列是否为空
* @return
*/
@Override
public boolean isEmpty() {
return array.isEmpty();
}
//使用动态数组实现栈的时候才存在容量这个概念(也可以使用链表)
public int getCapacity(){
return array.getCapacity();
}
@Override
public String toString() {
StringBuilder stringBuilder=new StringBuilder();
stringBuilder.append("Queue:");
stringBuilder.append("front[");
for (int i=0;i<array.getSize();i++){
stringBuilder.append(array.get(i));
if (i!=array.getSize()-1)
stringBuilder.append(",");
}
stringBuilder.append("]tail");
return stringBuilder.toString();
}
public static void main(String[] args) {
ArrayQueue<Integer> arrayQueue=new ArrayQueue();
for (int i=0;i<10;i++){
arrayQueue.enqueue(i);
System.out.println(arrayQueue);
if (i%3==2){
arrayQueue.dequeue();
System.out.println(arrayQueue);
}
}
}
}
数组队列的问题
出栈的时候时间复杂度O(n),太高了,出一个,后面的元素都得往前移动一位;
循环队列:
维护两指针front tail
- 空队列
- 有元素入队,只需要tail往后移动一位就行
- 有元素出队。front向后移动一位就行。
- 为什么叫循环队列呢?
当元素不断入队,如下,此时tail不能++了
由于队首元素出队了,有空间,没有被后面的元素挤掉,对于数组来说,前面可能还有可以利用的空间,这就是循环队列的来由。
把这个数组看做一个环,7之后的索引其实是0。之前一直说tail++,更准确的说tail的索引是:(当前的索引+1)%数组长度。
取余运算:取余只能整数除以整数,若除数比被除数大,被除数就是余数;若除数比被除数小,被除数就除以除数直到剩下的数比除数小,则这个数就是余数;4%7=4//4÷7=0……4,余4,因为4根本除不了7,被除数小于除数
所以上图的tail应该指向0:
如果继续添加元素:tail正常的往后移动一位即可
再继续添加一位:
此时tail就要往下移动,就会出现tail==front
的情况,但是此时队列不为空,那么tail==front
就会即可表示队列为空又可以表示队列为满,这不是我们想看到的情况。
所以定义tail+1==front
的条件为队列满,也就是说如果数组只剩下一个空间了,就是满了,有意识的浪费一个空间。
更准确的说条件是:(当前的索引+1)%数组长度==front
队列满
满了,触发扩容。
实现循环队列
实现循环队列就是为了弥补出队操作的复杂度是O(n),现在变成了O(1)
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;
}
@Override
public void enqueue(E e) {
//队列满了,扩容
if ((tail+1)%data.length==front)
resize(getCapacity()*2);
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 datum = data[front];
data[front]=null;
//维护指针front
front=(front+1) % data.length;
//维护size
size--;
//缩容
if(size==getCapacity() / 4 && getCapacity() /2 !=0 )
resize(getCapacity() / 2);
return datum;
}
@Override
public E getFront() {
//队列为空
if (isEmpty())
throw new IllegalArgumentException("Cannot dequeue from an empty queue.");
return data[front];
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return front==tail;
}
private void resize(int newCapacity){
E[] newData=(E[])new Object[newCapacity+1];
for (int i=0;i<size;i++)
//newData[0]对应的应该是data[front]
//newData[1]对应的应该是data[front+1]
//(front+i) % data.length 取余是怕产生数组越界
newData[i]=data[(front+i) % data.length];
data=newData;
front=0;
tail=size;
}
@Override
public String toString() {
StringBuilder res=new StringBuilder();
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){
res.append(data[i]);
if ((i+1) % data.length !=tail)
res.append(",");
}
res.append("] tail");
return res.toString();
}
public static void main(String[] args) {
LoopQueue<Integer> arrayQueue=new LoopQueue();
for (int i=0;i<10;i++){
arrayQueue.enqueue(i);
System.out.println(arrayQueue);
if (i%3==2){
arrayQueue.dequeue();
System.out.println(arrayQueue);
}
}
}
}
数组队列和循环队列的比较
public class Main {
private static double testQueue(Queue<Integer> q, int opCount){
long startTime = System.nanoTime();
Random random=new Random();
for (int i=0;i<opCount;i++)
q.enqueue(random.nextInt(Integer.MAX_VALUE));
for (int i=0;i<opCount;i++)
q.dequeue();
long endTime = System.nanoTime();
return (endTime-startTime) / 1000000000.0;
}
public static void main(String[] args) {
//循环队列和数组队列比较
int opCount=100000;
Queue<Integer> arrayQueue=new ArrayQueue();
System.out.println("ArrayQueue , time: "+testQueue(arrayQueue,opCount )+"s");
Queue<Integer> loopQueue=new LoopQueue();
System.out.println("LoopQueue , time: "+testQueue(loopQueue,opCount )+"s");
}
}
结果:
ArrayQueue , time: 3.8745792s
LoopQueue , time: 0.0156017s