Java實現最小棧的兩種方式——時間復雜度O(1)


一、前言

  最近依舊在刷《劍指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 - minValuenode就是當前要入棧的元素,而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;
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM