【常用演算法思路分析系列】棧和佇列高頻題集(修改版)
阿新 • • 發佈:2019-02-07
本文是【常用演算法思路分析系列】的第三篇,分析棧和佇列相關的高頻題目。本文分析:1、可查詢最值的棧;2、用兩個棧實現佇列的功能;3、反轉棧中元素;4、排序棧中元素;5、滑動視窗問題。
本系列前兩篇導航:
【常用演算法思路分析系列】排序高頻題集
【常用演算法思路分析系列】字串高頻題集
1、可查詢最值的棧
定義棧的資料結構,請在該型別中實現一個能夠得到棧最小元素的min函式。思路: 定義兩個棧stackData和stackMin,其中stackData用來存放進棧的資料,stackMin用來存放進棧過程中的最小值。 方案一:噹噹前要進棧元素<=stackMin棧頂元素時,將當前要進棧元素同時加入到stackMin中; 噹噹前要進棧元素>stackMin棧頂元素時,stackMin棧不壓入資料; 方案二:噹噹前要進棧元素<=stackMin棧頂元素時,將當前要進棧元素同時加入到stackMin中; 噹噹前要進棧元素>stackMin棧頂元素時,stackMin棧把當前stackMin的棧頂元素再壓入一遍; 上述兩種方案都需要和stackData棧保持同步,只不過因為第一種方案stackMin棧中只儲存較小值,在pop時需要判斷;第二種方案是在pop時要完全同步。 程式碼如下:
public class GetMinStack { Stack<Integer> stackData = new Stack<Integer>(); Stack<Integer> stackMin = new Stack<Integer>(); public void push(int node) { if(stackMin.empty()){ stackData.push(node); stackMin.push(node); }else{ //第一種方案 if(node > stackMin.peek()){ stackData.push(node); }else{ stackData.push(node); stackMin.push(node); } /* 第二種方案 int minTop = stackMin.peek(); if(node > minTop){ stackData.push(node); stackMin.push(minTop); }else{ stackData.push(node); stackMin.push(node); } */ } } public void pop() { if(!stackData.empty() && ! stackMin.empty()){ int dataTop = stackData.peek(); int minTop = stackMin.peek(); //第一種方案 if(dataTop == minTop){//此時兩個棧都需要出棧操作 stackData.pop(); stackMin.pop(); }else if(dataTop > minTop){ stackData.pop(); } /* * 第二種方案 stackData.pop(); stackMin.pop(); */ } } public int top() { return stackData.peek(); } public int min() { return stackMin.peek(); } }
2、用兩個棧實現佇列的功能
用兩個棧來實現佇列的入隊、出對功能。定義兩個棧stackPush和stackPoll,stackPush棧用來放進佇列的元素,stackPoll棧用來出佇列。進隊時,元素壓入到stackPush中;出對時,將stackPush棧中的元素全部匯入(pop操作,因為要清空stackPush棧)到stackPoll棧中,stackPoll棧再彈出棧頂元素,然後將stackPoll棧中的元素再全部匯入(同樣是pop操作,因為要清空stackPoll棧)到stackPush棧中。 (上面的關鍵是,兩個棧互相匯入資料時都要全部匯入,全部匯入了,另一個棧也就清空了,否則不會符合佇列的性質編寫一個類,只能用兩個棧結構實現佇列,支援佇列的基本操作(push,pop)。
給定一個操作序列ope及它的長度n,其中元素為正數代表push操作,為0代表pop操作,保證操作序列合法且一定含pop操作,請返回pop的結果序列。
測試樣例:[1,2,3,0,4,0],6
返回:[1,2]程式碼如下:
public class TwoStack { public int[] twoStack(int[] ope, int n) { if(ope == null || n == 0) return null; Stack<Integer> stackPush = new Stack<Integer>(); Stack<Integer> stackPoll = new Stack<Integer>(); int popCount = 0;//出棧次數 for(int i = 0; i < n; i++){ if(ope[i] != 0){ stackPush.push(ope[i]); }else{ popCount++; } } int[] result = new int[popCount]; //將stackPush棧中的所有資料匯入到stackPoll棧中 while(!stackPush.empty()){ stackPoll.push(stackPush.pop()); } for(int i = 0; i < popCount; i++){ result[i] = stackPoll.pop(); } return result; } }
3、反轉棧中元素
要反轉棧中的元素,我們首先要依次遞迴的拿到棧底元素,拿到棧底元素之後,再一步步把它新增進去。因此分為兩步:拿到棧底元素和反轉新增元素。 程式碼如下:public class StackReverse {
//反轉棧
public static int[] reverseStack(int[] A, int n) {
if(A == null || n == 0)
return null;
Stack<Integer> stack = new Stack<Integer>();
for(int i = 0; i < n; i++){
stack.push(A[i]);
}
reverse(stack);//開始反轉操作
for(int i = n-1; i >= 0; i--){
A[i] = stack.pop();
}
return A;
}
/**
* 反轉棧中的元素
* @param stack
*/
public static void reverse(Stack<Integer> stack){
if(stack.isEmpty()){
return ;
}
//下面就是先遞迴拿到棧底元素,然後再把棧底元素入棧,此時棧中元素順序反轉
int bottom = popBottom(stack);
reverse(stack);
stack.push(bottom);
}
/**
* 移除棧底元素,並返回
* @return
*/
public static int popBottom(Stack<Integer> stack){
int result = stack.pop();
if(stack.isEmpty()){//彈出一個棧頂元素後,棧為空了,表示該元素就是棧底元素
return result;
}else{
int last = popBottom(stack);
stack.push(result);//注意!!!這裡是把前面拿到的元素壓入,這樣棧底元素才不會再次壓入到棧中
return last;
}
}
public static void main(String[] args) {
int[] a = {9,8,7,6,5,4,3,2,1};
reverseStack(a,a.length);
}
}
4、排序棧中元素
請編寫一個程式,按升序對棧進行排序(即最大元素位於棧頂),要求最多隻能使用一個額外的棧存放臨時資料,但不得將元素複製到別的資料結構中。思路: 假設棧stack是存放原來資料的,再定義一個輔助棧help,先從stack棧中取出棧頂元素pop,將pop和help中棧頂元素比較,如果pop <= help棧頂元素,將pop壓入到help棧中;如果pop > help棧頂元素,取出help棧頂元素,將其放入到stack棧中,直到help為空或者pop <= help棧頂元素。程式碼如下:
public static ArrayList<Integer> twoStacksSort(int[] numbers) {
if(numbers == null)
return null;
Stack<Integer> stack = new Stack<Integer>();
for(int i = numbers.length - 1; i >= 0; i--){
stack.push(numbers[i]);
}
Stack<Integer> help = new Stack<Integer>();
int pop,temp;
while(!stack.isEmpty()){
pop = stack.pop();
if(help.isEmpty()){
help.push(pop);
}else{
if(pop <= help.peek()){
help.push(pop);
}else{
while(!help.isEmpty() && pop > help.peek()){//將help中元素放入到stack中
temp = help.pop();
stack.push(temp);
}
//help棧為空了或者找到了pop<=help棧頂的元素
help.push(pop);
}
}
}
while(!help.isEmpty()){
stack.push(help.pop());
}
ArrayList<Integer> res = new ArrayList<Integer>();
while(!stack.isEmpty()){
res.add(stack.pop());
}
return res;
}
當然,也可以使用陣列作為棧來使用,將陣列下標為0處作為棧頂,程式碼如下:
/**
* 陣列作為棧,0的位置為棧頂
* @param numbers
* @return
*/
public static ArrayList<Integer> twoStacksSort2(int[] numbers) {
if(numbers == null)
return null;
int[] help = new int[numbers.length];
int i = 0;//指向numbers棧頂元素
int j = -1;//指向help棧頂元素
int pop;
while(i >= 0 && i != numbers.length){
pop = numbers[i];
if(j < 0){
help[++j] = pop;
}else{
if(pop <= help[j]){
help[++j] = pop;
}else{
while(j >= 0 && pop > help[j]){
numbers[i--] = help[j--];
}
help[++j] = pop;
}
}
i++;
}
ArrayList<Integer> res = new ArrayList<Integer>();
for (int k = 0; k < help.length; k++) {
res.add(help[k]);
System.out.println(help[k]);
}
return res;
}
5、滑動視窗問題
有一個整型陣列 arr 和一個大小為 w 的視窗從陣列的最左邊滑到最右邊,視窗每次向右邊滑一個位置。 返回一個長度為n-w+1的陣列res,res[i]表示每一種視窗狀態下的最大值。 以陣列為[4,3,5,4,3,3,6,7],w=3為例。因為第一個視窗[4,3,5]的最大值為5,第二個視窗[3,5,4]的最大值為5,第三個視窗[5,4,3]的最大值為5。第四個視窗[4,3,3]的最大值為4。第五個視窗[3,3,6]的最大值為6。第六個視窗[3,6,7]的最大值為7。所以最終返回[5,5,5,4,6,7]。
給定整形陣列arr及它的大小n,同時給定w,請返回res陣列。保證w小於等於n,同時保證陣列大小小於等於500。
測試樣例:[4,3,5,4,3,3,6,7],8,3
返回:[5,5,5,4,6,7]思路: 核心是定義一個雙端佇列qmax,這個佇列維護一個w個數據的視窗,佇列中儲存的是陣列的下標,在隊頭和隊尾分別進行彈出和插入操作,使得以隊頭元素為下標所指的陣列元素,在這個視窗中值最大。 對於陣列arr,當遍歷到陣列中第i個元素時, 在隊尾執行插入規則: 佇列為空肯定直接插入; 佇列不空,如果隊尾元素為下標所指的陣列元素arr[qmax.peekLast] > 當前遍歷元素arr[i],直接將下標i插入到隊尾;(因為雖然當前元素arr[i]較小,但是當隊頭元素過期之後,它可能成為另一個視窗的最大值,因此需要加入); 如果隊尾元素為下標所指的陣列元素arr[qmax.peekLast] <= 當前遍歷元素arr[i],說明當前隊尾元素下標不可能成為後面視窗的最大值了,因此直接將隊尾元素彈出,再繼續比較新的隊尾元素所指陣列元素和當前元素arr[i],根據上面規則加入; 在隊頭執行彈出規則: 如果隊頭元素 == i- w,表示隊頭元素已過期,超出了w個視窗的範圍了,直接將隊頭元素彈出; 過程如下圖:
如果佇列中滿足維護w個元素(當然,不一定佇列中有w個元素值,因為佇列在隊頭維持了最大值),則可以直接拿到隊尾元素所指的陣列元素值,這個值就是當前視窗的最大值。 程式碼如下:
public static int[] slide(int[] arr, int n, int w) {
if(arr == null || w < 1 || n < w){
return null;
}
int[] res = new int[n - w + 1];
//一個維護w個視窗的雙端佇列,保持下標為對頭元素的值最大
LinkedList<Integer> qmax = new LinkedList<Integer>();
int index = 0;
for(int i = 0; i < n; i++){
//執行隊尾進入規則
while(!qmax.isEmpty() && arr[qmax.peekLast()] <= arr[i]){
qmax.pollLast();
}
qmax.addLast(i);//將下標加入到隊尾
//執行對頭彈出規則
if(qmax.peekFirst() == i - w){
qmax.pollFirst();
}
if(i >= w - 1){//如果雙端佇列裡面至少維持了w個數據,則每次可以從對頭中拿到最大值
res[index++] = arr[qmax.peekFirst()];
}
}
return res;
}
陣列下標值每次最多進qmax一次,出qmax一次,因此整個陣列元素進出佇列的時間複雜度為O(N),整個演算法複雜度也就為O(N)。
本系列下一篇將是與連結串列相關演算法題。