資料結構與演算法--棧
基本定義
棧是限定僅在表尾進行插入或刪除操作的線性表。先進後出
通常,表頭端稱為棧底,表尾端稱為棧頂。
棧只是對錶插入和刪除操作的位置進行了限制,並沒有限定插入和刪除操作進行的時間。
基本操作
InitStack( &S ) //構造空棧 StackEmpty( S ) //判斷棧空 Push( &S, e ) //出棧 Pop( &S, &e ) //退棧、出棧 GetTop( S, &e ) //取出棧頂元素 DestroyStack( &S ) //銷燬棧 ClearStack( &S ) //清空棧 StackLength( S ) //返回棧長 StackTraverse( S, visit() ) //遍歷棧
棧的表示和實現
順序棧
base指標指向線性表的第一個元素,稱為棧底指標。
top指標指向線性表的最後一個元素的下一個位置,稱為棧頂指標。
棧不存在:base == NULL;空棧:base == top
//結構的定義
typedef int SElemType;
typedef struct
{
SElemType *base; //棧底指標
SElemType *top; //棧頂指標
int stacksize; //當前可用空間大小
}SqStack;
readme
上面結構的定義只是為了更加方便理解順序棧
下面程式碼是棧的順序實現,需要藉助順序表進行實現
由於和順序表基本一致,我們採用類的繼承來簡化程式碼
同時,我們省略了下面程式碼的標頭檔案
如需執行,和順序表相關程式碼結合即可
需要將順序表中解構函式設為虛擬函式
//順序表實現棧,繼承自List //使用L來表示棧,另外設棧頂和棧底指標 template <typename ElemType> class Stack :public List<ElemType> { public: ElemType* base;//指向L實現的棧的棧底 ElemType* top;//指向L實現的棧的棧頂 Stack();//建構函式 ~Stack();//解構函式 Status InitStack();//棧的初始化 Status Stack_Pop();//彈出棧頂元素 Status Stack_Push(ElemType x);//壓入棧元素 Status Stack_Destroy();//銷燬棧 Status Stack_Clear();//清空棧 ElemType Stack_GetTop();//返回棧頂元素 Status Stack_Empty();//判斷棧是否為空 }; //Stack的建構函式 template <typename ElemType> Stack<ElemType>::Stack() { InitStack(); } //Stack的解構函式,只需要在base和top非空時釋放指標即可 template <typename ElemType> Stack<ElemType>::~Stack() { if (!base) free(base); if (!top) free(top); } //Stack的初始化 template <typename ElemType> Status Stack<ElemType>::InitStack() { //若構造的類為模板類,那麼派生類不可以直接使用繼承到的基類資料和方法,需要通過this指標使用 List<ElemType>::InitList_Sq(); base = this->L.elem; top = base;//初始化的時候,top = base return OK; } //從棧的初始化我們也能看出來 //base為空代表棧不存在,而base == top時代表棧為空 //棧的pop其實就是順序表刪除最後一個元素(相當於棧頂元素) template <typename ElemType> Status Stack<ElemType>::Stack_Pop() { ElemType e; //這裡根據需要設定e,如需返回pop的元素值,可以在引數列表中增加一個實參,將pop的元素值返回,同List_Delete List<ElemType>::List_Delete(this->L.length - 1, e); //不採取-1是為了避免刪除失敗而導致top錯誤 //另一種寫法是根據List_Delete的返回值判斷是否-1 top = this->L.elem + this->L.length; return OK; } template <typename ElemType> Status Stack<ElemType>::Stack_Push(ElemType x) { //儘管我們線上性表實現中已經考慮了空間不足L指標變化, //但由於base和top並未同時改變,所以我們還需在此基礎上對top和base重新賦值 base = this->L.elem; top = this->L.elem + this->L.length; List<ElemType>::List_Insert(this->L.length,x); ++top;//保持top始終指向最後一個元素的下一個位置 return OK; } //對棧進行銷燬,不僅需要考慮我們在stack子類中新增的指標 //還需要對父類中設定的elem指標進行釋放 template <typename ElemType> Status Stack<ElemType>::Stack_Destroy() { if (!base || !top) { printf("\nREFREE\n"); exit(REFREE); } free(base);//棧底指標的釋放 free(top);//棧頂指標的釋放 //釋放後將指標置空,避免成為野指標 base = NULL; top = NULL; List<ElemType>::List_Destroy();//棧的釋放 return OK; } //棧的清空,不需要將棧銷燬,只需要清空棧的元素 //由於棧的元素是在父類中,我們只需要呼叫父類函式即可 template <typename ElemType> Status Stack<ElemType>::Stack_Clear() { List<ElemType>::List_Clear(); base = this->L; top = this->L; return OK; } //得到棧頂元素,一個看似雞肋卻很實用的功能 template <typename ElemType> ElemType Stack<ElemType>::Stack_GetTop() { if (base == top) return ERROR; //再次提醒,top指向最後一個元素的下一個位置 return *(top-1); } //判斷棧是否為空,結合棧的初始化函式來看,更加容易理解 template <typename ElemType> Status Stack<ElemType>::Stack_Empty() { if(!base) { printf("\nError!!No exist\n"); exit(ERROR); } if (top == base) return TRUE; return FALSE; }
鏈式棧
同順序棧,我們藉助連結串列來實現鏈式棧
其實仔細思考就會發現,連結串列和鏈式棧的底層原理都一樣,只是一些函式(或者叫方法)不同,我們在後續程式碼中會更加詳細的看到。
為了方便出棧和入棧操作,我們將連結串列的首元結點作為棧頂。(如果採用連結串列結尾,我們還需要遍歷到尾部,增加了不必要開銷)
top指標指向頭結點(首元結點的前一個附加節點),這符合top指標指向最後一個元素的後一個位置的特點。
棧不存在:top == NULL;空棧:top -> next == NULL;(top指向最後一個元素的下一個位置,這裡top->next實際就是最後一個元素,為空時自然就代表棧為空)
//結構的定義
typedef int SElemType;
typedef struct SNode
{
SElemTye data;
dtruct SNode *next;
}SNode, *LinkStack;
readme
上面結構的定義只是為了更加方便理解鏈式棧
下面程式碼是棧的鏈式實現,需要藉助連結串列進行實現
由於和連結串列基本一致,我們採用類的繼承來簡化程式碼
同時,我們省略了下面程式碼的標頭檔案
如需執行,和連結串列相關程式碼結合即可
需要將連結串列中解構函式設為虛擬函式
//鏈式表實現棧,繼承自Link
template <typename ElemType>
class Stack :public Link<ElemType>
{
public:
//連結串列的頭指標即為top指標
//不再專門設base指標(實際為連結串列最後一個元素)
Stack();//建構函式
~Stack();//解構函式
Status InitStack();//棧的初始化
Status Stack_Pop();//彈出棧頂元素
Status Stack_Push(ElemType e);//壓入棧元素
Status Stack_Destroy();//銷燬棧
Status Stack_Clear();//清空棧
ElemType Stack_GetTop();//返回棧頂元素
Status Stack_Empty();//判斷棧是否為空
};
//Stack的建構函式
template <typename ElemType>
Stack<ElemType>::Stack()
{
Link<ElemType>::Init_LinkList();
}
//Stack的解構函式
template <typename ElemType>
Stack<ElemType>::~Stack()
{
//這裡子類中沒有增加需要釋放的指標
//在保證父類虛構函式時會自動釋放空間
}
//Stack的初始化
template <typename ElemType>
Status Stack<ElemType>::InitStack()
{
Link<ElemType>::Init_LinkList();
}
template <typename ElemType>
Status Stack<ElemType>::Stack_Pop()
{
ElemType e;//e暫時用不到,如需使用,在引數列表中增加實參即可
Link<ElemType>::LinkList_Delete(0, e);
//LinkList_Delete不會改變頭指標,即top指標仍然指向連結串列第一個元素(鏈式棧的最後一個元素)
return OK;
}
template <typename ElemType>
Status Stack<ElemType>::Stack_Push(ElemType e)
{
//同delete函式,實質就是在鏈式棧末尾(也就是連結串列第一個元素摻入和刪除)
Link<ElemType>::LinkList_Insert(0, e);
return OK;
}
//不同於解構函式自動呼叫父類析構,我們需要手動回收
template <typename ElemType>
Status Stack<ElemType>::Stack_Destroy()
{
Link<ElemType>::LinkList_Destroy();
return OK;
}
template <typename ElemType>
Status Stack<ElemType>::Stack_Clear()
{
Link<ElemType>::LinkList_Clear();
return OK;
}
template <typename ElemType>
ElemType Stack<ElemType>::Stack_GetTop()
{
if (!this->L)
{
printf("Error!!NO Exist\n");
exit(ERROR);
}
if (!(this->L->next))
{
printf("Error!!NULL\n");
exit(ERROR);
}
return this->L->next->data;
}
template <typename ElemType>
Status Stack<ElemType> ::Stack_Empty()
{
if (!this->L)
{
printf("Error!!NO Exist\n");
exit(ERROR);
}
if (!this->L->next)
return TRUE;
return FALSE;
}
順序表和連結串列的比較
- 時間效能:相同,都是常數時間O(1)
- 空間效能:順序棧有元素個數的限制和空間浪費的問題;鏈式棧,沒有棧滿的問題,只有當記憶體沒有可用空間時才會出現棧滿,但是每一個數據元素都需要一個指標域,增加了結構性開銷.
- 總之,在棧的使用過程中,如果元素個數變化較大,用鏈棧比較適宜,反之應採用順序棧.
棧的應用
簡單的比如,數制轉換(實際上只是在輸出時用到了棧),括號匹配等。下面重點分析表示式求值和中綴字尾表示式轉換。
四則表示式求值
readme
首先我們需要確保表示式的正確性。
需要設定兩個工作棧,分別是運算子棧和運算元棧。
從左往右讀取表示式
A.當遇到運算元的時候,直接將運算元進棧。
B.當遇到運算子的時候,需要分為以下三種情況
1.如果當前運算子高於棧頂運演算法,則當前運算子進棧,遍歷下一個資料
2.如果當前運算子等於棧頂運算子,則將棧頂元素出棧,並遍歷下一個元素
3.如果當前運算子低於棧頂運算子,則將棧頂運算子彈出,同時將運算元棧彈出兩個運算元,根據棧頂運算子進行運算。這時候不進行表示式下一個資料遍歷,仍然保持當前運算子。
----幾點說明:
----1.運算子優先順序比較時,我們是將相同運算子(除了括號和#)視作不相等的,即如果棧頂運算子和當前運算子相等,那我們視作當前運算子低於棧頂運算子,也就是要進行運算.
----2.只有括號和#才能視作相等,即左括號等於右括號,#等於#
----3.為了保證所有操作符都進行運算(在保證表示式正確基礎上),我們在表示式結尾增加一個#
----4.左括號視作優先順序最高,即無條件入棧;而右括號由優先順序最低,會一直進行運算,直到碰到左括號,然後相等,左括號彈出,同時進行表示式下一個資料遍歷.下面給出一種程式碼實現
輸入:一行字串,允許出現空格和浮點數
輸出:判斷表示式是否正確,如果錯誤,不進行計算.計算結果採用浮點數表示
以下為非法輸入,並提示“input error”
1.負數出現在非首位且不加括號
2.運算子(減號出現在首位視為負號)出現在首尾,或者連續出現
3.小數點左右兩邊存在不為數字的(包括空格)
4.小數點出現在末尾非法
5.左右括號不匹配
6.左括號右側出現 + * / ,右括號左側出現 + - * /
7.出現 + - * / ( ) 數字,小數點和空格外的任何符號
8.數字右側出現左括號,數字左側出現右括號
說明:我們規定如果表示式中含有負數,除了首位的負數可以省略括號外,其餘必須在括號內
//判斷表示式是否合法
int judge(std::string& str)
{
//首先我們將字串中空格去除
//同時,判斷左右括號是否合法,數字是否合法
int cnt_l = 0;//左括號數量
int cnt_r = 0;//右括號數量
std::string s = "";
for (unsigned int i = 0; i < str.size(); ++i)
{
if (str[i] == '(')
++cnt_l;
else if (str[i] == ')')
++cnt_r;
else if (str[i] == '.')
{
if (i == 0 || i == s.size() - 1)//小數點在首尾為false
return FALSE;
else if (!isdigit(str[i - 1]) || !isdigit(str[i + 1]))//左右不是數字非法
return FALSE;
}
else if (str[i] == ' ')//數字之間存在空格非法
{
if (i != 0 && i != str.size() - 1)
if(isdigit(str[i - 1]) && isdigit(str[i + 1]))
return FALSE;
}
if (cnt_l < cnt_r)//從左往右,右括號多了就說明左右已經不匹配了
return FALSE;
if (str[i] != ' ')
s += str[i];
}
if (cnt_l != cnt_r)//左右括號不相等非法
return FALSE;
//接下來就是判斷運算子是否合法
for (unsigned int i = 0; i < s.size(); ++i)
{
if (isdigit(s[i]) || s[i] == '.')//跳過數字
{
if (i > 0 && s[i - 1] == ')')
return FALSE;
else if (i < s.size()-1 && s[i + 1] == '(')
return FALSE;
}
else if (s[i] == '+' || s[i] == '*' || s[i] == '/')
{
if (i == 0 || i == s.size() - 1)//+ * / 運算子在首尾非法
return FALSE;
else if (!(isdigit(s[i - 1]) || s[i - 1] == ')'))//+ * / 運算子前如果不是數字或者)非法
return FALSE;
else if (!(isdigit(s[i + 1]) || s[i + 1] == '('))//+ * / 運算子後如果不是數字或者 ( 非法, ( 是代表後面為負數
return FALSE;
}
else if (s[i] == '-')
{
if (i == s.size() - 1)//末尾非法
return FALSE;
else if (i == 0)//出現在第一位的減號非法
{
if(!(isdigit(s[i + 1])||str[i+1]=='('))//這句話不能寫在上面的條件判斷裡面,已改
return FALSE;
}
else if (!(isdigit(s[i - 1]) || s[i - 1] == '(' || s[i - 1] == ')'))
return FALSE;
else if (!(isdigit(s[i + 1]) || s[i + 1] == '('))
return FALSE;
}
else if (s[i] == '(' && s[i + 1] == ')')//空括號返回FALSE,表示式的括號匹配保證了(不會出現在末尾,i+1不會溢位
return FALSE;
else if(s[i] != '('&&s[i] != ')')
return FALSE;
}
str = s;//如果合法,替換str
return TRUE;
}
//a是運算子棧頂元素,也就是表示式中先出現的運算子
//b是當前運算子,為後出現的運算子
//a<b -1;a=b 0;a>b 1;
//(無條件入棧,右括號無條件計算
//運算子優先順序相等視作>,即要進行運算
//由於輸入的合法性,a不會為);a為#時,b不會為)
//我們會在表示式末尾加上#以避免最後再進行一次處理,所以需要考慮b為#的情況
int Compare(char a, char b)
{
if (a == '+' || a == '-')
{
if (b == '*' || b == '/' || b == '(')//將b入棧
return -1;
return 1;//b只能為+-,進行運算
}
else if (a == '*' || a == '/')
{
if (b == '(')//將b進棧
return -1;
return 1;
}
else if (a == '(')//棧頂還為(時,一定未到表示式末尾,所以b不會為#
{
if (b == ')')
return 0;
return -1;//實際中不會出現b為#
}
//a不能為)
else//這時a只能為#
{
if (b == '#')
return 0;
return -1;
}
}
double cmp(std::string& str,Stack<double>& s1, Stack<char>& s2)//四則運算表示式求值
{
str += '#';//附加#,如果出現到表示式結尾運算子棧仍然包含多個運算子時能繼續運算,直到為空(最後一個#和附加#相同出棧)
s2.Stack_Push('#');
double x;
std::string s = "";//預處理字元段
std::stringstream ss;
unsigned int i = 0;
double x1 = 0, x2 = 0;
char ch;
while (i < str.size())
{
if (isdigit(str[i]))
{
s = "";
ss.clear();
while ((isdigit(str[i])||(str[i]=='.')) && i < str.size())
{
s += str[i];
++i;
}
ss << s;
ss >> x;
s1.Stack_Push(x);//運算元進棧
}
else if ((str[i] == '-') &&(i == 0 ||( i>0 && str[i - 1] == '('))&&str[i]!='(')//運算元進棧
{
s = "";
ss.clear();
s += str[i];
++i;
while ((isdigit(str[i])||(str[i]=='.')) && i < str.size())
{
s += str[i];
++i;
}
ss << s;
ss >> x;
s1.Stack_Push(x);
}
else//運算子進棧
{
char a = s2.Stack_GetTop(), b = str[i];//a是當前運算子,b是運算子棧頂元素
switch (Compare(a, b))
{
case -1://棧頂元素比當前運算子低,入棧
++i;
s2.Stack_Push(b);
break;
case 0://相等,出棧
++i;
s2.Stack_Pop();
break;
case 1://棧頂元素比當前運算子高,進行計算
//這時不能++i,要繼續將當前運算子和棧頂運算子進行比較,很重要
x2 = s1.Stack_GetTop();//第二個運算數
s1.Stack_Pop();
/*
說明,我們有時間很難確定負號和減號,比如-(-(-(1))),如果我們把其看作負號的話,如果最裡層括號是一個運算式而不是單純一個數,那麼在識別數字的時候是無法完成計算的,也就無法將-看作負號,所以我們統一將-後面是(的看作是減號,這產生的問題就是可能會出現只有一個運算元但是還剩下操作符的情況,比如-(-(-(1)))這種,最後就剩下三個減號和一個1,所以我們就將這時間的第一個操作符設為0從而完成運算.如下面程式碼所示.
*/
if(!s1.Stack_Empty());//第一個運算數
{
x1 = s1.Stack_GetTop();
s1.Stack_Pop();
}
else
x1 = 0;
ch = s2.Stack_GetTop();
s2.Stack_Pop();
switch (ch)
{
case '+':
x1 += x2;
break;
case '-':
x1 -= x2;
break;
case '*':
x1 *= x2;
break;
case '/':
x1 /= x2;
break;
default:
break;
}
s1.Stack_Push(x1);
break;
default:
break;
}
}
}
double ans= s1.Stack_GetTop();//在前期保證表示式合法性的基礎上,此時s1只有一個元素
s1.Stack_Pop();
return ans;
}
中綴轉字尾
中綴表示式:a*b + c/d
字首表示式:+*ab/cd
字尾表示式(逆波蘭表示式):ab*cd/+(便於計算)
- 遇見運算元,直接進棧
- 遇見操作符,彈出運算元並將計算結果入棧
- 無界限符
參考四則表示式,這裡只簡單介紹演算法
從左往右遍歷四則表示式
遇見數字 :直接輸出
遇見 ( :壓入棧
遇見 ) :持續出棧直到 ( ,如果出棧元素不是 ( 則輸出,否則停止出棧.左括號和右括號均不輸出
遇見符號:判斷與棧頂符號的優先順序,如果低於等於棧頂元素,則將持續將棧頂元素彈出並輸出,直到不再低於等於棧頂元素.最後將當前符號入棧.
處理完字串,將棧中剩餘符號全部輸出.
棧與遞迴的實現
一個函式呼叫另一個函式,執行前需要完成
- 傳遞所有的實在引數和返回地址
- 為被呼叫函式的區域性變數分配儲存區
- 將控制轉移到被呼叫函式的入口
被呼叫函式返回之前,需要完成
- 儲存函式計算結果
- 釋放被調函式資料區
- 將控制返回到呼叫函式