玩轉資料結構——第二章:棧和佇列
內容概覽:
- 棧和棧的應用:撤銷操作和系統棧
- 棧的基本實現
- 棧的另外一個應用:括號匹配
- 關於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
(轉自發條魚)