這個系列是我多年前找工作時對資料結構和演算法總結,其中有基礎部分,也有各大公司的經典的面試題,最早釋出在CSDN。現整理為一個系列給需要的朋友參考,如有錯誤,歡迎指正。本系列完整程式碼地址在 這裡。
0 概述
棧作為一種基本的資料結構,在很多地方有運用,比如函式遞迴,前字尾表示式轉換等。本文會用C陣列來實現棧結構(使用連結串列實現可以參見連結串列那一節,使用頭插法構建連結串列即可),並對常見的幾個跟棧相關的面試題進行分析,本文程式碼在 這裡。
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:取棧頂元素,但是不修改棧。
如圖所示:
程式碼如下:
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 + *
,求該字尾表示式的值。
解: 字尾表示式也叫逆波蘭表示式,其求值過程可以用到棧來輔助儲存。則其求值過程如下:
- 1)遍歷表示式,遇到的數字首先放入棧中,此時棧為
[6 5 2 3]
。 - 2)接著讀到
+
,則彈出3和2,計算3 + 2
,計算結果等於5
,並將5
壓入到棧中,棧為[6 5 5]
。 - 3)讀到
8
,將其直接放入棧中,[6 5 5 8]
。 - 4)讀到
*
,彈出8
和5
,計算8 * 5
,並將結果40
壓入棧中,棧為[6 5 40]
。而後過程類似,讀到+
,將40
和5
彈出,將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)
的時間。但是這樣的話,為了建最小堆 push
和 pop
操作就需要 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);
}
複製程式碼