數據結構輔導---棧和隊列

數據結構輔導---棧和隊列(2)
  3. 把中綴表達式轉換爲後綴表達式的算法
  設以’@’字符作爲結束符的中綴算術表達式已經保存在s1字符串中,轉換後得到的後綴算術表達式擬存於s2字符串中。由中綴表達式轉換爲後綴表達式的規則可知:轉換前後,表達式中的數值項的次序不變,而運算符的次序發生了變化,由處在兩個運算對象的中間變爲處在兩個運算對象的後面,同時去掉了所有的括號。爲了使轉換正確,必須設定一個運算符棧,並在棧底放入一個特殊算符,假定爲’@’字符,讓它具有最低的運算符優先級,假定爲數值0,此棧用來保存掃描中綴表達式得到的暫不能放入後綴表達式中的運算符,待它的兩個運算對象都放入到後綴表達式以後,再令其出棧並寫入到後綴表達式中。
  把中綴表達式轉換爲後綴表達式算法的基本思路是從頭到尾地掃描中綴表達式中的每個字符,對於不同類型的字符按不情況進行處理。若遇到的是空格則認爲是分隔符,不需要進行處理;若遇到的是數字或小數點,則直接寫入到s2中,並在每個數值的最後寫入一個空格;若遇到的是左括號,則應把它壓入到運算符棧中,待以它開始的括號內的表達式轉換完畢後再出棧;若遇到的是右括號,則表明括號內的中綴表達式已經掃描完畢,把從棧底直到保存着的對應左括號之間的運算符依次退棧並寫入s2串中;若遇到的是運算符,當該運算符的優先級大於棧頂運算符的優先級(加減運算符的優先級設定爲1,乘除運算符的優先級設定爲2,在棧中保存的特殊運算符’@’和’(’的優先級設定爲0)時,表明該運算符的後一個運算對象還沒有被掃描並放入到s2串中,應把它暫存於運算符棧中,待它的後一個運算對象從s1串中讀出並寫入到s2串中後,再另其出棧並寫入s2串中;若遇到的運算符的優先級小於等於棧頂運算符的優先級,這表明棧頂運算符的兩個運算對象已經被保存到s2串中,應將棧頂運算符退棧並寫入到s2串中,對於新的棧頂運算符仍繼續進行比較和處理,直到被處理的運算符的優先級大於棧頂運算符的優先級爲止,然後另該運算符進棧即可。
  按照以上過程掃描到中綴表達式結束符’@’時,把棧中剩餘的運算符依次退棧並寫入到後綴表達式中,再向s2寫入表達式結束符’@’和字符串結束符’/0’,整個轉換過程就處理完畢,在s2中就得到了轉換成的後綴表達式。
  例如,設中綴算術表達式s1爲:10+(18+9*3)/15-6@,使用的運算符棧用R表示,則轉換過程如下:
  (1)開始時存放後綴表達式的字符串s2爲空,R中壓入有’@’算符,它具有最低的優先級0:
  
  
  @
  
  (2)當掃描到s1中的左括號時,s2和R中的數據變化如下:
  1 0
  @ + (
  (3)當掃描到s1中的數值3時,s2和R中的數據變化爲:
  1 0 1 8 9 3
  @ + ( + *
  (4)當掃描到s1中的右括號時,s2和R變爲:
  1 0 1 8 9 3 * +
  @ +
  (5)當掃描到s1中的數值15時,s2和R又變爲:
  1 0 1 8 9 3 * + 1 5
  @ + /
  (6)當掃描到s1中的’@’字符時,s2和R爲:
  1 0 1 8 9 3 * + 1 5 / + 6
  @ -
  1 0 1 8 9 3 * + 1 5 / + 6 - @
  (7)當整個處理過程結束後,R棧爲空,s2爲:
  將中綴算術表達式轉換爲後綴算術表達式的算法描述如下:
  void Change(char* s1, char* s2)
  // 將字符串s1中的中綴表達式轉換爲存於字符串s2中的後綴表達式
  {
  Stack R; // 定義用於暫存運算符的棧
  InitStack(R); // 初始化棧
  Push(R,'@'); // 給棧底放入'@'字符,它具有最低優先級0
  int i,j;
  i=0; // 用於指示掃描s1串中字符的位置,初值爲0
  j=0; // 用於指示s2串中待存字符的位置,初值爲0
  char ch=s1[i]; // ch保存s1串中掃描到的字符,初值爲第一個字符
  while(ch!='@')
  { // 順序處理中綴表達式中的每個字符
  if(ch==' ')
  // 對於空格字符不做任何處理,順序讀取下一個字符
  ch=s1[++i];
  else if(ch=='(')
  { // 對於左括號,直接進棧
  Push(R,ch);
  ch=s1[++i];
  }
  else if(ch==')')
  { // 對於右括號,使括號內的仍停留在棧中的運算符依次
  // 出棧並寫入到s2中
  while(Peek(R)!='(')
  s2[j++]=Pop(R);
  Pop(R); // 刪除棧頂的左括號
  ch=s1[++i];
  }
  else if(ch=='+'||ch=='-'||ch=='*'||ch=='/')
  { // 對於四則運算符,使暫存在棧中的不低於ch優先級
  // 的運算符依次出棧並寫入到s2中
  char w=Peek(R);
  while(Precedence(w)>=Precedence(ch))
  { // Precedence(w)函數返回運算符形參的優先級
  s2[j++]=w;
  Pop(R); w=Peek(R);
  }
  Push(R,ch); // 把ch運算符寫入棧中
  ch=s1[++i];
  }
  else
  { // 此處必然爲數字或小數點字符
  while(isdigit(ch)||ch=='.')
  { // 把一個數值中的每一位依次寫入到s2串中
  s2[j++]=ch;
  ch=s1[++i];
  }
  s2[j++]=' '; // 被轉換後的每個數值後放入一個空格
  }
  }
  // 把暫存在棧中的運算符依次出棧並寫入到s2串中
  ch=Pop(R);
  while(ch!='@') {
  if(ch=='(') {
  cerr<<"expression error!"<  exit(1);
  }
  else {
  s2[j++]=ch;
  ch=Pop(R);
  }
  }
  // 在後綴表達式的末尾放入表達式結束符和字符串結束符
  s2[j++]='@';
  s2[j++]='/0';
  }
  求運算符優先級的Precedence函數爲:
  int Precedence(char op)
  // 返回運算符op所對應的優先級數值
  {
  switch(op)
  {
  case '+':
  case '-':
  return 1; // 定義加減運算的優先級爲1
  case '*':
  case '/':
  return 2; // 定義乘除運算的優先級爲2
  case '(':
  case '@':
  default:
  return 0; // 定義在棧中的左括號和棧底字符的優先級爲0
  }
  }
  在轉換算法中,中綴算術表達式中的 每個字符均需要掃描一遍,對於掃描到的每個運算符,最多需要進行入棧、出棧和寫入後綴表達式這三次操作,對於掃描到的數字或小數點,只需要把它直接寫入到後綴表達式即可。所以,此算法的時間複雜度爲O(n),n爲後綴表達式中字符的個數。該算法需要使用一個運算符棧,需要的深度不會超過中綴表達式中運算符的個數,所以此算法的空間複雜度至多也爲O(n)。
  利用表達式的後綴表示和堆棧技術只需要兩遍掃描就可完成中綴算術表達式的計算,顯然比直接進行中綴算術表達式計算的掃描次數要少得多。
  在上述討論的中綴算術表達式求值的兩個算法中,把中綴表示轉換爲後綴表示的算法需要使用一個字符棧,而進行後綴表達式求值的算法又需要使用一個浮點數棧,這兩個棧的元素類型不同,所以棧的類型無法作爲全局量來定義,棧運算的函數也無法適應這種要求。爲了解決這個問題,必須把Stack棧類型定義爲模板類,把棧運算的函數定義爲該類的公用成員函數,通過調用成員函數來實現棧的運算。這裏對此不作深入討論,留給讀者作爲練習。
  假定採用類模板來定義Stack類和編寫計算中綴算術表達式值的程序,若執行下面的主程序:
  void main()
  {
  char a[30];
  char b[30];
  cout<<"請輸入一個以'@'字符結束的中綴算術表達式:"<  cin.getline(a,sizeof(a)); // 從鍵盤上輸入一行表示中綴算術表達
  // 式的字符串存入到字符數組a中
  Change(a,b);
  cout<<"對應的後綴算術表達式爲:"<  cout<  cout<<"求值結果爲:"<  }
  則得到的顯示結果如下:
  請輸入一個以'@'字符結束的中綴算術表達式:
  12+(3*(20/4)-8)*6@
  對應的後綴算術表達式爲:
  12 3 20 4 /*8 -6 *+@
  求值結果爲:54
  三、隊列
  
  1. 隊列的定義
  隊列(Queue)簡稱隊,它也是一種運算受限的線性表,其限制是僅允許在表的一端進行插入,而在表的另一端進行刪除。我們把進行插入的一端稱作隊尾(rear),進行刪除的一端稱作隊首(front)。向隊列中插入新元素稱爲進隊或入隊,新元素進隊後就成爲新的隊尾元素;從隊列中刪除元素稱爲離隊或出隊,元素離隊後,其後繼元素就成爲隊首元素。由於隊列的插入和刪除操作分別是在各自的一端進行的,每個元素必然按照進入的次序離隊,所以又把隊列稱爲先進先出表(First In First Out, 簡稱FI
  FO)。
  在日常生活中,人們爲購物或等車時所排的隊就是一個隊列,新來購物或等車的人接到隊尾(即進隊),站在隊首的人購到物品或上車後離開(即出隊),當最後一人離隊後,則隊列爲空。
  例如,假定有a,b,c,d四個元素依次進隊,則得到的隊列爲(a,b,c,d),其中字符a爲隊首元素,字符d爲隊尾元素。若從此隊中刪除一個元素,則字符a出隊,字符b成爲新的隊首元素,此隊列變爲(b,c,d);若接着向該隊列插入一個字符e,則e成爲新的隊尾元素,此隊列變爲(b,c,d,e);若接着做三次刪除操作,則隊列變爲(e),此時只有一個元素e,它既是隊首元素又是隊尾元素,當它被刪除後隊列變爲空。
  2. 隊列的存儲結構
  隊列的存儲結構同線性表和棧一樣,既可以採用順序結構,也可以採用鏈接結構。
  (1) 隊列的順序存儲結構
  隊列的順序存儲結構需要使用一個數組和兩個整型變量來實現,利用數組來順序存儲隊列中的所有元素,利用兩個整型變量來分別存儲隊首元素和隊尾元素的下標位置,分別稱它們爲隊首指針和隊尾指針。假定存儲隊列的數組用queue[QueueMaxSize]表示,隊首和隊尾指針分別用front和rear表示,則元素類型爲ElemType的隊列的順序存儲類型可定義爲:
  ElemType queue[QueueMaxSize];
  int front, rear;
  其中QueueMaxSize爲一個整型全局常量,需事先通過const語句定義,由它確定順序隊列(即順序存儲的隊列)的最大長度,即最多能夠存儲的元素個數。當然,隊列的順序存儲空間也可以採用動態分配,此時用於決定最大長度的量可以爲全局常量,也可以爲全局或局部變量。如在一個函數的函數體中使用下面語句能夠爲一個隊列分配長度爲n的數組空間,該數組名仍用queue表示。
  ElemType* queue=new ElemType[n];
  隊列的順序存儲類型同樣可以用一個記錄類型來表示,假定記錄類型名爲Queue,則該類型定義爲:
  struct Queue {
  ElemType queue[QueueMaxSize];
  int front, rear;
  };
  假定一個隊列的當前狀態如圖4-10(a)所示,此時已經有a,b,c三個元素相繼出棧(爲了同隊列中的元素相區別,把它們分別括了起來),隊首指針front的值爲3,指向的隊首元素爲d,隊尾指針的值爲7,指向的隊尾元素爲h;若接着插入一個新元素i,則隊列的當前狀態如圖4-10(b)所示;若再接着刪除一個元素,則變爲圖4-10(c)所示。
  0 1 2 3 4 5 6 7 8 QueueMaxSize-1
  (a) (b) (c) d e f g h
  
  front rear
  (a)
  
  0 1 2 3 4 5 6 7 8 QueueMaxSize-1
  (a) (b) (c) d e f g h i
  front rear
  (b)
  0 1 2 3 4 5 6 7 8 QueueMaxSize-1
  (a) (b) (c) (d) e f g h i
  front rear
  (c)
  圖4-10 順序隊列的插入和刪除操作示意圖
  每次向隊列插入一個元素,需要首先使隊首指針後移一個位置,然後再向這個位置寫入新元素。當隊尾指針指向數組空間的最後一個位置QueueMaxSize-1時,若隊首元素的前面仍存在空閒的位置,則表明隊列未佔滿整個數組空間,下一個存儲位置應是下標爲0的空閒位置,因此,首先要使隊尾指針指向下標爲0的位置,然後再向該位置寫入新元素。通過語句rear=(rear+1)%QueueMaxSize可使存儲隊列的整個數組空間變爲首尾相接的一個環(稱此爲循環隊列),當rear指向最後一個存儲位置時,下一個所求的位置自動爲數組空間的開始位置(即下標爲0的位置)。
  同對線性表和棧的插入一樣,每次在進行隊列插入前,也要判斷隊列是否已滿(即數組空間是否已被用完),若是則停止插入,終止程序運行,否則可向隊列中插入新元素。若隊尾指針的下一個位置(採用(rear+1)%QueueMaxSize計算出來)恰是隊首指針front所指的位置,則表明隊列已滿,可知判斷隊滿的條件是(rear+1)%QueueMaxSize==front。從另一方面看,若隊首指針和隊尾指針已經指向了同一個位置,則表明隊列中只有一個元素,當刪除該元素後,隊列爲空,隊尾指針不變,而隊首指針指向了下一個位置,此時隊尾指針的下一個位置正好也是隊首指針所指的位置,由此可知,當條件(rear+1)%QueueMaxSize==front成立時,可能是隊滿的情況,也可能是隊空的情況。爲了區別這兩種情況,可設置一個標記,當進行插入操作後,置該標記爲1,當進行刪除操作後,置該標記爲0。在標記爲1的情況下,上述條件成立則表明隊列已滿,在標記爲0的情況下,上述條件成立則表明隊列爲空。爲了省去設置一個標記的麻煩,通常採用的處理方法是:讓front指針不是指向隊首元素的位置,而是指向它的前一個位置,當上述條件成立時隊列必然爲滿,當隊首指針等於隊尾指針時隊列爲空,此時整個數組空間只能利用QueueMaxSize-1個存儲位置,而不是QueueMaxSize個存儲位置。
  在順序隊列中進行插入和刪除時,不需要比較和移動任何元素,只需要修改隊尾和隊首指針,並向隊尾寫入元素或從隊首取出元素,所以其時間複雜性爲O(1)。
  (2) 隊列的鏈接存儲結構
  隊列的鏈接存儲結構也是通過由結點構成的單鏈表實現的,此時只允許在單鏈表的表頭進行刪除和在單鏈表的表尾進行插入,因此它需要使用兩個指針:隊首指針front和隊尾指針rear。用front指向隊首(即表頭)結點的存儲位置,用rear指向隊尾(即表尾)結點的存儲位置。用於存儲隊列的單鏈表簡稱鏈接隊列或鏈隊。假定鏈隊中的結點類型仍採用第二章定義的LNode結點類型,那麼隊首和隊尾指針爲LNode*指針類型。若把一個鏈隊的隊首指針和隊尾指針定義在一個記錄類型中,並假定該記錄類型用標識符LinkQueue表示,則定義如下:
  struct LinkQueue {
  LNode* front;
  LNode* rear;
  };
  其中LNode結點類型重寫如下:
  struct LNode {
  ElemType data;
  LNode* next;
  };
  
  
  圖4-12 鏈隊的插入和刪除操作示意圖
  對鏈隊的插入和刪除操作同樣不需要比較和移動元素,只需要修改個別相關指針和進行結點的動態分配或回收操作,所以其時間複雜度爲O(1)。另外,使用鏈隊不存在隊滿的問題,因爲它使用的結點是動態分配的,只要內存中動態存儲區仍有可用空間,就可以得到一個新結點,使之插入到鏈隊中;鏈隊也可能爲空,此時front和rear指針均爲空。

本文轉自
http://sysop.com.cn/system4864,1.html

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章