棧結構解析及其應用


導言

隨着生活水平的不斷提高,越來越多的轎車走進千家萬戶,不過這也帶來了一個嚴重的問題——停車位的尋找變得困難,因此在生活中我們經常會遇到把車停在不應該停的位置,導致半夜接到電話要求挪車或者收了罰單。現在我們來想象一個情景,我要在一個只有一個出口的窄巷子停車,那么停在內部的車想要開出來,就必須等在最外面的車開走,新的車停進來,只能停在窄巷子的最外面,最里面的車想要開出來就必須讓其他所有的車都開走。

這真是一種我們很不願意見到的情景,好在現實中司機一般不會做這種事情。如果我們把這個窄巷子抽象成一個線性表,車當做表中的元素,我們會發現這個線性表只能對表尾操作,放入新的元素就必須從表尾放入,由於尾部的元素把表的唯一出口堵死了,因此想要把表中的元素拿出,就只能拿出表尾,即最后一個元素。那么這種特殊的順序表就是一種新的數據結構——棧,它的特點是先進后出,后進先出。棧在計算機相關領域中使用廣泛,舉個大家熟悉的例子,例如瀏覽器的后退功能,同個這個按鍵,我們可以查看單個網頁的頁面之前查看過的連接,而且這個按鍵的操作也是單向的,后查看的鏈接會被先查看。

什么是棧?

棧(stack)又名堆棧,它是一種運算受限的線性表,受限於該結構僅能在表尾進行元素的插入和刪除操作。首先棧本質上還是一個線性表,只是有一些操作上較為特殊,棧中的元素具有仍然具有線性關系。在允許進行插入和刪除的一段被稱之為棧頂,表的另一端被稱為棧底,若在棧中沒有任何元素,棧就被稱為空棧,棧結構的插入操作被稱為壓棧,刪除操作被稱為退棧出棧。棧最鮮明的特點就是先進后出,后進先出,出棧的元素一定是位於棧頂的元素,在棧頂的元素出棧之后,下一個元素就成為新的棧頂,當棧底的元素執行出棧操作之后棧就成為了空棧。

棧的抽象數據類型

ADT Stack
{
    Data:
        D = {ai | 1 ≤ i ≤ n, n ≥ 0, ai 為 ElemType 類型}    //同線性表
    Relation:
        R = { <ai,ai+1> | ai,ai+1 ∈ D, i = 1, i ∈ (0,n)}    //同線性表
    Operation:
        InitStack(&s);    //初始化棧,開辟一個空間給棧 s
        StackEmpty(*s);    //判斷棧是否為空棧,若為空棧返回 true,否則返回 false
        Push(&s,e);    //進棧操作,將元素 e 加入棧結構中並使其作為棧頂
        Pop(&s,&e);    //出棧操作,將位於棧頂的元素刪除,並賦值給變量 e
        GetTop(s,&e);    //取棧頂操作,若棧不為空棧,返回棧頂元素並賦值給變量 e
        ClearStack(&s);    //清空棧,將棧中的所有元素清空,即將棧變為空棧
        DestroyStack(&s);    //銷毀棧,將釋放棧的空間
}

順序棧及其基本操作

順序棧

棧是一種特殊的線性表,也自然可以使用順序存儲結構來實現。在 C\C++ 中,我們對於順序存儲往往使用數組來描述,因此我們需要為一個數組選擇棧底和棧頂,為了方便描述空棧判定和棧滿判定,我們使用下標為 0 的位置作為棧底,當棧頂的下標為數組元素上限時即為棧滿,為了時刻定位棧頂的位置,需要定義一個棧頂指針作為游標來輔助。

順序棧的結構體定義

#define MAXSIZE 100
typedef struct
{
    ElemType data[MAXSIZE];
    int top;    //棧頂指針
}SqStack;

初始化棧

為一個新建立的棧 s 分配足夠的空間,由於空棧沒有任何元素,因此棧頂指針將初始化為 -1。

void InitStack(SqStack s)
{
    s = new SqStack;
    s->top = -1;    //棧頂指針將初始化為 -1,表示沒有任何元素
}

空棧判斷

某個結構為空一直是一個顯然而敏感的狀態,如果不妥善處理就會出現嚴重的異常,就例如對空棧執行出棧操作,就會出現非法讀取的情況。因此雖然空棧判斷的代碼簡單,但是值得我們重視。函數在棧為空棧時返回 true,反之返回 false。

bool StackEmpty(SqStack *s)
{
    if(s->top == -1)
    {
        return true;
    }
    return false;
}

進棧操作

由於棧是一種操作受限的線性表,因此進棧操作是其核心操作之一。進棧的關鍵在於只能在表尾進行插入,並且當棧的空間為滿的時候,不能入棧。函數將在棧不為滿棧的情況下,在棧頂指針 top 處插入元素 e 並使其自增 1,插入成功返回 true,否則返回 false。時間復雜度 O(1)。

bool Push(SqStack &s,ElemType e)
{
    if(s->top == MAXSIZE - 1)    //判斷是否棧滿
    {
        return false;
    }
    s->data[s->top++] = e;    //入棧
    return true;
}

出棧操作

同進棧,出棧也是很重要的操作,出棧的關鍵在於只能在表尾進行插入,並且當棧的空間為空的時候,不能出棧。函數將在棧不為空棧的情況下,將位於棧頂指針 top 處的元素出棧並賦值給變量 e ,top 需要並使其自減 1,退棧成功返回 true,否則返回 false。時間復雜度 O(1)。

bool Pop(SqStack &s,ElemType e)
{
    if(StackEmpty(s))    //判斷是否為空棧
    {
        return false;
    }
    e = s->data[s->top--];    //退棧
    return true;
}

取棧頂操作

取棧頂操作與出棧操作不同的是,取棧頂操作只需把棧頂元素賦值給變量 e,無需對棧進行修改。時間復雜度 O(1)。

bool GetTop(SqStack &s,ElemType e)
{
    if(StackEmpty(s))    //判斷是否為空棧
    {
        return false;
    }
    e = s->data[s->top];    //取棧頂
    return true;
}

鏈棧及其基本操作

鏈棧

當棧使用鏈式存儲結構來存儲時,可以建立單鏈表來描述,顯然以鏈表的表頭結點作為棧頂是最方便的。使用連式存儲結構的優點在於,棧的空間在一般情況下不需要考慮上限。對於鏈棧來說,我們可以不設置頭結點。

鏈棧的結構體定義

typedef struct StackNode
{
    ElemType data;
    struct StackNode *next;
}Node,*Stack;

初始化棧

初始化的操作是為了構造一個空棧,在不設置頭結點的情況下,我們把棧頂指針搞成 NULL 即可。

bool InitStack(Stack &s)
{
    s = NULL;
    return true;
}

空棧判斷

某個結構為空一直是一個顯然而敏感的狀態,如果不妥善處理就會出現嚴重的異常,就例如對空棧執行出棧操作,就會出現非法讀取的情況。因此雖然空棧判斷的代碼簡單,對於鏈棧值得我們重視。函數在棧為空棧時返回 true,反之返回 false。

bool StackEmpty(Stack *s)
{
    if(s == NULL)
    {
        return true;
    }
    return false;
}

進棧操作

對於鏈棧的進棧操作,我們不需要判斷是否出現棧滿的情況,只需要用頭插法引入新結點即可,插入成功返回 true,否則返回 false。時間復雜度 O(1)。

bool Push(Stack &s,ElemType e)
{
    Stack ptr = new Node;    //為新結點申請空間
    
    ptr->next = s;    //修改新結點的后繼為 s 結點,入棧
    ptr->data = e;
    s = ptr;    //修改棧頂為 ptr
    return true;
}

出棧操作

同順序棧,當棧的空間為空的時候,不能出棧,函數將在棧不為空棧的情況下,需要把棧頂結點的空間釋放掉,退棧成功返回 true,否則返回 false。時間復雜度 O(1)。

bool Pop(Stack &s,ElemType e)
{
    Stack ptr;

    if(StackEmpty(s))    //判斷是否為空棧
    {
        return false;
    }
    e = s->data;    //將棧頂元素賦值給 e
    ptr = S;    //拷貝棧頂元素
    S = S->next;    //退棧
    delete ptr;    //釋放原棧頂元素結點的空間
    return true;
}

取棧頂操作

當棧非空時,把棧頂元素賦值給變量 e,時間復雜度 O(1)。

bool GetTop(SqStack &s,ElemType e)
{
    if(StackEmpty(s))    //判斷是否為空棧
    {
        return false;
    }
    e = s->data;    //取棧頂
    return true;
}

雙端棧

實現目標

復雜的操作由基本操作組合而成

我們這么去理解,假設我們已經定義了兩個棧,開辟了一定的空間,那么會不會出現一個棧滿了,而另一個棧還有很多空間呢?那么我們在這個時候就很希望能夠讓第一個棧使用第二個棧的空間,從理論上講,這樣是完全可行的,因為我們只需要讓這兩個棧能夠分別找到自己的棧頂和棧底即可。例如在一個數組中,我們可以讓數組的始端和末端分別為兩個棧的棧底,再通過操作游標來實現對棧頂的描述。對於棧滿的判斷呢?只要兩個棧的棧頂不見面,棧就不為滿棧。


代碼實現

建立雙端棧

Stack CreateStack(int MaxSize)    //建立雙端棧
{
    Stack sak = (Stack)malloc(sizeof(struct SNode));
    sak->MaxSize = MaxSize;
    sak->Data = (ElementType*)malloc(MaxSize * sizeof(ElementType));
    sak->Top1 = -1;
    sak->Top2 = MaxSize;

    return sak;
}

入棧操作

bool Push(Stack S, ElementType X, int Tag)    //入棧
{
    if (S->Top2 - 1 == S->Top1)
    {
        printf("Stack Full\n");
        return false;
    }

    if (Tag == 1)
    {
        S->Data[++S->Top1] = X;
    }
    else
    {
        S->Data[--S->Top2] = X;
    }
    return true;
}

出棧操作

ElementType Pop(Stack S, int Tag)    //出棧
{
    if (Tag == 1)
    {
        if (S->Top1 < 0)
        {
            printf("Stack %d Empty\n",Tag);
            return ERROR;
        }
        else
        {
            return S->Data[S->Top1--];
        }
    }
    else
    {
        if (S->Top2 == S->MaxSize)
        {
            printf("Stack %d Empty\n",Tag);
            return ERROR;
        }
        else
        {
            return S->Data[S->Top2++];
        }
    }
}

棧的應用-符號配對

應用情景

情景分析

由於我們只關注表達式的括號是否是成雙成對的,因此只需要獲取我們所需即可。當我獲取第一個括號時,雖然后面可能會有賊多括號,但是我們只繼續接受下一個括號,若下一個括號仍然為左括號,那么這個括號需要配對的優先級是高於第一個左括號的。繼續讀取,若下一個括號為右括號,就拿來和配對優先級較高的第二個括號比對,若成功配對則消解第二個括號,而第一個括號需要配對的優先級就提升了。經過分析我們發現,使用棧結構來描述這個過程極為合適。

偽代碼

代碼實現

#include <iostream>
#include <stack>
#include <string>
using namespace std;
int main()
{
    string equation;
    stack<char> brackets;   //存儲被配對的左括號
    int flag = 0;

    cin >> equation;
    for (int i = 0; equation[i] != 0; i++)
    {
        if (equation[i] == '(' || equation[i] == '[' || equation[i] == '{')    //第 i 個字符是左括號
        {
            brackets.push(equation[i]);
        }
        else if (brackets.empty() && (equation[i] == ')' || equation[i] == ']' || equation[i] == '}'))
        {
            flag = 1;                                                     //第 i 個字符是右括號但棧是空棧
            break;
        }
        else if (equation[i] == ')' && brackets.top() == '(')    //棧頂括號與右括號配對
        {
            brackets.pop();
        }
        else if (equation[i] == ']' && brackets.top() == '[')
        {
            brackets.pop();
        }
        else if (equation[i] == '}' && brackets.top() == '{')
        {
            brackets.pop();
        }
    }

    if (flag == 1)    //輸出配對結果
    {
        cout << "no";
    }
    else if (brackets.empty() == true)
    {
        cout << "yes";
    }
    else
    {
        cout << brackets.top() << "\n" << "no";
    }
    return 0;
}

棧的應用-逆波蘭式的轉換

逆波蘭式

眾所周知,對於一個算式而言,不同的運算符有優先級之分,例如“先乘除,后加減”,如果是我們人工進行計算的話,可以用肉眼觀察出算式的運算順序進行計算。可是對於計算機而言,如果是一個一個讀取算式進行計算的話,可能不能算出我們想要的答案,因為這么做是沒有優先級可言的。想要讓計算機實現考慮優先級的算式計算,我們首先要先找到一種算式的描述方式,這種方式不需要考慮運算符優先級。
逆波蘭式(Reverse Polish notation,RPN,或逆波蘭記法),也叫后綴表達式(將運算符寫在操作數之后的表達式),是波蘭邏輯學家盧卡西維奇提出的,例如“2 + 3 * (7 - 4) + 8 / 4”這樣一個表達式,它對應的后綴表達式是“2 3 7 4 - * + 8 4 / +”,這種表達式的計算方法是遇到運算符就拿前面的兩個數字來計算,用這個數字替換掉計算的兩個數字和運算符,直到得出答案。

應用情景

偽代碼

代碼實現

#include <iostream>
#include <stack>
#include <queue>
#include <string>
#include <map>
using namespace std;

int main()
{
	string str;
	stack<char> sign;    //存儲符號
	queue<char> line;    //存儲轉換好的逆波蘭式,便於后續實現計算
	map<char, int> priority;
	priority['('] = 3;    //為符號設置優先級
	priority[')'] = 3;
	priority['*'] = 2;
	priority['/'] = 2;
	priority['+'] = 1;
	priority['-'] = 1;
	int flag = 0;

	cin >> str;
	for (int i = 0; i < str.size(); i++)
	{                                                                                           //讀取到數字
		if (((i == 0 || str[i - 1] == '(') && (str[i] == '+' || str[i] == '-')) || (str[i] >= '0' && str[i] <= '9'))
		{
			line.push('#');
			if (str[i] != '+')
			{
				line.push(str[i]);
			}
			while ((str[i + 1] >= '0' && str[i + 1] <= '9') || str[i + 1] == '.')
			{
				line.push(str[++i]);
			}
		}
		else    //讀取到運算符
		{
			if (str[i] == ')')    //運算符是右括號
			{
				while (!sign.empty() && sign.top() != '(')    //左括號之后的運算符全部出棧
				{
					line.push('#');
					line.push(sign.top());
					sign.pop();
				}
				sign.pop();
				continue;
			}
			else
			{
				while (!sign.empty() && sign.top() != '(' && priority[str[i]] <= priority[sign.top()])
				{
					line.push('#');
					line.push(sign.top());
					sign.pop();
				}
			}
			sign.push(str[i]);
		}
	}
	while (!sign.empty())    //將棧內剩余的符號出棧
	{
		line.push('#');
		line.push(sign.top());
		sign.pop();
	}

	while (!line.empty())
	{
		if (flag == 0 && line.front() == '#')
		{
			flag++;
		}
		else if(line.front() == '#')
		{
			cout << ' ';
		}
		else
		{
			cout << line.front();
		}
		line.pop();
	}
	return 0;
}

迷宮尋路(深度優先)

左轉博客——棧和隊列應用:迷宮問題

八皇后問題(棧實現)

左轉我另一篇博客八皇后問題——回溯法思想運用

參考資料

《大話數據結構》—— 程傑 著,清華大學出版社
《數據結構教程》—— 李春葆 主編,清華大學出版社
《數據結構與算法》—— 王曙燕 主編,人民郵電出版社
《數據結構(C語言版|第二版)》—— 嚴蔚敏 李冬梅 吳偉民 編著,人民郵電出版社


免責聲明!

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



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