數據結構和算法面試題系列—棧

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏git

0 概述

棧做爲一種基本的數據結構,在不少地方有運用,好比函數遞歸,先後綴表達式轉換等。本文會用C數組來實現棧結構(使用鏈表實現能夠參見鏈表那一節,使用頭插法構建鏈表便可),並對常見的幾個跟棧相關的面試題進行分析,本文代碼在 這裏github

1 定義

咱們使用結構體來定義棧,使用柔性數組來存儲元素。幾個宏定義用於計算棧的元素數目及棧是否爲空和滿。面試

typedef struct Stack {
    int capacity;
    int top;
    int items[];
} Stack;

#define SIZE(stack) (stack->top + 1)
#define IS_EMPTY(stack) (stack->top == -1)
#define IS_FULL(stack) (stack->top == stack->capacity - 1)
複製代碼

2 基本操做

棧主要有三種基本操做:算法

  • push:壓入一個元素到棧中。
  • pop:彈出棧頂元素並返回。
  • peek:取棧頂元素,可是不修改棧。

如圖所示:express

棧示意圖

代碼以下:數組

Stack *stackNew(int capacity)
{
    Stack *stack = (Stack *)malloc(sizeof(*stack) + sizeof(int) * capacity);
    if (!stack) {
        printf("Stack new failed\n");
        exit(E_NOMEM);
    }

    stack->capacity = capacity;
    stack->top = -1;
    return stack;
}

void push(Stack *stack, int v)
{
    if (IS_FULL(stack)) {
        printf("Stack Overflow\n");
        exit(E_FULL);
    }
    stack->items[++stack->top] = v;
}

int pop(Stack *stack)
{
    if (IS_EMPTY(stack)) {
        printf("Stack Empty\n");
        exit(E_EMPTY);
    }

    return stack->items[stack->top--];
}

int peek(Stack *stack)
{
    if (IS_EMPTY(stack)) {
        printf("Stack Empty\n");
        exit(E_EMPTY);
    }
    return stack->items[stack->top];
}

複製代碼

3 棧相關面試題

3.1 後綴表達式求值

題: 已知一個後綴表達式 6 5 2 3 + 8 * + 3 + *,求該後綴表達式的值。bash

解: 後綴表達式也叫逆波蘭表達式,其求值過程能夠用到棧來輔助存儲。則其求值過程以下:數據結構

  • 1)遍歷表達式,遇到的數字首先放入棧中,此時棧爲 [6 5 2 3]
  • 2)接着讀到 +,則彈出3和2,計算 3 + 2,計算結果等於 5,並將 5 壓入到棧中,棧爲 [6 5 5]
  • 3)讀到 8 ,將其直接放入棧中,[6 5 5 8]
  • 4)讀到 *,彈出 85 ,計算 8 * 5,並將結果 40 壓入棧中,棧爲 [6 5 40]。然後過程相似,讀到 +,將 405 彈出,將 40 + 5 的結果 45 壓入棧,棧變成[6 45],讀到3,放入棧 [6 45 3]...以此類推,最後結果爲 288

代碼:數據結構和算法

int evaluatePostfix(char *exp)
{
    Stack* stack = stackNew(strlen(exp));
    int i;
 
    if (!stack) {
        printf("New stack failed\n");
        exit(E_NOMEM);
    }
 
    for (i = 0; exp[i]; ++i) {
        // 若是是數字,直接壓棧
        if (isdigit(exp[i])) {
            push(stack, exp[i] - '0');
        } else {// 若是遇到符號,則彈出棧頂兩個元素計算,並將結果壓棧
            int val1 = pop(stack);
            int val2 = pop(stack);
            switch (exp[i])
            {
                case '+': push(stack, val2 + val1); break;
                case '-': push(stack, val2 - val1); break;
                case '*': push(stack, val2 * val1); break;
                case '/': push(stack, val2/val1);   break;
            }
        }
    }

    return pop(stack); 
}
複製代碼

3.2 棧逆序

題: 給定一個棧,請將其逆序。函數

解1: 若是不考慮空間複雜度,徹底能夠另外弄個輔助棧,將原棧數據所有 pop 出來並 push 到輔助棧便可。

解2: 若是在面試中遇到這個題目,那確定是但願你用更好的方式實現。能夠先實現一個在棧底插入元素的函數,而後即可以遞歸實現棧逆序了,不須要用輔助棧。

* 在棧底插入一個元素
 */
void insertAtBottom(Stack *stack, int v)
{
    if (IS_EMPTY(stack)) {
        push(stack, v);
    } else {
        int x = pop(stack);
        insertAtBottom(stack, v);
        push(stack, x);
    }
}

/**
 * 棧逆序
 */
void stackReverse(Stack *stack)
{
    if (IS_EMPTY(stack))
        return;

    int top = pop(stack);
    stackReverse(stack);
    insertAtBottom(stack, top);
}
複製代碼

3.3 設計包含min函數的棧

題: 設計一個棧,使得push、pop以及min(獲取棧中最小元素)可以在常數時間內完成。

分析: 剛開始很容易想到一個方法,那就是額外創建一個最小二叉堆保存全部元素,這樣每次獲取最小元素只須要 O(1) 的時間。可是這樣的話,爲了建最小堆 pushpop 操做就須要 O(lgn) 的時間了(假定棧中元素個數爲n),不符合題目的要求。

解1:輔助棧方法

那爲了實現該功能,可使用輔助棧使用一個輔助棧來保存最小元素,這個解法簡單不失優雅。設該輔助棧名字爲 minStack,其棧頂元素爲當前棧中的最小元素。這意味着

  • 1)要獲取當前棧中最小元素,只須要返回 minStack 的棧頂元素便可。
  • 2)每次執行 push 操做時,檢查 push 的元素是否小於或等於 minStack 棧頂元素。若是是,則也push 該元素到 minStack 中。
  • 3)當執行 pop 操做的時候,檢查 pop 的元素是否與當前最小值相等。若是相等,則須要將該元素從minStack 中 pop 出去。

代碼:

void minStackPush(Stack *orgStack, Stack *minStack, int v)
{
    if (IS_FULL(orgStack)) {
        printf("Stack Full\n");
        exit(E_FULL);
    }

    push(orgStack, v);
    if (IS_EMPTY(minStack) || v < peek(minStack)) {
        push(minStack, v);
    }
}

int minStackPop(Stack *orgStack, Stack *minStack)
{
    if (IS_EMPTY(orgStack)) {
        printf("Stack Empty\n");
        exit(E_EMPTY);
    }

    if (peek(orgStack) == peek(minStack)) {
        pop(minStack);
    }
    return pop(orgStack);
}

int minStackMin(Stack *minStack)
{
    return peek(minStack);
}
複製代碼

示例:

假定有元素 3,4,2,5,1 依次入棧 orgStack,輔助棧 minStack 中元素爲 3,2,1

解2:差值法

另一種解法利用存儲差值而不須要輔助棧,方法比較巧妙:

  • 棧頂多出一個空間用於存儲棧最小值。
  • push 時壓入的是當前元素與壓入該元素前的棧中最小元素(棧頂的元素)的差值,而後經過比較當前元素與當前棧中最小元素大小,並將它們中的較小值做爲新的最小值壓入棧頂。
  • pop 函數執行的時候,先 pop 出棧頂的兩個值,這兩個值分別是當前棧中最小值 min 和最後壓入的元素與以前棧中最小值的差值 delta。根據 delta < 0 或者 delta >= 0 來得到以前壓入棧的元素的值和該元素出棧後的新的最小值。
  • min 函數則是取棧頂元素便可。

代碼:

void minStackPushUseDelta(Stack *stack, int v)
{
    if (IS_EMPTY(stack)) { // 空棧,直接壓入v兩次
        push(stack, v);
        push(stack, v);
    } else { 
       int oldMin = pop(stack); // 棧頂保存的是壓入v以前的棧中最小值
       int delta = v - oldMin; 
       int newMin = delta < 0 ? v : oldMin;
       push(stack, delta); // 壓入 v 與以前棧中的最小值之差
       push(stack, newMin); // 最後壓入當前棧中最小值
   }
}

int minStackPopUseDelta(Stack *stack)
{
    int min = pop(stack);
    int delta = pop(stack);
    int v, oldMin;

    if (delta < 0) { // 最後壓入的元素比min小,則min就是最後壓入的元素
        v = min;
        oldMin = v - delta;
    } else { // 最後壓入的值不是最小值,則min爲oldMin。
        oldMin = min;
        v = oldMin + delta;
    }

    if (!IS_EMPTY(stack)) { // 若是棧不爲空,則壓入oldMin
        push(stack, oldMin);
    }
    return v;
}

int minStackMinUseDelta(Stack *stack)
{
    return peek(stack);
}
複製代碼

示例:

push(3): [3 3] 
push(4): [3 1 3] 
push(2): [3 1 -1 2] 
push(5): [3 1 -1 3 2] 
push(1): [3 1 -1 3 -1 1] 

min(): 1,pop(): 1,[3 1 -1 3 2]
min(): 2,pop(): 5,[3 1 -1 2] 
min(): 2,pop(): 2,[3 1 3] 
min(): 3,pop(): 4,[3 3] 
min(): 3,pop(): 3,[ ]
複製代碼

3.4 求出棧數目和出棧序列

求出棧數目

題: 已知一個入棧序列,試求出全部可能的出棧序列數目。例如入棧序列爲 1,2,3,則可能的出棧序列有5種:1 2 3,1 3 2 ,2 1 3,2 3 1,3 2 1

解: 要求解出棧序列的數目,還算比較容易的。已經有不少文章分析過這個問題,最終答案就是卡特蘭數,也就是說 n 個元素的出棧序列的總數目等於 C(2n, n) - C(2n, n-1) = C(2n, n) / (n+1) ,如 3 個元素的總的出棧數目就是 C(6, 3) / 4 = 5

若是不分析求解的通項公式,是否能夠寫程序求出出棧的序列數目呢?答案是確定的,咱們根據當前棧狀態能夠將 出棧一個元素入棧一個元素 兩種狀況的總的數目相加便可獲得總的出棧數目。

/**
 * 計算出棧數目
 * - in:目前棧中的元素數目
 * - out:目前已經出棧的元素數目
 * - wait:目前還未進棧的元素數目
 */
int sumOfStackPopSequence(Stack *stack, int in, int out, int wait)
{
    if (out == stack->capacity) { // 元素所有出棧了,返回1
        return 1;
    } 

    int sum = 0;

    if (wait > 0) // 進棧一個元素
        sum += sumOfStackPopSequence(stack, in + 1, out, wait - 1);

    if (in > 0) // 出棧一個元素
        sum += sumOfStackPopSequence(stack, in - 1, out + 1, wait);

    return sum;
}
複製代碼

求全部出棧序列

題: 給定一個輸入序列 input[] = {1, 2, 3},打印全部可能的出棧序列。

解: 這個有點難,不僅是出棧數目,須要打印全部出棧序列,須要用到回溯法,回溯法比簡單的遞歸要難很多,後面有時間再單獨整理一篇回溯法的文章。出棧序列跟入棧出棧的順序有關,對於每一個輸入,都會面對兩種狀況: 是先將原棧中元素出棧仍是先入棧 ,這裏用到兩個棧來實現,其中棧 stk 用於模擬入棧出棧,而棧 output 用於存儲出棧的值。注意退出條件是當遍歷完全部輸入的元素,此時棧 stk 和 output 中均可能有元素,須要先將棧 output 從棧底開始打印完,而後將棧 stk 從棧頂開始打印便可。 另一點就是,當咱們使用的模擬棧 stk 爲空時,則這個分支結束。代碼以下:

void printStackPopSequence(int input[], int i, int n, Stack *stk, Stack *output)
{
    if (i >= n) {
        stackTraverseBottom(output); // output 從棧底開始打印
        stackTraverseTop(stk); // stk 從棧頂開始打印
        printf("\n");
        return;
    }   

    push(stk, input[i]);
    printStackPopSequence(input, i+1, n, stk, output);
    pop(stk);

    if (IS_EMPTY(stk))
        return;

    int v = pop(stk);
    push(output, v); 
    printStackPopSequence(input, i, n, stk, output);
    push(stk, v); 
    pop(output);
}
複製代碼

參考資料

相關文章
相關標籤/搜索