Transcript Chap.3

第三章—
堆疊與佇列
1
3.2
堆疊之抽象資料型態
堆疊是一種有序串列,其插入和刪除僅在串列的一端進行,稱此為頂端(top)。給
予一個堆疊 s  ( a 0 ,..., a n 1 ) , a n 1 為底端元素 a 0 為頂端元素,且元素 a i
在元素 a i 1 之上,0<i<n。
下圖說明這一連串的運算。
A ¬ top
推入
C ¬ top
B ¬ top
B
A
A
推入
推入
D ¬ top
C
B
A
推入
E ¬ top
D
C
B
A
推入
D ¬ top
C
B
A
彈出
圖:在一個堆疊中插入和刪除元素
2
因為最後插入堆疊的元素為第一個被刪除的元素,所以堆疊又稱後進先出(Last-InFirst-Out,LIFO)串列。
假設有一個主函數,他呼叫函數a1。
因為所有的函數同時儲存在系統堆疊中,如果一個函數呼叫其自身時並沒有任何
的差別。
然而,遞迴呼叫會消耗大部分配置給系統堆疊的記憶體;他可能消耗整個可用的
系統空間。
3
實作最簡單的方法是使用一維陣列,堆疊第一個,或底端的,元素儲存於stack[0],
第二個在stack[1],而第i個在stack[i-1]。
與此陣列相關的變數為top,他指向堆疊頂端的元素。起初,top設定為-1,表示一
個空的堆疊。以這種表示法,我們可用如上的方式來實做。
4
template < class T >
class Stack
{ // 一個含有零個或多個元素的有限序串列
public:
Stack (int stackCapacity = 10);
// 建立一個起始容量為stackCapacity的空堆疊
bool IsEmpty ( ) const;
// 如果堆疊裡的元素個數為0則回傳true,否則回傳false
T& Top ( ) const;
// 回傳堆疊的頂端元素
void Push (const T& item);
// 將item插入堆疊的頂端
void Pop ( );
// 刪除堆疊頂端的元素
};
5
程式:加入元素到堆疊中
template < class T >
void Stack < T >::Push (const T& x)
{ // 將x加入堆疊中
if (top = = capacity – 1)
{
ChangeSize1D (stack, capacity, 2 * capacity);
capacity *= 2;
}
stack [ ++top ] = x;
}
6
程式:自堆疊中刪除元素
template < class T >
void Stack < T >::Pop ( )
{ // 刪除堆疊的頂端元素
if (IsEmpty ( ) ) throw “Stack is empty. Cannot delete.”;
stack [ top-- ].~T(); // T的解構子
}
7
佇列之抽象資料型態
3.3
佇列是一種有序串列,其中所有的插入發生在串列的一端,所有的刪除發生在串
列的另一端。
下圖說明這一連串的事件。因為第一個加入佇列的元素也是第一個刪除的元素,
佇列又稱先進先出(First-In-First-Out,FIFO)串列。
圖:在佇列中插入和刪除元素
A
A
B
A
↑
↑
↑
f, r
f
r
add
B
C
A
↑
↑
f
r
D
A
↑
↑
f
r
add
f = queue front
B
C
add
B
C
E
B
↑
↑
↑
↑
f
r
f
r
add
D
r = queue rear
C
D
delete
E
結構:抽象資料型態queue
template < class T >
class Queue
{ // 一個含有零個或多個元素的有限序串列
public:
Queue (int queueCapacity = 0);
// 建立一個起始容量為stackCapacity的空佇列
bool IsEmpty ( ) const;
// 如果佇列中的元素個數為0則回傳true,否則回傳false
T& Front ( ) const;
// 回傳在佇列前端的元素
T& Rear ( ) const;
// 回傳在佇列後端的元素
void Push (const T& item);
// 將item插入到佇列的後端
void Pop ( );
// 將佇列的前端元素刪除
};
9
範例[job scheduling]:佇列的一個典型的例子是由作業系統所建立的工作佇列(job
queue)。如果作業系統未採用工作優先權,則工作依照他們進入系統的順序被處
理。
10
圖:在一個循序佇列中的插入和刪除
前端
末端
-1
-1
-1
-1
0
1
-1
0
1
2
2
2
Q[0]
J1
J1
J1
Q[1]
J2
J2
J2
Q[2]
J3
J3
J3
Q[3]
說明
空佇列
加入Job 1
加入Job 2
加入Job 3
刪除Job 1
刪除Job 2
將陣列移位是一種耗時的工作,特別是當它有很多的元素時。事實上,queue_full
最差狀況的複雜度為O(MAX_QUEUE_SIZE)。
11
如果將陣列queue[MAX_QUEUE_SIZE]看成一個環狀,則可以得到一個更有效率的
佇列表示法。
在這種表示法中,front和rear的初值設定為0而不是-1。 Front索引永遠以逆時針
方向指著佇列中第一個元素的前一個位置。
rear索引指向佇列目前的最後位置。若且為若front = rear,佇列是空的。
12
3.5
迷宮問題
要表示一個迷宮,最明顯的選擇是二維陣列,其中0代表可通行的路徑,1代表障礙。
下圖是一個簡單的迷宮。
入口
0
1
0
0
0
1
1
0
0
0
1
1
1
1
1
1
0
0
0
1
1
0
1
1
1
0
0
1
1
1
0
1
1
0
0
0
0
1
1
1
1
0
0
1
1
1
1
0
1
1
1
1
0
1
1
0
1
1
0
0
1
1
0
1
0
0
1
0
1
1
1
1
1
1
1
0
0
1
1
0
1
1
1
0
1
0
0
1
0
1
0
1
1
1
1
0
0
1
1
1
1
1
1
1
1
0
0
1
1
0
1
1
0
1
1
1
1
1
0
1
1
1
0
0
0
1
1
0
1
1
0
0
0
0
0
0
0
1
1
1
1
1
0
0
0
1
1
1
1
0
0
1
0
0
1
1
1
1
1
0
1
1
1
1
0
出口
13
以二維陣列來表示迷宮,則任何時候老鼠的位置為MAZE[row][col]。
必須注意的是,因為並非每一個位置都有八個相鄰的點。如果[row, col]在邊界上,
則有少於八個,甚致只有三個鄰點存在。
簡化這個問題的方法是在一個陣列move中預先定義八個可能的移動方向。
我們以數值0到7來代表這八個可能的移動方向。對於每一個方向,我們指出其
垂直和水平的位移。
14
圖:移動的表格
方向
方向數值
N
NE
E
SE
S
SW
W
NW
0
1
2
3
4
5
6
7
move[dir].vert
-1
-1
0
1
1
1
0
-1
move[dir].horiz
0
1
1
1
0
-1
-1
-1
我們假設move是根據圖所提供的資料來設定初值。這表示,如果目前位在
maze[row][col]而想要找到下一個走到的位置, maze[next_row][next_col],我們可以
設定:
15
next_row = row + move[dir].vert;
next_col = col + move[dir].horiz;
對於堆疊的大小,我們也需要決定一個合理的範圍。因為迷宮中的每一個位置只
能走過一次,堆疊所需之空間最多和迷宮中的0元素之個數一樣多。
圖:具有一條長路徑的簡易迷宮
0

1

1

0
1

1
1

0

1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1
1
1
1
0
0
0
0
1

0

1

1
1

0
1

1

0
16
在圖中,因為在一m*p的迷宮中最多有mp個元素不為0,設定堆疊的容量有mp
個元素就夠了。
程式:迷宮搜尋函數
void Path(const int m, const int p)
{ // 輸出迷宮的一個路徑(如果有的話); maze[0][i] = maze[m+1][i] =
// maze[j][0] = maze[j][p+1] = 1, 0  i  p+1, 0  j  m+1。
// 從 (1, 1) 開始
mark[1][1] = 1;
Stack<Items> stack(m*p);
Items temp(1, 1, E);
// 設定 temp.x、temp.y、與temp.dir
Stack.Push(temp);
while (!stack.IsEmpty( ))
{ // 堆疊不是空的
temp = stack.Top( );
stack.Pop( ); // 彈出
int i = temp.x; int j = temp.y; int d = temp.dir;
while (d < 8) // 往前移動
17
{
int g = i + move[d].a; int h = j + move[d].b;
if ((g = = m) && (h = = p)) { // 抵達出口
// 輸出路徑
cout << stack;
cout << i << " " << j << endl; // 路徑上的上兩個方塊
cout << m << " " << p << endl;
return;
}
if ((!maze [g ][h]) && (!mark [g ][h])) { // 新位置
mark[g][h] = 1;
temp.x = i; temp.y = j; temp.dir = d+1;
stack.Push(temp); // 加入堆疊
i = g; j = h; d = N; // 移到 (g, h)
else d++; // 試下一個方向
}
}
cout << "No path in maze." << endl;
}
HW3
18
3.6
運算式的計算
簡介
在任何程式語言中,都有一種用來決定計算運算符號先後順序的優先權等級。
在關連性一欄中指出具有相同優先權運算符號如何計算。
19
圖:C++語言優先權等級
優先權
運算子
1
負號, !
2
*, /, %
3
+, -
4
<, <=, >=, >
5
= =, !=
6
&&
7
||
20
計算後序運算式
運算式標準的寫法稱作中序運算式(infix notation),因為其中的二元運算符號
(binary operator)被寫在他的兩個運算元之間。
雖然中序法為編寫運算式最常用的方法,但它並不是編譯程式用來評估運算式的
方法。編譯程式通常使用一種沒有括號的表示法,稱為後序表示法(postfix
notation)。
這種表示法中,每一個運算符號出現在其運算元之後。
21
圖:中序和後序表示法
中序
後序
2+3*4
A*b+5
(1+2)*7
A*b/c
((a/(b-c+d))*(e-a)*c
a/b-c+d*e-a*c
2 3 4 *+
A b *5+
1 2 +7*
Ab*c/
Abc-d+/ea-*c*
Ab/c-de*+ac*-
要計算一個運算式,先由左而右掃描一次。在找到運算符號之前,先將運算元放入堆
疊中。接著從堆疊取出該運算符號所需的正確個數之運算元,進行運算,並將結果放
回堆疊中。
在到達運算式結尾前,以這種方式反覆處理。最後將結果自堆疊頂端取出。
22
圖:後序運算式計算
符號
[0]
6
2
/
3
4
2
*
+
6
6
6/2
6/2
6/2-3
6/2-3
6/2-3
6/2-3
6/2-3+4*2
堆疊
[1]
頂端
[2]
2
3
4
4
4*2
2
0
1
0
1
0
1
2
1
0
23
中序轉成後序
從一個中序運算式產生後序運算式的演算法說明如下:
(1) 將運算式完全的加上括號。
(2) 移動所有的二元運算符號。使他們取代其對應的右括號。
(3) 刪除所有的括號。
例如:a/b-c+d*e-a*c完全的加上括號以後變成:
((((a/b)-c)+(d*e))-a*c))
執行步驟2和3以後得到:
ab/c-de*+ac*24
雖然這個方法以手算時效果很好。但是對電腦是沒有效率的,因為它需要掃描兩
次。
範例 [Simple expression]:假設有一個簡易式a+b*c,他產生的後序運算式為
abc*+。如圖所示,運算元立即輸出,但兩個運算符號必須反置。
圖:將a+b*c轉換成後序運算式
符號
堆疊
[0]
[1]
a
+
b
*
c
eos
+
+
+
+
*
*
頂端
輸出
[2]
-1
0
0
1
1
-1
a
a
ab
ab
abc
abc*+
25
一般而言,具有較高優先權的運算符號必須在低優先權運算符號之前輸出。
在此例中,將運算符號自堆疊推出的動作只有在到達運算式結尾時才發生。在此
時,有兩個運算符號被推出。因為具有較高優先權的運算符號在堆疊的頂端,他
會先被推出。
範例[Parenthesized expression]:以運算式a*(b+c)*d為例,產生後序運算式
abc+*d*。
應注意,在到達右括號之前,將運算符號推入堆疊中。到達時,將運算符號自堆
疊推出,直到對應的左括號為止。而後將堆疊中的左括號刪除。(右括號絕不會存
入堆疊。)
此時中序運算式中只剩下*d。因為兩個乘號的優先權等級相同,一種處理方法是
在d之前輸出* ,另一種處理方法是將*推入堆疊,並在d之後輸出。
26
圖:將a*(b+c)*d
符號
[0]
a
*
(
b
+
c
)
*
d
eos
*
*
*
*
*
*
*
*
*
堆疊
[1]
(
(
(
(
頂端
輸出
[2]
+
+
-1
0
1
1
2
2
0
0
0
0
a
a
a
ab
ab
abc
abc+
abc+*
abc+*d
abc+*d*
27
我們有兩種優先權,一種是堆疊內優先權(in-stacck precedence,jsp),另一種是輸
入優先權(incoming precedence,icp)。建立這些優先權和堆疊的宣告如下:
precedence stack [MAX_STACK_SIZE];
static int isp[ ] = {0, 19, 12, 12, 13, 13, 13, 0};
static int icp [ ] = {20, 19, 12, 12, 13, 13, 13, 0};
程式:將中序運算式轉成後序運算的函數
void Postfix(Expression e)
{// 把中置運算式e轉成後置運算式並輸出。NextToken就跟函式Eval(程式3.18)
裡
// 的一樣。假設在e裡的最後一個符號是’#’。另外,’#’也用在堆疊的底部。
Stack<Token>stack; // 初始化stack
stack.Push(‘#’);
for (Token x = NextToken(e); x != ‘#’; x = NextToken(e))
28
if (x是一個運算元) cout << x;
else if (x = =’)’)
{// 從堆疊中彈出直到出現’(‘
for (;stack.Top( ) != ‘(‘; stack.Pop( ))
cout << stack.Top( );
stack.Pop( ); // 從堆疊中彈出 ’(‘
}
else { // x是一個運算子
for (; isp(stack.Top( )) <= icp(x); stack.Pop( ))
cout << stack.Top( );
stack.Push(x);
}
// 已經到了一個運算式的結尾;清空堆疊
for (; !stack.IsEmpty( ); cout << stack.Top( ), stack.Pop( ));
cout << endl;
}
HW4
29