一、前言
最近依舊在刷《劍指offer》的題目,然后今天寫到了一道蠻有意思的題目,叫做包含min函數的棧,解題思路有點妙,寫篇博客記錄一下。
二、描述
這道題目的描述是:定義棧的數據結構,請在該類型中實現一個能夠得到棧中所含最小元素的min函數(時間復雜度應為O(1))。
然后這題給出的原始代碼如下,具體方法代碼需要自己補充:
import java.util.Stack;
public class Solution {
public void push(int node) {
}
public void pop() {
}
public int top() {
}
public int min() {
}
}
三、思路
看到這題,大多數人的第一反應應該就是:在類中聲明一個變量minVal,記錄當前棧中的最小值,然后在調用min方法時將這個最小值返回。但是仔細想一想,會發現這種辦法是不可行的,因為如果執行了pop操作,將最小值出棧了,那我們怎么知道,剩下的元素中最小的是哪個,如何找到它從而去更新變量minVal呢?或許你認為可以在最小值出棧后,遍歷剩下的元素,重新找出新的最小值。這樣確實可以,相比於每次調用min都遍歷一遍找最小值這種最笨的方法要好一些,但是別忘了,題目要求我們這個算法的時間復雜度是O(1),而且在面試中,這種方法是拿不到分的。所以,我們需要找出更加高效的算法。
3.1 時間復雜度O(1),空間復雜度O(n)的實現方式
提高時間效率的一個常用方法就是犧牲空間換取時間,這里也可以使用這種辦法。我們可以定義一個輔助棧minStack,幫助我們記錄最小值。在我們的類中,需要有兩個棧,一個就是我們用來存值的棧dataStack,另外一個就是幫助我們維護最小值的棧minStack。
push入棧操作有以下兩種情況:
- dataStack為空:此時,棧中沒有元素,我們將push傳入的參數直接放入到
dataStack以及minStack中; - dataStack不為空:此時,將push操作傳入的參數先放入
dataStack中,然后判斷這個元素與minStack的棧頂元素誰更大,若這個參數小於或等於minStack的棧頂元素,我們就將它加入到minStack中,否則minStack不變;
這樣,我們就可以保證,minStack的棧頂元素,一定是當前棧中最小的元素,而當我們調用min方法時,直接返回minStack的棧頂元素就行了。
與push操作相對應的,pop出棧操作,也有兩種情況:
- 出棧的元素大於棧中最小值:此時
dataStack的棧頂元素出棧,而minStack不變; - 出棧的元素等於棧中最小值:此時
dataStack的棧頂元素出棧,同時,minStack的棧頂元素也出棧;
這樣做有什么意義呢?很簡單,這個minStack里面的元素是怎么來的?如果當前入棧的元素小於等於最小值(即minStack的棧頂元素),我們就把他加入到minStack中;這樣一路下來,minStack中的元素一定是單調遞減的,而且棧頂元素一定是dataStack中的最小值,而棧頂元素的下一個元素,一定是所有元素中的第二小值,再往下就是第三小值,第四小值......依次類推(這里好好理解一下)。所以,如果我們現在出棧的值,和minStack的棧頂元素(即最小值)相等,那我們就讓minStack的棧頂元素出棧,然后神奇的事情發生了:原來的第二小值現在變成了minStack的棧頂元素,而原來的最小值出棧后,第二小值就是新的最小值了;通過這種方式,我們就成功的解決了之前說過的,最小值出棧后,無法立即找到新最小值的問題;
棧中值重復引發的問題
這里還有一個問題,就是:為什么也在進行push時,小於等於最小值的元素需要放入minStack中,而不是小於?這是因為,棧中可能存在重復的值,而最小值也可能有重復。比如說,【1,2,3,1】依次入棧,而第一次入棧的1就是最小值,第四個數也同樣為1,它處在棧頂。假設我們使用的是小於,而不是小於等於,則四個數入棧后,minStack的值為【1】。此時,若棧頂元素1出棧,我們檢測到出棧的數和最小值1相等,於是我們就讓minStack的棧頂元素出棧,然后minStack就為空了,而dataStack是【1,2,3】。可是我們看的出來,棧中的最小值應該還是1,因為1在棧中出現了兩次。此時就產生了問題,我們現在已經找不到最小值了。
所以,為了防止這種情況發生,我們在push操作時,檢測到當前入棧的元素小於等於最小值時,就需要將它加入minStack中,這時我們再看上面的例子:當四個數都入棧后,minStack的情況是【1,1】,dataStack為【1,2,3,1】,而此時,dataStack的棧頂元素1出棧,minStack的棧頂也出棧,則dataStack變成【1,2,3】,而minStack變成【1】,minStack的棧頂依舊是dataStack中的最小元素1,巧妙避免了上面的問題。
3.2 時間復雜度O(1),空間復雜度O(1)的實現方式
這道題,我在網上找別人的博客,發現基本上所有人都是上面這種實現方式,但是我還找到一篇博客,有另外一種實現方式,且時間復雜度和空間復雜度都為O(1)。可以說這種實現方式更加的妙,要讓我來想,我估計一輩子也想不出這種方法。下面我就來簡單介紹一下。
這個新思路實際上就是我們剛開始看到這題時候所想的思路:設置一個變量minValue,記錄當前棧中的最小值,然后在調用min方法的時候,直接返回就可以了。但是我們前面也說過,這種方法有一個問題,就是當最小值出棧后,我們如何能夠知道剩下的數中,最小值是哪一個,也就是找到上一個最小值。而接下來要講的思路非常巧妙的解決了這個問題。
首先,在我們自定義的棧中,需要兩個屬性,一個就是和第一個思路一樣的dataStack,而另一個就是minValue,用來存儲棧中當前的最小值。可是,這里的dataStack存儲的不是加入棧中的元素,我們在dataStack中放的,是當前入棧的元素與當前最小值的差值,即diff = node - minValue(node就是當前要入棧的元素,而minValue是當前棧中的最小值)。然后,這個差值diff我們分兩種情況處理:
- diff < 0:差值
diff小於0,根據計算公式可知,當前入棧的元素,小於棧中的最小值,於是我們將最小值minValue更新為當前入棧的元素,並將diff加入dataStack中; - diff >= 0:差值
diff大於等於0,表示當前入棧的元素,大於等於棧中的最小值,則最小值minValue不需要改變;
以上即是入棧push時要進行的操作,而出棧pop時也需要分情況考慮:
- 棧頂元素 < 0:若當前棧頂的元素小於0,表示什么?從上面的描述我們知道,這表示這個元素入棧時,小於當時的最小值,所以它就是當前的最小值,於是
minValue記錄的就是當前要出棧的元素;而minValue出棧后,我們如何找到剩下元素中的最小值呢?我們知道,當前棧頂的值是通過diff = node - minValue算出來的,而我們又知道當前的minValue就是這個公式中的node,於是我們就可以知道,minValue(之前) = node - diff = minValue(現在)- diff,通過這個公式,我們成功的獲得了上一個最小值(太巧妙了); - 棧頂元素 >= 0:此時說明當前要出棧的元素不是最小值,所以我們可以根據公式
diff = node - minValue得出,這個元素的實際值node = diff + minValue;
就這樣,我們成功的解決了最開始說的,最小值出棧后,無法找到上一個最小值的問題。但是我們上網搜索發現,很少有博客分享這種思路,要說原因,可能是因為這種思路存在一個比較致命的bug。
數據溢出造成的問題
我們試想這樣一種情況,假設我們需要在棧中依次放入兩個數,【2147483647, -2147483648】,首先,我們在棧中放入2147483647,此時棧中只有一個數,所以最小值就是2147483647。然后我們再向棧中加入 -2147483648,此時,我們需要計算它與最小值的差值,也就是diff = -2147483648 - 2147483647;按我們之前所說,新入棧的值更小,此時應該得到一個負數,上面的公式計算出來也確實是個負數;但是不要忘記,int是有范圍的,而上面的公式計算的結果超過了int所能存儲的最小值,造成數據溢出,得到的結果是一個 1,是個正數,於是產生了錯誤的結果,這個思路自然gg。
為了解決這個問題,我們可以用long代替int存儲每一個元素,但是這樣每個元素的存儲空間就擴大了一倍。而這個思路相對於第一個思路的好處就是不需要開辟輔助棧,節省空間,如果改為了long,那這個思路的優勢也將不復存在,並且如果我們想在棧中存long類型的值呢,難道要用BigInteger嗎?所以華而不實可能就是這個思路沒有傳播開來的原因吧。當然,這個思路還是非常巧妙的,學習一下也挺好,如果面試中遇到這個題,說出這種思路,也是一個加分項嘛。
四、代碼
下面就是上述思路的實現,偷懶了點懶,下面的代碼我直接使用了Java自帶的棧來實現,主要是關注最小棧的實現思路,push或者pop這些操作的具體代碼我就不自己寫了;
思路一實現
import java.util.Stack;
public class Solution {
private Stack<Integer> dataStack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();
/**
* 入棧操作
*/
public void push(int node) {
// 判斷是否需要更新minStack
if(dataStack.isEmpty() || minStack.peek() >= node) {
minStack.push(node);
}
// 將元素放入dataStack
dataStack.push(node);
}
/**
* 出棧操作
*/
public void pop() {
// 若棧不為空才執行出棧
if(!dataStack.isEmpty()) {
// 若當前出棧的元素,等於棧中的最小值(即minStack的棧頂)
// 則minStack的棧頂出棧
if(dataStack.pop() == minStack.peek()) {
minStack.pop();
}
}
}
/**
* 查看棧頂元素
*/
public int top() {
return dataStack.peek();
}
/**
* 返回棧中最小值
*/
public int min() {
// 返回最小值,即minStack的棧頂元素
return minStack.peek();
}
}
思路二實現
import java.util.Stack;
import java.util.EmptyStackException;
public class Solution {
// 存儲diff
private Stack<Integer> dataStack = new Stack<>();
// 存儲當前棧中的最小值
private Integer minValue;
/**
* 入棧操作
*/
public void push(int node) {
// 棧為空
if (dataStack.isEmpty()) {
minValue = node; // 最小值就是第一個入棧的值
dataStack.push(0); // 而它與當前最小值的差值為0
}else {
Integer diff = node - minValue; // 計算差值
dataStack.push(diff);
// 若新入棧的值小於最小值,則更新最小值
if(diff < 0) {
minValue = node;
}
}
}
/**
* 出棧操作
*/
public void pop() {
Integer val = dataStack.pop();
// 若當前出棧的值是最小值,則計算出上一個最小值並更新
if(val < 0) {
minValue = minValue - val;
}
}
/**
* 查看棧頂元素
*/
public int top() {
Integer val = dataStack.peek();
// 若棧頂元素不是最小值,則計算元素的實際值
return val < 0 ? minValue : minValue + val;
}
/**
* 返回棧中最小值
*/
public int min() {
if(dataStack.isEmpty()) {
throw new EmptyStackException();
}
return minValue;
}
}
