八、【栈和队列】栈的应用
棧的應用
棧具有先進后出的特點,這個特點在解決某些問題時是很有效的。本節我們來看幾個棧的常見應用以及棧結構適合解決的問題類型。
1 數制轉換
我們日常生活中用的是十進制,而計算機中絕大多數時候是二進制,八進制或十六進制,這就涉及到數制轉換的問題。十進制向其他進制轉換一般是通過連除實現的,例如,如果我們想找出十進制的 42 對應的二進制表示,就需要對 42 連除 2,直到商為 0 。下圖展示了完整過程:
用公式概括就變為了:
N=(Ndivd)×d+NmoddN = (N\ \mathrm{div} \ d ) \times d + N\ \mathrm{mod}\ d N=(N?div?d)×d+N?mod?d
其中,div\mathrm{div}div 為整除運算,mod\mathrm{mod}mod 為求余運算。
假設現在要編寫一個程序來將任意的十進制數轉換為八進制數。由于上述方法最先計算出的余數位于最低位,最后計算出的位于最高位,符合后入先出的思想,所以可以用棧來保存余數。
/** Function: 進制轉換函數* ----------------------------* 輸入一個十進制數,轉換為對應的八進制表示。*/ void conversion(int n){int r; // 用來保存余數LinkedStack S;InitStack(S);while (n!=0){r = n % 8; // 求余數n = n / 8; // 將n更新為商,以便后續運算Push(S, r); // 將余數入棧}Print(S); // 將棧內元素輸出 }2 括號匹配問題
括號匹配問題是關于棧的結構考察中最常見的一類問題,也是很能體現棧的結構特點的一類問題。假設一個表達式中可以包含三種括號:小括號,中括號和大括號,我們要用程序來檢測這個表達式中每一個左括號是否都有以正確形式與之對應的右括號。例如:
// 為了表示方便,表達式中只寫出括號 {[()]} // 正確 {}[()]() // 正確 )[] // 錯誤,缺失對應的括號 [(]) // 錯誤,對應形式不正確讓我們先以 “{ [ ( ) ] }” 為例,分析一下如何正確的檢測。
通過上述分析我們發現,在解決括號匹配問題的過程中,我們提出了一系列的任務,最后被提出的任務擁有最高的優先級,即最緊急的任務,需要最早被完成。這一點也正好符合棧的特點:后入先出,因此可以用棧來幫助解決這個問題。詳細的實現步驟如下:
上述步驟中,左括號會被壓入棧內,但右括號一定不會被壓入棧內,只用來檢測棧頂元素是否匹配。這主要是因為在該問題中左括號和右括號有不同的作用,左括號可以被抽象地理解為待操作的數值,待解決的問題等,而右括號就是對應的操作或解決辦法。具體到這個問題,左括號就代表一個待匹配的任務,而對應的右括號就代表完成了匹配任務。嘗試將輸入數據分為待操作的數值和對應的操作兩類是用棧解決很多問題時的常用思路,在下面的問題中會有更多說明。
括號匹配問題實現代碼如下:
/** Function: 括號匹配檢測* ----------------------------* 檢測一個表達式中的括號是否匹配正確。* 支持的括號種類有三種:{}, []和()。*/ bool bracketMatch(string str){LinkedStack S;InitStack(S);string match = "{[(}])"; // 用來記錄可能的括號輸入char bracket;// 首先檢測第一個元素是左括號還是右括號if (match.find(str[0])>2){return false;}for (int i=0;i<str.length();i++){// 開始配對if (match.find(str[i])<3){ // 如果是是左括號Push(S, str[i]); } else { // 如果是右括號GetTop(S, bracket);if (match.find(str[i])-3==match.find(bracket)){ Pop(S, bracket);} else {return false;}}}return StackEmpty(S); }3 行編輯程序
行編輯程序是一種簡單的文本編輯程序,它的主要功能是:接收用戶輸入的數據并存如數據區。但是用戶在進行輸入時不能保證不出差錯,所以如果每輸入一個數據就立刻保存顯然是一種不好的做法。因此,我們希望有一段緩沖區,可以接收用戶輸入的一行數據,如果發現錯誤可以及時更改。例如,可以規定 “#” 為退格符,表示前一位數據無效;如果發現一行內有太多錯誤,可以規定 “@” 為退行符,表示當前行無效。例如,假設程序接收了這樣的兩行:
whli##ile (s#*s)outcha@putchar(*s=#++);實際有效的是下列兩行:
while (*s)putchar(*s++);這種編輯程序的特點就是無法使用光標選擇想要修改的位置,當前操作只能修改最新輸入的字符,正好符合棧只能在棧頂修改元素的特性。在這一個問題中,我們將所有的輸入數據分為兩類,字符和操作符。我們只定義了兩個操作符對字符進行操作,且都是對位于操作符之前的字符進行操作,所以結合棧的結構還是很好理解的。具體實現時,可以逐個檢測輸入的字符,如果是字符則存入棧內,如果是符號則彈出棧頂或是棧內全部元素。
4 綜合計算器
下面來想一個稍復雜一點的問題,如何用棧實現一個簡單的綜合計算器。這個綜合計算器有加減乘除四種操作,出于簡單考慮,操作數僅限于整數。我們想輸入一個表達式,然后讓綜合計算器來計算這個表達式,例如:計算 3+6*2-2。
第一眼看上去這個問題和行編輯程序問題很類似,輸入數據都可以分為兩類:操作數和操作符。但也有兩點區別:一,計算器的操作符通常是二元的,即操作符左右兩邊的數都是它的操作數;二,計算器的操作符有不同的優先級,如乘除的優先級高于加減。
結合之前的想法,我們將輸入數據分為操作數和操作符,依次讀取表達式的元素,如果為數值就壓入棧中;如果為操作符就開始對棧頂元素進行計算。那么首先將 3 入棧,然后檢測到加法操作符,將 3 出棧并讀取下一位數據 6,計算他們的和。但這時就會發現一個問題,原式中 6 應該屬于優先級更高的乘法操作符的操作數,直接將它給予加法操作符會導致計算順序產生錯誤。因此,我們還需要一些額外的信息來保存操作符的優先級,優先級最高的操作符應該是最早被執行的。而我們之前提到,棧的元素順序就可以表示優先級,越靠近棧頂優先級越高。因此可以用棧來保存操作符,只需要將優先級最高的操作符放在棧頂即可。
那么,該問題的解決思路就是:
4.1. 待入棧符號的優先級高于或等于棧中符號的優先級,則直接入棧。
4.2.若待入棧符號的優先級小于棧中符號的優先級,則將數字棧前兩位出棧,將當前符號棧中第一位出棧,運算后得到數字加入數字棧中,再加入符號。
注意: 上述第 4 步需要判斷符號的優先級,雖然經常說加減屬于同一優先級,但是加法滿足交換律和結合律,而減法不滿足,所以此處減法的優先級應當高于加法。例如:我們想計算 3-12+1 = -8,如果將加法和減法視為同樣優先級的操作,按照上面的操作可得下圖,那么最終計算出來的結果會是 3-(12+1) = -10,這顯示是錯誤的。乘除同理。
以下只有部分代碼,完整代碼見附錄。
注意: 上述代碼在只涉及整數(要求操作數必須是整數,操作的結果也必須是整數)時,可正常實現表達式的求值。但是在操作數為整數,操作結果不為整數時會和實際值有一些不同,這主要是因為C++的 “/” 符號代表的是求整數商。例如:計算 1+2*3/4 時,程序會先計算 3/4,整數商為 0,整個表達式結果為 1,不符合實際。 只要將操作數類型從整數類型擴展為浮點數類型就可解決這個問題。
5 后綴表達式 Reverse Polish
通過綜合計算器的實現,我們可以發現,對于計算機來說,即使是一個很簡單的表達式也需要很繁瑣的步驟來計算它。不過這主要是因為我們沒有把表達式翻譯為計算機可以理解的格式,在計算時需要用很多判斷語句來幫助其理解正確的運算順序。現在我們來介紹一種計算機更容易看懂的表達式——后綴表達式。
數學表達式可分為三種:前綴、中綴和后綴。我們一般使用的都是中綴表達式,即運算符在數字之間,這種表示方式人類很容易理解,但不利于計算機做運算。因此產生了前綴表達式(波蘭式)和后綴表達式(逆波蘭式),前和后分別表示二元運算符位于運算對象之前和之后,這兩種表達式僅依靠入棧,出棧兩種操作就可以完成中綴表達式的全部運算。
假如我們有表達式 (3+4)*5-6 ,那么其中綴、前綴及后綴表達式分別為:
- 中綴表達式: (3+4)*5-6
- 前綴表達式:- * + 3 4 5 6
- 后綴表達式:3 4 + 5 * 6 -
可以觀察到前綴和后綴表達式都不含括號,這是因為他們經過格式轉換后,符號的順序就代表了計算的順序,因此不再需要括號。我們上面實現的簡易綜合計算器就是一種中綴計算器,它其實更適合用樹來實現,而前綴和后綴計算器更適合用棧實現。
5.1 后綴表達式
又稱為逆波蘭式,比前綴表達式更加常用,二元運算符總是置于與之相關的兩個運算對象之后。例如,3 4 + 5 * 6 -,加號位于它的兩個運算對象之后,第一個數 3 是加號的左操作數,第二個數 4 是加號的右操作數。
給定一個后綴表達式,它的運算規則如下:
運算規則
上述過程簡潔易懂,可以看出后綴表達式在計算上的優勢。這也表示如果有已經“翻譯”好的表達式,計算機是可以進行快速又簡潔的運算的。因此,我們再來看一下如何將中綴表達式轉換為后綴表達式。
轉換規則
注意:上述方法的轉換結果有些問題,不符合從左向右的運算規則。 例如計算 “A+B*(C-D)-E/F” , 用上述方法得到的后綴表達式為 “ABCD-*EF/-+”,按規則計算該后綴表達式,得如下步驟:
其中前三步都沒有問題,但是到了第四步我們已經分別計算出了該表達式的三個部分:A、B*(C-D) 和 E/F,接下來按照從左向右的順序計算即可。但是按照上述規則,我們先計算了減法部分,后計算的加法部分。這樣做對答案不會產生影響,但是違反了從左向右計算的運算規則,所以得到的其實是錯誤的后綴表達式。(以后修改代碼)
部分實現代碼如下,完整代碼見附錄:
/* Global variable */ LinkedStack SN; // 數字棧 LinkedStack SO; // 符號棧 string op = "(+-*/)"; // 用來匹配符號,符號的位序越小代表其優先級越低/** Function: 判斷數字函數* Usage: bool b = isNumber(c);* ----------------------------* 判斷輸入的字符是否屬于數字,* 如是則返回true;否則返回false。*/ bool isNumber(char c);/** Function: 讀取函數* Usage: int read = readStr(str, i);* ---------------------------------* 用來讀取表達式位置i之后的數字或符號。* 如果讀取的是數字,則直接返回數字。* 如果讀取的是符號,將(,+,-,*,/,)分別以* 整數-6,-5,-4,-3,-2,-1返回。*/ int readStr(string str, int &i);void print();/** Function: 轉換函數* Usage: conversion(string str);* ------------------------------* 輸入一個中綴表達式,將其轉換為后綴表達式,* 存入SO中并輸出后綴表達式。*/ void conversion(string str){int i=0; // 用來遍歷中綴表達式int read=0; // 用來保存讀取到的值int res=0; // 遍歷表達式while (i<str.length()){read = readStr(str, i);if (read>=0){ // 如果讀取的是數字Push(SN, read); // 將數字壓入數字棧中 } else { // 如果讀取的是符號if (read==-6){ // 1.如果是左括號,則直接壓入符號棧Push(SO, read);}else if (read==-1){ // 2.如果是右括號,則將棧頂符號彈出并while (GetTop(SO)!=-6){ // 壓入數字棧,直到遇見左括號 Push(SN, Pop(SO));}Pop(SO); // 將左括號彈出 } else if (StackEmpty(SO) || GetTop(SO)<read){// 3.如果符號棧為空或待入棧符號的Push(SO, read); // 優先級更高,直接壓入符號棧}else { // 4.如果符號棧非空且棧頂元素優先級更高,while (!StackEmpty(SO) && read< GetTop(SO)){ // 則將棧頂符號彈出并壓入數字棧,Push(SN, Pop(SO)); // 直到棧頂元素優先級低于待入棧符號 }Push(SO, read); // 將待入棧符號壓入符號棧} }}// 將符號棧清空while (!StackEmpty(SO)){Push(SN, Pop(SO));}// 將數字棧中的元素倒序,存入符號棧中while (!StackEmpty(SN)){Push(SO, Pop(SN));}// 輸出print(); }5.2 前綴表達式
又稱為波蘭式,前綴表達式和后綴表達式的作用一樣,都是為了方便計算機計算。表達式 (3+4)*5-6 的前綴表達式為 - * + 3 4 5 6。前綴表達式的運算規則和后綴表達式很類似,唯一的區別就是遍歷方向不同。
運算規則
轉換規則
轉換規則和后綴表達式類似,只是要注意從右到左掃描中綴表達式,且左右括號的優先級互換,剩下的操作和后綴表達式一致。詳情見后綴表達式 。
相關章節
第一節 【緒論】數據結構的基本概念
第二節 【緒論】算法和算法評價
第三節 【線性表】線性表概述
第四節 【線性表】線性表的順序表示和實現
第五節 【線性表】線性表的鏈式表示和實現
第六節 【線性表】雙向鏈表、循環鏈表和靜態鏈表
第七節 【棧和隊列】棧
第八節 【棧和隊列】棧的應用
第九節 【棧和隊列】棧和遞歸
第十節 【棧和隊列】隊列
附錄
綜合計算器的實現
/** Filename: Counter.cpp* -----------------------* 用棧實現綜合計算器。*/#include <iostream> #include <string> #include <stack> using namespace std;/* Prototype */ bool isNumber(char c); int readStr(string str, int &i); int caculate(int num1, int num2, int op); int counter(string str);/* Global variable */ stack<int> SN; // 數字棧 stack<int> SO; // 符號棧,用數字-4,-3,-2,-1來表示加,減,乘,除 string op = "+-*/"; // 記錄四種操作符/* Main program */ int main(){string str = "4/2-6/2+1*3";int res = counter(str);printf("The result is %d.\n", res);return 0; }/** Function: 判斷數字函數* Usage: bool b = isNumber(c);* ----------------------------* 判斷輸入的字符是否屬于數字,* 如是則返回true;否則返回false。*/ bool isNumber(char c){return (48<=c && c<=57); }/** Function: 讀取函數* Usage: int read = readStr(str, i);* ---------------------------------* 用來讀取表達式位置i之后的數字或符號。* 如果讀取的是數字,則直接返回數字。* 如果讀取的是符號,將加減乘除分別以整數-4,-3,-2,-1返回。*/ int readStr(string str, int &i){string num; // 用來讀取數字int n = 0; // 用來記錄多位數在字符串中的下標if (isNumber(str[i])){ // 如果讀取到的是數字while (i+n<str.length() && isNumber(str[i+n])){num[n] = str[i+n]; // 將讀取到的連續數字存入num中n++;}i = i+n; // 更新i的值return stoi(num);} return op.find(str[i++])-4; // 如果讀取到的是操作符,則直接轉換為數字返回 }/** Function: 計算函數* Usage: int res = caculate(num1, num2, op);* ------------------------------------------* 用來計算每一步計算的結果并返回。*/ int caculate(int num1, int num2, int op){int res=0;switch(op){case -4: // 加法res = num1+num2;break;case -3: // 減法res = num1-num2;break;case -2: // 乘法res = num1*num2;break;case -1: // 除法res = num1/num2;break;}return res; }/** Function: 綜合計算器* ----------------------------* 可以處理加減乘除在整數范圍內的運算。* 不包括括號。*/ int counter(string str){int i = 0; // 用來遍歷表達式字符串int read = 0; // 用來保存讀取到的值int res = 0; // 用來保存計算的結果int num1, num2; // 用來保存從數字棧中彈出的值int op; // 保存從符號棧中彈出的符號// 遍歷字符串 while (i<str.length()){read = readStr(str, i);if (read>=0){ // 如果讀取的是數字SN.push(read); // 將數字壓入棧中 } else { // 如果讀取的是符號if (SO.empty() || SO.top()<read){ // 如果符號棧為空或待入棧符號的SO.push(read); // 優先級更高,直接壓入 } else { // 否則,先運算再壓入while (!SO.empty() && read< SO.top()){num2 = SN.top(); SN.pop();num1 = SN.top(); SN.pop();op = SO.top(); SO.pop();res = caculate(num1, num2, op);SN.push(res); // 將新計算出來的值壓入棧中}SO.push(read);}}}// 清空棧while (!SO.empty()){num2 = SN.top(); SN.pop();num1 = SN.top(); SN.pop();op = SO.top(); SO.pop();res = caculate(num1, num2, op);SN.push(res); // 將新計算出來的值壓入棧中}return SN.top(); }后綴表達式
鏈棧定義及實現
/** Filename: LinkedStack.h* -----------------------* 使用單鏈表來實現鏈棧*/#ifndef _SINGLE_LINKED_LIST_h_ #define _SINGLE_LINKED_LIST_h_#include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std;/********** 鏈棧的結點的類型定義 **********/ typedef int ElemType; // typedef char ElemType; typedef struct LNode{ElemType data; // 數據域struct LNode *next; // 指針域 } LNode, *LinkedStack;/********** 主要操作的實現 **********/ /** Function: 初始化操作* ----------------------------* 初始化一個空棧*/ void InitStack(LinkedStack &S){S = new LNode;S->next = NULL; }/** Function: 判空操作* ----------------------------* 判斷棧S是否為空,若為空則返回true,否則返回false。*/ bool StackEmpty(LinkedStack S){return !S->next; }/** Function: 讀取棧頂元素操作* ----------------------------* 返回棧頂元素。*/ ElemType GetTop(LinkedStack S){return S->next->data; }/** Function: 入棧操作* ----------------------------* 使用頭插法將e插入,使之成為新棧頂。*/ void Push(LinkedStack &S, ElemType e){LNode *n = new LNode;n->data = e;n->next = S->next;S->next = n; }/** Function: 出棧操作* ----------------------------* 彈出棧頂元素并返回。*/ ElemType Pop(LinkedStack &S){LinkedStack q;ElemType e;q = S->next;e = q->data;S->next = q->next;delete q;return e; }/** Function: 輸出操作* ----------------------------* 由于單鏈表的特性,更方便按從棧尾到棧頭的順序輸出。* 后續可以用遞歸法來正向輸出。*/ void Print(LinkedStack S){LinkedStack tmp=S;while (tmp->next!=NULL){tmp = tmp->next;// printf("%d <- ", tmp->data);printf("%d ", tmp->data);}// printf("base\n");printf("\n"); }#endif // _SINGLE_LINKED_LIST_h_后綴表達式轉換及運算的實現
/** Filename: ReversePolish.cpp* -----------------------* 用棧實現中綴表達式向后綴表達式轉換* 以及計算后綴表達式的程序。*/#include <iostream> #include <string> #include "../LinkedStack/LinkedStack.h" using namespace std;/* Global variable */ LinkedStack SN; // 數字棧 LinkedStack SO; // 符號棧 string op = "(+-*/)"; // 用來匹配符號,符號的位序越小代表其優先級越低/* Prototype */ bool isNumber(char c); int readStr(string str, int &i); void print(); void conversion(string str); int caculate(int num1, int num2, int op); int calculateReverse();/* Main program */ int main(){InitStack(SN);InitStack(SO);int res=0;string str = "1+2/2*(8-4)"; // "(3+4)*5-6"printf("Reverse polish is: ");conversion(str);res = calculateReverse();printf("The result is: %d\n", res);return 0; }/** Function: 判斷數字函數* Usage: bool b = isNumber(c);* ----------------------------* 判斷輸入的字符是否屬于數字,* 如是則返回true;否則返回false。*/ bool isNumber(char c){return (48<=c && c<=57); }/** Function: 讀取函數* Usage: int read = readStr(str, i);* ---------------------------------* 用來讀取表達式位置i之后的數字或符號。* 如果讀取的是數字,則直接返回數字。* 如果讀取的是符號,將(,+,-,*,/,)分別以* 整數-6,-5,-4,-3,-2,-1返回。*/ int readStr(string str, int &i){string num; // 用來讀取數字int n = 0; // 用來記錄多位數在字符串中的下標if (isNumber(str[i])){ // 如果讀取到的是數字while (i+n<str.length() && isNumber(str[i+n])){num[n] = str[i+n]; // 將讀取到的連續數字存入num中n++;}i = i+n; // 更新i的值return stoi(num);} return op.find(str[i++])-6; // 如果讀取到的是操作符,則直接轉換為數字返回 }void print(){LinkedStack tmp=SO;while (tmp->next!=NULL){tmp = tmp->next;if (tmp->data>=0){printf("%d ", tmp->data);} else {switch(tmp->data){case -5:printf("+ ");break;case -4:printf("- ");break;case -3:printf("* ");break;case -2:printf("/ ");break;}} }printf("\n"); }/** Function: 轉換函數* Usage: conversion(string str);* ------------------------------* 輸入一個中綴表達式,將其轉換為后綴表達式,* 存入SO中并輸出后綴表達式。*/ void conversion(string str){int i=0; // 用來遍歷中綴表達式int read=0; // 用來保存讀取到的值int res=0; // 遍歷表達式while (i<str.length()){read = readStr(str, i);if (read>=0){ // 如果讀取的是數字Push(SN, read); // 將數字壓入數字棧中 } else { // 如果讀取的是符號if (read==-6){ // 1.如果是左括號,則直接壓入符號棧Push(SO, read);}else if (read==-1){ // 2.如果是右括號,則將棧頂符號彈出并while (GetTop(SO)!=-6){ // 壓入數字棧,直到遇見左括號 Push(SN, Pop(SO));}Pop(SO); // 將左括號彈出 } else if (StackEmpty(SO) || GetTop(SO)<read){// 3.如果符號棧為空或待入棧符號的Push(SO, read); // 優先級更高,直接壓入符號棧}else { // 4.如果符號棧非空且棧頂元素優先級更高,while (!StackEmpty(SO) && read< GetTop(SO)){ // 則將棧頂符號彈出并壓入數字棧,Push(SN, Pop(SO)); // 直到棧頂元素優先級低于待入棧符號 }Push(SO, read); // 將待入棧符號壓入符號棧} }}// 將符號棧清空while (!StackEmpty(SO)){Push(SN, Pop(SO));}// 將數字棧中的元素倒序,存入符號棧中while (!StackEmpty(SN)){Push(SO, Pop(SN));}// 輸出print(); }/** Function: 計算函數* Usage: int res = caculate(num1, num2, op);* ------------------------------------------* 用來計算運算結果并返回。*/ int caculate(int num1, int num2, int op){int res;switch(op){case -5: // 加法res = num1+num2;break;case -4: // 減法res = num1-num2;break;case -3: // 乘法res = num1*num2;break;case -2: // 除法res = num1/num2;break;}return res; }/** Function: 計算后綴表達式函數* Usage: int res = calculateReverse(LinkedStack S);* -------------------------------------------------* 后綴表達式保存在棧S中,輸入棧S,返回該表達式的值。*/ int calculateReverse(){int num1, num2, res=0;// 遍歷后綴表達式while (!StackEmpty(SO)){if (GetTop(SO)>=0){Push(SN, Pop(SO));} else {num2 = Pop(SN);num1 = Pop(SN);res = caculate(num1, num2, Pop(SO));Push(SN, res);}} return GetTop(SN); }總結
以上是生活随笔為你收集整理的八、【栈和队列】栈的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 十、【栈和队列】队列
- 下一篇: 九、【栈和队列】栈和递归