前言:题图无关,只是好看,接下来就来复习一下栈和队列的相关知识java
前序文章:git
栈是一种用于存储数据的简单数据结构(与链表相似)。数据入栈的次序是栈的关键。能够把一桶桶装的薯片看做是一个栈的例子,当薯片作好以后,它们会依次被添加到桶里,每一片都会是当前的最上面一片,而每次咱们取的时候也是取的最上面的那一片,规定你不能破坏桶也不能把底部捅穿,因此第一个放入桶的薯片只能最后一个从桶里取出;github
定义:栈(Stack)是一个有序线性表,只能在表的一端(称为栈顶,top)执行插入和删除操做。最后插入的元素将第一个被删除,因此栈也称为后进先出(Last In First Out,LIFO)或先进后出(First In Last Out)线性表;web
两个改变栈的操做都有专用名称。一个称为入栈(push),表示在栈中插入一个元素;另外一个称为出栈(pop),表示从栈中删除一个元素。试图对一个空栈执行栈操做称为下溢(underflow);试图对一个满栈执行栈操做称为溢出(overflow)。一般,溢出和下溢均认为是异常;面试
下面给出栈抽象数据类型中的操做,为了简单起见,假设数据类型为整型;算法
void push(int data)
:将data(数据)插入栈;int pop()
:删除并返回最后一个插入栈的元素;int top()
:返回最后一个插入栈的元素,但不删除;int size()
:返回存储在栈中元素的个数;int isEmpty()
:判断栈中是否有元素;int isStackFull()
:判断栈中是否存满元素;咱们结合以前建立的Array类,咱们可以很好的建立属于咱们本身的动态数组实现的栈结构,对于用户来讲,咱们只须要完成咱们的相关操做,而且知道我可以不断地往里添加元素而不出错就好了,因此咱们先来定义一个通用的栈接口:数组
public interface Stack<E> { int getSize(); boolean isEmepty(); void push(E e); E pop(); E top(); }
而后咱们往以前的动态数组中添加两个用户友好的方法:微信
public E getLast() { return get(size - 1); } public E getFirst() { return get(0); }
而后实现本身的动态数组为底层的栈结构就轻松多了:数据结构
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 isEmepty() { 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 top() { 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("]"); return res.toString(); } }
从代码中能够看出,几乎全部的时间复杂度都为O(1)级别,比较特别的是push()
和pop()
操做可能涉及到底层数组的扩容或缩容的操做,因此是均摊下来的复杂度;app
队列是一种用于存储数据的数据结构(与链表和栈相似),数据到达的次序是队列的关键;在平常生活中队列是指从序列的开始按照顺序等待服务的一队人或物;
定义:队列是一种只能在一端插入(队尾),在另外一端删除(队首)的有序线性表。队列中第一个插入的元素也是第一个被删除的元素,因此队列是一种先进先出(FIFO,First In First Out)或后进后出(LiLO,Last In Last Out)线性表;
与栈相似,两个改变队列的操做各有专用名称;在队列中插入一个元素,称为入队(EnQueue),从队列中删除一个元素,称为出队(DeQueue);试图对一个空队列执行出队操做称为下溢(underflow),试图对一个满队列执行入队操做称为溢出(overflow);一般认为溢出和下溢是异常。
咱们仍然定义一个Queue接口来讲明咱们队列中经常使用的一些方法:
public interface Queue<E> { int getSize(); boolean isEmpty(); void enqueue(E e); E dequeue(); E getFront(); }
借由咱们以前本身实现的动态数组,那么咱们的队列就很简单了:
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(); } }
void enquque(E)
:O(1)(均摊)E dequeue()
:O(n)E front()
:O(1)int getSize()
:O(1)boolean isEmpty()
:O(1)循环队列的实现其实就是维护了一个front和一个tail分别指向头和尾,而后须要特别注意的呢是断定队满和队空的条件:
front == tail
,这没啥好说的;tail + 1 == front
,这里实际上是有意浪费了一个空间,否则就断定不了究竟是队空仍是队满了,由于条件都同样...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 boolean isEmpty(){ return front == tail; } @Override public int getSize(){ return size; } @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 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]; } 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]; 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(); } }
void enquque(E)
:O(1)(均摊)E dequeue()
:O(1)(均摊)E front()
:O(1)int getSize()
:O(1)boolean isEmpty()
:O(1)咱们来简单对比一下两个队列的性能吧,这里直接上代码:
// 测试使用q运行opCount个enqueueu和dequeue操做所须要的时间,单位:秒 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; 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(loopQueue, opCount); System.out.println("LoopQueue, time: " + time2 + " s"); }
我这里的测试结果是这样的,你们也就可见一斑啦:
其实ArrayQueue慢主要是由于出栈时每次都须要把整个结构往前挪一下
个人答案:(10ms)
public boolean isValid(String s) { // 正确性判断 if (null == s || s.length() == 1) { return false; } Stack<Character> stack = new Stack<>(); // 遍历输入的字符 for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); // 若是为左括号则push进栈 if (c == '(' || c == '[' || c == '{') { stack.push(c); } else { if (stack.isEmpty()) { return false; } char topChar = stack.pop(); if (c == ')' && topChar != '(') { return false; } if (c == ']' && topChar != '[') { return false; } if (c == '}' && topChar != '{') { return false; } } } // 最后栈为空才能返回true return stack.isEmpty(); }
参考答案:(8ms)
public boolean isValid(String s) { // 正确性判断 if (0 == s.length()) { return true; } if (s.length() % 2 == 1) { return false; } Stack<Character> stack = new Stack(); char[] cs = s.toCharArray(); for (int i = 0; i < cs.length; i++) { if (cs[i] == '(' || cs[i] == '[' || cs[i] == '{') { stack.push(cs[i]); } else { if (stack.isEmpty()) { return false; } char c = stack.pop(); if ((cs[i] == ')' && c == '(') || (cs[i] == '}' && c == '{') || (cs[i] == ']' && c == '[')) { } else { return false; } } } return stack.isEmpty(); }
参考答案(107ms)
class MinStack { // 数据栈,用于存放插入的数据 private Stack<Integer> dataStack; // 最小数位置栈,存放数据栈中最小的数的位置 private Stack<Integer> minStack; /** * initialize your data structure here. */ public MinStack() { this.dataStack = new Stack<>(); this.minStack = new Stack<>(); } /** * 元素入栈 * * @param x 入栈的元素 */ public void push(int x) { dataStack.push(x); // 若是最小栈是空的,只要将元素入栈 if (minStack.isEmpty()) { minStack.push(x); } // 若是最小栈中有数据 else { minStack.push(Math.min(x, minStack.peek())); } } /** * 出栈方法 */ public void pop() { // 若是栈已经为空,则返回(LeetCode不能抛异常...) if (dataStack.isEmpty()) { return; } // 若是有数据,最小数位置栈和数据栈一定是有相同的元素个数, // 两个栈同时出栈 minStack.pop(); dataStack.pop(); } /** * 返回栈顶元素 * * @return 栈顶元素 */ public int top() { return dataStack.peek(); } /** * 获取栈中的最小元素 * * @return 栈中的最小元素 */ public int getMin() { // 若是最小数公位置栈已经为空(数据栈中已经没有数据了),则抛出异常 if (minStack.isEmpty()) { return 0; } // 获取数据占中的最小元素,而且返回结果 return minStack.peek(); } }
改进答案:
上面求解方法的主要问题在于,每次push操做时,minStack也执行了一次push操做(新元素或当前的最小元素),也就是说,重复执行了最小值的入栈操做,因此如今咱们来修改算法下降空间复杂度。仍然须要设置一个minStack,可是只有当从dataStack中出栈的元素等于minStack栈顶元素时,才对minStack执行出栈的操做;也只有当dataStack入栈的元素小于或等于当前最小值时,才对minStack执行入栈操做,下面就简单写一下了主要看一下出栈和入栈实现的逻辑就行了:
class MinStack { private Stack<Integer> dataStack; private Stack<Integer> minStack; public MinStack() { this.dataStack = new Stack<>(); this.minStack = new Stack<>(); } public void push(int x) { dataStack.push(x); if (minStack.isEmpty() || minStack.peek() >= (Integer) x) { minStack.push(x); } } public void pop() { if (dataStack.isEmpty()) { return; } Integer minTop = minStack.peek(); Integer dataTop = dataStack.peek(); if (minTop.intValue() == dataTop.intValue()) { minStack.pop(); } dataStack.pop(); } public int top() { return dataStack.peek(); } public int getMin() { return minStack.peek(); } }
个人答案:(118ms)
class MyStack { private Queue<Integer> queue1; private Queue<Integer> queue2; /** * Initialize your data structure here. */ public MyStack() { queue1 = new LinkedList<>(); queue2 = new LinkedList<>(); } /** * Push element x onto stack. */ public void push(int x) { if (queue1.isEmpty()) { queue2.offer(x); } else { queue1.offer(x); } } /** * Removes the element on top of the stack and returns that element. */ public int pop() { int size; if (!queue1.isEmpty()) { size = queue1.size(); for (int i = 0; i < size - 1; i++) { queue2.offer(queue1.poll()); } return queue1.poll(); } else { size = queue2.size(); for (int i = 0; i < size - 1; i++) { queue1.offer(queue2.poll()); } return queue2.poll(); } } /** * Get the top element. */ public int top() { int size; if (!queue1.isEmpty()) { size = queue1.size(); for (int i = 0; i < size - 1; i++) { queue2.offer(queue1.poll()); } int result = queue1.peek(); queue2.offer(queue1.poll()); return result; } else { size = queue2.size(); for (int i = 0; i < size - 1; i++) { queue1.offer(queue2.poll()); } int result = queue2.peek(); queue1.offer(queue2.poll()); return result; } } /** * Returns whether the stack is empty. */ public boolean empty() { return queue1.isEmpty() && queue2.isEmpty(); } }
参考答案:(121ms)
class MyStack { Queue<Integer> q; /** * Initialize your data structure here. */ public MyStack() { this.q = new LinkedList<Integer>(); } /** * Push element x onto stack. */ public void push(int x) { q.add(x); } /** * Removes the element on top of the stack and returns that element. */ public int pop() { int size = q.size(); for (int i = 0; i < size - 1; i++) { q.add(q.remove()); } return q.remove(); } /** * Get the top element. */ public int top() { int size = q.size(); for (int i = 0; i < size - 1; i++) { q.add(q.remove()); } int ret = q.remove(); q.add(ret); return ret; } /** * Returns whether the stack is empty. */ public boolean empty() { return q.isEmpty(); } }
确实写得简洁啊,这样一来我就使用一个队列和两个队列都掌握啦,开心~
参考答案:(72ms)
class MyQueue { Stack<Integer> pushstack; Stack<Integer> popstack; /** * Initialize your data structure here. */ public MyQueue() { this.pushstack = new Stack(); this.popstack = new Stack(); } /** * Push element x to the back of queue. */ public void push(int x) { pushstack.push(x); } /** * Removes the element from in front of queue and returns that element. */ public int pop() { if (popstack.isEmpty()) { while (!pushstack.isEmpty()) { popstack.push(pushstack.pop()); } } return popstack.pop(); } /** * Get the front element. */ public int peek() { if (popstack.isEmpty()) { while (!pushstack.isEmpty()) { popstack.push(pushstack.pop()); } } return popstack.peek(); } /** * Returns whether the queue is empty. */ public boolean empty() { return pushstack.isEmpty() && popstack.isEmpty(); } }
题目:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的全部数字均不相等。例如,序列{1,2,3,4,5}是某栈的压栈序列,序列{4,5,3,2,1}是该压栈序列对应的一个弹出序列,但{4,3,5,1,2}就不多是该压栈序列的弹出序列。
参考答案:(原文连接:https://blog.csdn.net/derrantcm/article/details/46691083)
public class Test22 { /** * 输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。 * 假设压入栈的全部数字均不相等。例如序列1 、二、3 、四、5 是某栈压栈序列, * 序列四、五、三、二、1是该压栈序列对应的一个弹出序列, * 但四、三、五、一、2就不多是该压棋序列的弹出序列。 * 【与书本的的方法不一样】 * * @param push 入栈序列 * @param pop 出栈序列 * @return true:出栈序列是入栈序列的一个弹出顺序 */ public static boolean isPopOrder(int[] push, int[] pop) { // 输入校验,参数不能为空,而且两个数组中必须有数字,而且两个数组中的数字个数相同 // 不然返回false if (push == null || pop == null || pop.length == 0 || push.length == 0 || push.length != pop.length) { return false; } // 通过上面的参数校验,两个数组中必定有数据,且数据数目相等 // 用于存放入栈时的数据 Stack<Integer> stack = new Stack<>(); // 用于记录入栈数组元素的处理位置 int pushIndex = 0; // 用于记录出栈数组元素的处理位置 int popIndex = 0; // 若是还有出栈元素要处理 while (popIndex < pop.length) { // 入栈元素还未所有入栈的条件下,若是栈为空,或者栈顶的元素不与当前处理的相等,则一直进行栈操做, // 直到入栈元素所有入栈或者找到了一个与当出栈元素相等的元素 while (pushIndex < push.length && (stack.isEmpty() || stack.peek() != pop[popIndex])) { // 入栈数组中的元素入栈 stack.push(push[pushIndex]); // 指向下一个要处理的入栈元素 pushIndex++; } // 若是在上一步的入栈过程当中找到了与出栈的元素相等的元素 if (stack.peek() == pop[popIndex]) { // 将元素出栈 stack.pop(); // 处理下一个出栈元素 popIndex++; } // 若是没有找到与出栈元素相等的元素,说明这个出栈顺序是不合法的 // 就返回false else { return false; } } // 下面的语句老是成立的 // return stack.isEmpty(); // 为何能够直接返回true:对上面的外层while进行分析可知道,对每个入栈的元素, // 在stack栈中,经过一些入栈操做,总能够在栈顶上找到与入栈元素值相同的元素, // 这就说明了这个出栈的顺序是入栈顺序的一个弹出队列,这也能够解释为何stack.isEmpty() // 老是返回true,全部的入栈元素均可以进栈,而且能够被匹配到,以后就弹出,最后栈中就无元素。 return true; } /** * 输入两个整数序列,第一个序列表示栈的压入顺序,请判断二个序列是否为该栈的弹出顺序。 * 【按书本上的思路进行求解,二者相差不大】 * * @param push 入栈序列 * @param pop 出栈序列 * @return true:出栈序列是入栈序列的一个弹出顺序 */ public static boolean isPopOrder2(int[] push, int[] pop) { // 用于记录判断出栈顺序是否是入栈顺的一个出栈序列,默认false boolean isPossible = false; // 当入栈和出栈数组者都不为空,而且都有数据,而且数据个数都相等 if (push != null && pop != null && push.length > 0 && push.length == pop.length) { // 用于存放入栈时的数据 Stack<Integer> stack = new Stack<>(); // 记录下一个要处理的入栈元素的位置 int nextPush = 0; // 记录下一个要处理的出栈元素的位置 int nextPop = 0; // 若是出栈元素没有处理完就继续进行处理 while (nextPop < pop.length) { // 若是栈为空或者栈顶的元素与当前处理的出栈元素不相同,一直进行操做 while (stack.isEmpty() || stack.peek() != pop[nextPop]) { // 若是入栈的元素已经所有入栈了,就退出内层循环 if (nextPush >= push.length) { break; } // 执行到此处说明还有入栈元素能够入栈 // 即将元素入栈 stack.push(push[nextPush]); // 指向下一个要处理的入栈元素的位置 nextPush++; } // 执行到此处有两种状况: // 第一种:在栈顶上找到了一个与入栈元素相等的元素 // 第二种:在栈顶上没有找到一个与入栈元素相等的元素,并且输入栈的元素已经所有入栈了 // 对于第二种状况就说弹出栈的顺序是不符合要求的,退出外层循环 if (stack.peek() != pop[nextPop]) { break; } // 对应到第一种状况:须要要栈的栈顶元素弹出 stack.pop(); // 指向下一个要处理的出栈元素的位置 nextPop++; } // 执行到此处有两种状况 // 第一种:外层while循环的在第一种状况下退出, // 第二种:全部的出栈元素都被正确匹配 // 对于出现的第一种状况其stack.isEmpty()必不为空,缘由为分析以下: // 全部的入栈元素必定会入栈,可是只有匹配的状况下才会出栈, // 匹配的次数最多与入栈元素个数元素相同(两个数组的长度相等),若是有不匹配的元素, // 必然会使出栈的次数比入栈的次数少,这样栈中至少会有一个元素 // 对于第二种状况其stack.isEmpty()必定为空 // 因此书本上的nextPop == pop.length(pNextPop-pPop==nLength)是多余的 if (stack.isEmpty()) { isPossible = true; } } return isPossible; } public static void main(String[] args) { int[] push = {1, 2, 3, 4, 5}; int[] pop1 = {4, 5, 3, 2, 1}; int[] pop2 = {3, 5, 4, 2, 1}; int[] pop3 = {4, 3, 5, 1, 2}; int[] pop4 = {3, 5, 4, 1, 2}; System.out.println("true: " + isPopOrder(push, pop1)); System.out.println("true: " + isPopOrder(push, pop2)); System.out.println("false: " + isPopOrder(push, pop3)); System.out.println("false: " + isPopOrder(push, pop4)); int[] push5 = {1}; int[] pop5 = {2}; System.out.println("false: " + isPopOrder(push5, pop5)); int[] push6 = {1}; int[] pop6 = {1}; System.out.println("true: " + isPopOrder(push6, pop6)); System.out.println("false: " + isPopOrder(null, null)); // 测试方法2 System.out.println(); System.out.println("true: " + isPopOrder2(push, pop1)); System.out.println("true: " + isPopOrder2(push, pop2)); System.out.println("false: " + isPopOrder2(push, pop3)); System.out.println("false: " + isPopOrder2(push, pop4)); System.out.println("false: " + isPopOrder2(push5, pop5)); System.out.println("true: " + isPopOrder2(push6, pop6)); System.out.println("false: " + isPopOrder2(null, null)); } }
栈和队列的应用远不止上面学习到的那些,实现方式也有不少种,如今也只是暂时学到这里,经过刷LeetCode也加深了我对于这两种数据结构的认识,不过本身还须要去熟悉了解一下计算机系统关于栈的应用这方面的知识,由于栈这种结构自己就很适合用来保存CPU现场之类的工做,仍是抓紧时间吧,过两天还考试,这两天就先复习啦...
欢迎转载,转载请注明出处!
简书ID:@我没有三颗心脏
github:wmyskxz 欢迎关注公众微信号:wmyskxz_javaweb 分享本身的Java Web学习之路以及各类Java学习资料