劍指offer之棧隊列堆

[TOC]

9. 用兩個棧實現(xiàn)隊列

用兩個棧來實現(xiàn)一個隊列,完成隊列的 Push 和 Pop 操作半抱。

mysolution

import java.util.Stack;

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();
    
    public void push(int node) {
        stack1.push(node);
    }
    
    public int pop() {
        if (stack2.isEmpty()) {
            tranferStack1ToStack2();
        }
        return stack2.pop();
    }
    
    private void tranferStack1ToStack2() {
        while (!stack1.isEmpty()) {
            stack2.push(stack1.pop());
        }
    }
}

解題思路

in 棧用來處理入棧(push)操作,out 棧用來處理出棧(pop)操作膜宋。一個元素進(jìn)入 in 棧之后窿侈,出棧的順序被反轉(zhuǎn)。當(dāng)元素要出棧時秋茫,需要先進(jìn)入 out 棧史简,此時元素出棧順序再一次被反轉(zhuǎn),因此出棧順序就和最開始入棧順序是相同的肛著,先進(jìn)入的元素先退出圆兵,這就是隊列的順序。

[圖片上傳失敗...(image-dc5b93-1633656573051)]

包含 min 函數(shù)的棧

實現(xiàn)一個包含 min() 函數(shù)的棧枢贿,該方法返回當(dāng)前棧中最小的值殉农。

mysolution

import java.util.Stack;

public class Solution {

    Stack<Integer> stack1 = new Stack<>();
    Stack<Integer> stack2 = new Stack<>();

    
    public void push(int node) {
        stack1.push(node);
        if (stack2.isEmpty()) {
            stack2.push(node);
        } else if (node < stack2.peek()) {
            stack2.push(node);
        } else {
            stack2.push(stack2.peek());
        }
            
    }
    
    public void pop() {
        stack1.pop();
        stack2.pop();
    }
    
    public int top() {
        return stack1.peek();
    }
    
    public int min() {
        return stack2.peek();
    }
}

解題思路

使用一個額外的 minStack,棧頂元素為當(dāng)前棧中最小的值萨咕。在對棧進(jìn)行 push 入棧和 pop 出棧操作時统抬,同樣需要對 minStack 進(jìn)行入棧出棧操作,從而使 minStack 棧頂元素一直為當(dāng)前棧中最小的值危队。在進(jìn)行 push 操作時,需要比較入棧元素和當(dāng)前棧中最小值钙畔,將值較小的元素 push 到 minStack 中茫陆。

[圖片上傳失敗...(image-b6f180-1633656573051)]

private Stack<Integer> dataStack = new Stack<>();
private Stack<Integer> minStack = new Stack<>();

public void push(int node) {
    dataStack.push(node);
    minStack.push(minStack.isEmpty() ? node : Math.min(minStack.peek(), node));
}

public void pop() {
    dataStack.pop();
    minStack.pop();
}

public int top() {
    return dataStack.peek();
}

public int min() {
    return minStack.peek();
}

棧的壓入、彈出序列

輸入兩個整數(shù)序列擎析,第一個序列表示棧的壓入順序簿盅,請判斷第二個序列是否為該棧的彈出順序挥下。假設(shè)壓入棧的所有數(shù)字均不相等。

例如序列 1,2,3,4,5 是某棧的壓入順序桨醋,序列 4,5,3,2,1 是該壓棧序列對應(yīng)的一個彈出序列棚瘟,但 4,3,5,1,2 就不可能是該壓棧序列的彈出序列。

mysolution

import java.util.ArrayList;
import java.util.Stack;

public class Solution {
    public boolean IsPopOrder(int [] pushA,int [] popA) {
        Stack<Integer> stack = new Stack<>();
        int pushIndex = 0;
        int popIndex = 0;
        while (pushIndex < pushA.length && popIndex < pushA.length) {
            if (pushA[pushIndex] == popA[popIndex]) {
                pushIndex++;
                popIndex++;
                continue;
            }
            if (!stack.isEmpty() && popA[popIndex] == stack.peek()) {
                stack.pop();
                popIndex++;
                continue;
            }
            stack.push(pushA[pushIndex++]);
           
        }
        if (pushIndex == pushA.length && popIndex < popA.length) {
            while (popIndex < popA.length) {
                if (popA[popIndex] != stack.peek()) {
                    return false;
                }
                stack.pop();
                popIndex++;
            }
        }
        return true;
    }      
}

解題思路

使用一個棧來模擬壓入彈出操作喜最。每次入棧一個元素后偎蘸,都要判斷一下棧頂元素是不是當(dāng)前出棧序列 popSequence 的第一個元素,如果是的話則執(zhí)行出棧操作并將 popSequence 往后移一位瞬内,繼續(xù)進(jìn)行判斷迷雪。

public boolean IsPopOrder(int[] pushSequence, int[] popSequence) {
    int n = pushSequence.length;
    Stack<Integer> stack = new Stack<>();
    for (int pushIndex = 0, popIndex = 0; pushIndex < n; pushIndex++) {
        stack.push(pushSequence[pushIndex]);
        while (popIndex < n && !stack.isEmpty() 
                && stack.peek() == popSequence[popIndex]) {
            stack.pop();
            popIndex++;
        }
    }
    return stack.isEmpty();
}

40. 最小的 K 個數(shù)

Mysolution
import java.util.ArrayList;

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> result = new ArrayList<>();
        if (k <= 0) {
            return result;
        }
        buildHeap(input);
        for(int i = 0; i < k; i++) {
            int tmp = input[0];
            input[0] = input[input.length - i - 1];
            input[input.length - i - 1] = tmp;
            result.add(tmp);
          
            adjustHeap(input, 0, input.length - i - 2);
        }
        return result;
    }
    
    private void buildHeap(int[] input) {
        int start = (input.length - 2) / 2;
        for (int i = start; i >= 0; i--) {
            adjustHeap(input, i, input.length - 1);
        }
    }
    
    private void adjustHeap(int[] input, int position, int end) {
        int subPosition = position * 2 + 1;
        int tmp = input[position];
        while (subPosition <= end) {
            if (subPosition + 1 <= end && input[subPosition + 1] < input[subPosition]) {
                subPosition += 1;
            }
            if (tmp < input[subPosition]) {
                break;
            }
            input[position] = input[subPosition];
            position = subPosition;
            subPosition = position * 2 + 1;
        }
        input[position] = tmp;
    }
}

解題思路

大小為 K 的最小堆
復(fù)雜度:O(NlogK) + O(K)
特別適合處理海量數(shù)據(jù)
維護(hù)一個大小為 K 的最小堆過程如下:使用大頂堆。在添加一個元素之后虫蝶,如果大頂堆的大小大于 K章咧,那么將大頂堆的堆頂元素去除,也就是將當(dāng)前堆中值最大的元素去除能真,從而使得留在堆中的元素都比被去除的元素來得小赁严。

應(yīng)該使用大頂堆來維護(hù)最小堆,而不能直接創(chuàng)建一個小頂堆并設(shè)置一個大小粉铐,企圖讓小頂堆中的元素都是最小元素误澳。

Java 的 PriorityQueue 實現(xiàn)了堆的能力,PriorityQueue 默認(rèn)是小頂堆秦躯,可以在在初始化時使用 Lambda 表達(dá)式 (o1, o2) -> o2 - o1 來實現(xiàn)大頂堆忆谓。其它語言也有類似的堆數(shù)據(jù)結(jié)構(gòu)。

public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
    if (k > nums.length || k <= 0)
        return new ArrayList<>();
    PriorityQueue<Integer> maxHeap = new PriorityQueue<>((o1, o2) -> o2 - o1);
    for (int num : nums) {
        maxHeap.add(num);
        if (maxHeap.size() > k)
            maxHeap.poll();
    }
    return new ArrayList<>(maxHeap);
}

快速選擇

復(fù)雜度:O(N) + O(1)
只有當(dāng)允許修改數(shù)組元素時才可以使用
快速排序的 partition() 方法踱承,會返回一個整數(shù) j 使得 a[l..j-1] 小于等于 a[j]倡缠,且 a[j+1..h] 大于等于 a[j],此時 a[j] 就是數(shù)組的第 j 大元素茎活£悸伲可以利用這個特性找出數(shù)組的第 K 個元素,這種找第 K 個元素的算法稱為快速選擇算法载荔。

public ArrayList<Integer> GetLeastNumbers_Solution(int[] nums, int k) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (k > nums.length || k <= 0)
        return ret;
    findKthSmallest(nums, k - 1);
    /* findKthSmallest 會改變數(shù)組盾饮,使得前 k 個數(shù)都是最小的 k 個數(shù) */
    for (int i = 0; i < k; i++)
        ret.add(nums[i]);
    return ret;
}

public void findKthSmallest(int[] nums, int k) {
    int l = 0, h = nums.length - 1;
    while (l < h) {
        int j = partition(nums, l, h);
        if (j == k)
            break;
        if (j > k)
            h = j - 1;
        else
            l = j + 1;
    }
}

private int partition(int[] nums, int l, int h) {
    int p = nums[l];     /* 切分元素 */
    int i = l, j = h + 1;
    while (true) {
        while (i != h && nums[++i] < p) ;
        while (j != l && nums[--j] > p) ;
        if (i >= j)
            break;
        swap(nums, i, j);
    }
    swap(nums, l, j);
    return j;
}

private void swap(int[] nums, int i, int j) {
    int t = nums[i];
    nums[i] = nums[j];
    nums[j] = t;
}

41.1 數(shù)據(jù)流中的中位數(shù)

如何得到一個數(shù)據(jù)流中的中位數(shù)?如果從數(shù)據(jù)流中讀出奇數(shù)個數(shù)值懒熙,那么中位數(shù)就是所有數(shù)值排序之后位于中間的數(shù)值丘损。如果從數(shù)據(jù)流中讀出偶數(shù)個數(shù)值,那么中位數(shù)就是所有數(shù)值排序之后中間兩個數(shù)的平均值工扎。

/* 大頂堆徘钥,存儲左半邊元素 */
private PriorityQueue<Integer> left = new PriorityQueue<>((o1, o2) -> o2 - o1);
/* 小頂堆,存儲右半邊元素肢娘,并且右半邊元素都大于左半邊 */
private PriorityQueue<Integer> right = new PriorityQueue<>();
/* 當(dāng)前數(shù)據(jù)流讀入的元素個數(shù) */
private int N = 0;

public void Insert(Integer val) {
    /* 插入要保證兩個堆存于平衡狀態(tài) */
    if (N % 2 == 0) {
        /* N 為偶數(shù)的情況下插入到右半邊呈础。
         * 因為右半邊元素都要大于左半邊舆驶,但是新插入的元素不一定比左半邊元素來的大,
         * 因此需要先將元素插入左半邊而钞,然后利用左半邊為大頂堆的特點沙廉,取出堆頂元素即為最大元素,此時插入右半邊 */
        left.add(val);
        right.add(left.poll());
    } else {
        right.add(val);
        left.add(right.poll());
    }
    N++;
}

public Double GetMedian() {
    if (N % 2 == 0)
        return (left.peek() + right.peek()) / 2.0;
    else
        return (double) right.peek();
}

字符流中第一個不重復(fù)的字符

思路

字符出現(xiàn)次數(shù)的判斷(不重復(fù)字符):
這個做法大致相同臼节,利用 Hash 思想采用128大小的計數(shù)數(shù)組進(jìn)行計數(shù)也好撬陵,或者是使用 Map 鍵值對映射也好,都差不多官疲,使用數(shù)組會更簡單袱结。

字符出現(xiàn)順序的判斷(第一個字符):
這里就是改進(jìn)的關(guān)鍵之處了,容易發(fā)現(xiàn)途凫,字符流中不重復(fù)的字符可能同時存在多個垢夹,我們只要把這些 “不重復(fù)字符” 保存起來就可以,而無需保存那些重復(fù)出現(xiàn)的字符维费,而為了維護(hù)字符出現(xiàn)的順序果元,我們使用隊列(先進(jìn)先出)這一結(jié)構(gòu),先出現(xiàn)的不重復(fù)字符先輸出:

入隊:獲取字符流中的一個字符時犀盟,當(dāng)我們判斷它是不重復(fù)時而晒,將它加入隊列;
輸出/出隊:注意阅畴,因為隊列中存儲的 “不重復(fù)字符” 在一系列的流讀取操作后倡怎,隨時有可能改變狀態(tài)(變重復(fù)),所以贱枣,隊列中的字符不能直接輸出监署,要先進(jìn)行一次重復(fù)判斷,如果發(fā)現(xiàn)隊頭字符已經(jīng)重復(fù)了纽哥,就將它移出隊列并判斷新的隊頭钠乏,否則,輸出隊頭的值春塌;
復(fù)雜度計算:
從上面的描述來看晓避,好像存在一個循環(huán),隊列的長度好像無邊無際只壳,就給人一種O(n)的感覺俏拱,其實,并不是吕世,有如下結(jié)論:

通過分析可以發(fā)現(xiàn)彰触,循環(huán)(出隊)的最大次數(shù)其實就是隊列的長度,而隊列的長度最大為128命辖;
并且隨著時間的推移况毅,隊列長度 總體 先增大,后減小尔艇,正常條件下尔许,最終隊列會為空(因為隨著字符流的增大,重復(fù)的字符會越來越多终娃,隊列就會不斷地移除元素而越來越短)味廊;
更愉快的是,如果隊列長度不減小棠耕,則循環(huán)就只執(zhí)行一次余佛,返回速度快,如果隊列長度減小了窍荧,那么辉巡,循環(huán)次數(shù)上限也就減小了;
所以時間蕊退、空間復(fù)雜度是一致的郊楣,都是常數(shù)級,可是這是為什么呢瓤荔,分析如下:

字符的重復(fù)判斷净蚤,因為使用的是直接 Hash,而且功能是計數(shù)输硝,沒有沖突今瀑,所以是O(1);
只有不重復(fù)的字符才入隊列点把,但是不重復(fù)的字符有幾個呢橘荠?ASCII字符最多也就128個,那么同一字符會不會多次入隊呢愉粤? 不會的砾医,見3;
只有隊頭元素變得重復(fù)了才執(zhí)行循環(huán)衣厘,所以執(zhí)行循環(huán)就意味著隊列長度要變小如蚜。要注意,根據(jù)題意影暴,字符的出現(xiàn)次數(shù)只增不減4戆睢!型宙!所以撬呢,出隊的字符不會再入隊,隊列長度總體上只會越來越凶倍摇(或者上升到峰值就不再上升了魂拦,128種字符用盡)毛仪。

import java.util.Queue;
import java.util.LinkedList;
import java.lang.Character;

public class Solution {
    int[] charCnt = new int[128];
    Queue<Character> queue = new LinkedList<Character>();

    //Insert one char from stringstream
    public void Insert(char ch) {
        if (charCnt[ch]++ == 0) //新來的單身字符,入隊
            queue.add(ch);
    }
    //return the first appearence once char in current stringstream
    public char FirstAppearingOnce() {
        Character CHAR = null;
        char c = 0;
        while ((CHAR = queue.peek()) != null) {
            c = CHAR.charValue();
            if (charCnt[c] == 1) //判斷是否脫單了芯勘,沒脫單則輸出
                return c;
            else queue.remove(); //脫單了就移出隊列箱靴,它不會再回來了
        }
        return '#'; //隊空,返回#
    }
}
思路2(其實一樣的)

使用統(tǒng)計數(shù)組來統(tǒng)計每個字符出現(xiàn)的次數(shù)荷愕,本題涉及到的字符為都為 ASCII 碼衡怀,因此使用一個大小為 128 的整型數(shù)組就能完成次數(shù)統(tǒng)計任務(wù)。

使用隊列來存儲到達(dá)的字符安疗,并在每次有新的字符從字符流到達(dá)時移除隊列頭部那些出現(xiàn)次數(shù)不再是一次的元素抛杨。因為隊列是先進(jìn)先出順序,因此隊列頭部的元素為第一次只出現(xiàn)一次的字符荐类。

private int[] cnts = new int[128];
private Queue<Character> queue = new LinkedList<>();

public void Insert(char ch) {
    cnts[ch]++;
    queue.add(ch);
    while (!queue.isEmpty() && cnts[queue.peek()] > 1)
        queue.poll();
}

public char FirstAppearingOnce() {
    return queue.isEmpty() ? '#' : queue.peek();
}

滑動窗口的最大值

給定一個數(shù)組和滑動窗口的大小怖现,找出所有滑動窗口里數(shù)值的最大值。

例如掉冶,如果輸入數(shù)組 {2, 3, 4, 2, 6, 2, 5, 1} 及滑動窗口的大小 3真竖,那么一共存在 6 個滑動窗口,他們的最大值分別為 {4, 4, 6, 6, 6, 5}厌小。

[圖片上傳失敗...(image-84603f-1633656573051)]

解題思路

維護(hù)一個大小為窗口大小的大頂堆恢共,頂堆元素則為當(dāng)前窗口的最大值。

假設(shè)窗口的大小為 M璧亚,數(shù)組的長度為 N讨韭。在窗口向右移動時,需要先在堆中刪除離開窗口的元素癣蟋,并將新到達(dá)的元素添加到堆中透硝,這兩個操作的時間復(fù)雜度都為 log2M,因此算法的時間復(fù)雜度為 O(Nlog2M)疯搅,空間復(fù)雜度為 O(M)濒生。

public ArrayList<Integer> maxInWindows(int[] num, int size) {
    ArrayList<Integer> ret = new ArrayList<>();
    if (size > num.length || size < 1)
        return ret;
    PriorityQueue<Integer> heap = new PriorityQueue<>((o1, o2) -> o2 - o1);  /* 大頂堆 */
    for (int i = 0; i < size; i++)
        heap.add(num[i]);
    ret.add(heap.peek());
    for (int i = 0, j = i + size; j < num.length; i++, j++) {            /* 維護(hù)一個大小為 size 的大頂堆 */
        heap.remove(num[i]);
        heap.add(num[j]);
        ret.add(heap.peek());
    }
    return ret;
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市幔欧,隨后出現(xiàn)的幾起案子罪治,更是在濱河造成了極大的恐慌,老刑警劉巖礁蔗,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件觉义,死亡現(xiàn)場離奇詭異,居然都是意外死亡浴井,警方通過查閱死者的電腦和手機(jī)晒骇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人洪囤,你說我怎么就攤上這事徒坡。” “怎么了箍鼓?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵崭参,是天一觀的道長呵曹。 經(jīng)常有香客問我款咖,道長,這世上最難降的妖魔是什么奄喂? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任铐殃,我火速辦了婚禮,結(jié)果婚禮上跨新,老公的妹妹穿的比我還像新娘富腊。我一直安慰自己,他們只是感情好域帐,可當(dāng)我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布赘被。 她就那樣靜靜地躺著,像睡著了一般肖揣。 火紅的嫁衣襯著肌膚如雪民假。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天龙优,我揣著相機(jī)與錄音羊异,去河邊找鬼。 笑死彤断,一個胖子當(dāng)著我的面吹牛野舶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播宰衙,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼平道,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了供炼?” 一聲冷哼從身側(cè)響起一屋,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劲蜻,沒想到半個月后陆淀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡先嬉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年轧苫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡含懊,死狀恐怖身冬,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岔乔,我是刑警寧澤酥筝,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站雏门,受9級特大地震影響嘿歌,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茁影,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一宙帝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧募闲,春花似錦步脓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至要出,卻和暖如春鸳君,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背厨幻。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工相嵌, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人况脆。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓饭宾,卻偏偏與公主長得像,于是被迫代替她去往敵國和親格了。 傳聞我的和親對象是個殘疾皇子看铆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容