Transcript Chap.1

第一章-
基本概念
1
1.1 概述:系統生命週期
(1). 需求:所有大的程式專案都以一些定義專業目的規格書開始。
(2). 分析:在此階段中,我們先將問題分割成數個容易管理的小問題。 分
析的方法有2種:由下而上與由上而下
(3). 設計:這個階段繼續在分析階段所完成的工作。 設計者從程式所需要
的資料物件和資料的運算兩方面來設計系統。
(4). 改良和編碼:在這個階段中,我們為每一個資料物件選擇表示法,並編
寫他的各種運算之演算法。 作這件事的順序是關鍵性的。
(5). 驗證:這個階段包括程式正確性的驗證,以不同的輸入資料測試程式,
並改正錯誤。
2
1.3 資料抽象化 (Data Abstraction)
C提供了兩種方法幫助我們將資料群集在一起。他們是陣列和結構。
1.
陣列是具有相同的基本資料型態的元素之集合。他們以隱含的方式定義,例
如,int list [ 5 ]。
2.
結構是元素的集合,其元素的資料型態不一定是相同的。他們以明顯的方式
定義。
3
例如:struct student {
char last_name;
int student_id;
char grade;
}
C語言也提供指標資料型態。對每一種基本資料型態都有一個對應的指標資料型態。
指標以放在變數名稱前面的星號,*,來表示。例如:int I, *pi;
定義:資料型態是物件(object)和可以在這個物件之上作用的一組運算(operations)
之集合。
不論程式處理預設的資料型態或使用者自定資料型態,都必須考慮兩個觀點:物件
和運算。
4
定義:一個抽象資料型態(abstract data type, ADT)是一種資料型態,他的組織方式使的物
件的規格與物件上的運算之規格和該物件的內部表示法與運算的實作法是獨立的。
有些程式語言有明確的方法來支援定義和實作之間的差別。舉例而言,Ada中有封包
(package)的觀念,C++中有類別(Class)的觀念。
可以將一種資料型態的函數分成數種類型
:
(1) 產生/建構:這種函數對指定的型態產生一個新的實體(instance)。
(2) 轉換:這種函數也用來產生指定的型態之實體,通常是利用一個或多個其他的
實體來產生。
(3) 觀察/彙報:這種函數提供有關指定型態的一種實體之資訊,但不會改變實體
一般而言,一個ADT的定義中至少會包含三個函數,他們分別屬於這三類函數中個別的類
型。
5
structure Natural_number is
object: : an ordered subrange of the integers starting at zero and ending at the maximum integer
( INT_MAX ) on the computer
functions :
for all x , y
Nat_Number , TRUE , FALSE
Boolean

and where + , - ,< , and == are the usual integer operations
Nat_No Zero ( )
::= 0
Boolean Is_Zero ( x )
::= if ( x ) return FALSE
else return TRUE
Nat_No Add ( x , y )
::=
if ((x+y)<=INT_MAX)return x + y
else return INT_MAX
Boolean Equal (x,y)
::=
if (x==y) return TURE
else return FALSE
Nat _No Successor(x)
::=
if (x==INT_MAX) return x
else return x + 1
6
Nat_No Subtract ( x , y )
::=
if (x < y) return 0
else return x – y
end Natural_Number
結構的定義以結構名稱和其名稱縮寫為首。在定義中有兩大部分:物件和函數。
此例中物件以整數來定義。而函數的定義則較複雜;首先,定義中使用X和Y代表
Natural_Number集合中的兩個元素,TRUE和FALSE則為Boolean值集合中的元素。
定義中還使用了由整數集合所定義的一些函數,即:+, - ,=, <。
對每一個函數,其結果之型態放在函數名稱的右邊,其定義放在右邊。符號“::=”應讀
為“定義為”。
第一個函數Zero沒有參數,傳回自然數0。
如果在數序中已經沒有下一個數,即x值已經是INT-MAX,則定義為Successor傳回INTMAX,有些人會較喜歡Successor傳回一個錯誤旗號,這也是很好的。
7
1.5 演算法定義
簡介
定義:演算法是一組有限的指令,根據這些指令,可以完成一項特定工
作。 此外,所有的演算法必須符合下列的條件:
(1)
輸入:可以沒有或從外部提供多個資料。
(2)
輸出:至少產生一個結果。
(3)
明確的:每一個指令都每明白且沒有混淆不清。
(4)
有限的:如果追蹤一個演算法的指令,則在所有的情況下,演算法會
在有限的步驟之後停止。
(5)
有效率的:每個指令即使如(3)所規定的一般明確仍是不夠的;他必須
是個可行的運算。
在計算理論中,演算法和程式的差別之一是程式可以不符合第4個條件。
8
例如,作業系統可以看成是一個等待迴路,直到有更多的工作進到系統中。
我們可以有許多的方法來描述一個演算法,如英文;或是使用通稱為流程圖
的圖形表示法。
在本書中,多數的演算法將以C語言來表示,偶爾會採用英文和C語言的混
合表示法。
範例
[Selection sort]﹔假設我們要發展一個可以將一組n個整數排序的程
式,n>=1。 下列是簡單的解法:
From those integers that are currently unsorted, find the smallest and place it next in the
sorted list.
這並不算是個演算法,因為它還留有數個問題沒有回答﹔
(1)
沒有告知原始的整數儲存在何處。
(2)
以何種方式儲存。
(3)
結果應儲存在何處。
9
void SelectionSort ( int *a, const int n)
{ //把a[0]至a[n-1]總共n個數以遞增的順序排列
for (int i = 0 ; i < n ; i ++)
{
int j = i ;
//找出a[i]到a[n-1]中最小的一個整數
for (int k = i + 1 ; k < n ; k++)
if (a[k] < a[j]) j = k;
swap(a[i], a[j]) ;
}
}
10
int BinarySearch (int *a, const int x, const int n)
{ // 在排序好的陣列a[0], …, a[n-1]中找出x
初始化left和right ;
while (還有元素)
{
令middle為中間的元素 ;
if (x < a[middle]) 把right設定成middle-1 ;
else if (x > a[middle]) 把left設定成middle+1 ;
else return middle ;
}
沒找到 ;
}
11
int BinarySearch (int *a, const int x, const int n)
{ // 在排序好的陣列a[0], …, a[n-1]中找出x
int left = 0, right = n-1 ;
while (left <= right)
{ // 還有元素
int middle = (left + right)/2;
if (x < a[middle]) right=middle-1 ;
else if (x > a[middle]) left = middle+1 ;
else return middle ;
} // while迴圈結束
return -1; // 沒找到
}
12
* 這個搜尋策略叫做二分搜尋法。
* 函數是用來將一個大程式分割成容易管理的小程式的主要工具。它使的
程式容易閱讀,並且因為函數可以個別的測試,而增加了程式正確執行的機
率。
遞迴演算法
* 函數是一種可被其他的函數所引用的程式單元。同時函數可以呼叫自身(直接遞
迴, direct recursion), 或者, 函數可以再度引用其呼叫函數(間接遞迴 indirect
recursion) 。
13
二分搜尋法的迴路版本。要將此函數轉成遞迴函數, 我們必須:
(1). 建立終止遞回呼叫的臨界條件。
(2). 建立遞迴呼叫, 使的每一次呼叫都更進一步的接近解答。
int BinarySearch(int *a, const int x, const int left, const int right)
{ // 在排序好的陣列a[left], …, a[right]中找
if (left <= right)
{
int middle = (left + right)/2 ;
if (x < a[middle]) return BinarySearch(a, x, left, middle-1) ;
else if (x > a[middle]) return BinarySearch(a, x, middle+1, right) ;
else return middle ;
} // if 結束
return -1 ; //沒找到
}
14
雖然上述的程式碼已經改變,但遞迴的函數呼叫與迴路式的函數呼叫是相同的。
範例:對於一個有n個元素的集合,n>=1,印出此集合所有可能的排列組合。舉例而
言,如果集合為{a,b,c},則此集合的排列組合為{
(a,b,c),(a,c,b),(b,a,c),(b,c,a),(c,a,b ),(c,b,a )}
15
void Permutations (char *a, const int k, const int m)
{ // 產生a[k], …, a[m] 的所有排列
if (k = = m) //輸出排列
{
for (int i =0; i <=m; i++) cout << a[i] << “ “ ;
cout << endl ;
}
else // a [k : m] 還有超過一種以上的排列,遞迴產生它們
for (i = k ; i <= m ; i++)
{
swap(a[k], a[i]);
Permutations(a, k+1, m) ;
swap(a[k], a[i]) ;
}
}
16
1.7
效率分析
首先,我們定義一個程式的空間的時間複雜度。
定義:一個程式的空間複雜度是完全地執行程式所需的記憶體。一個程式的時間複雜度
是完全地執行程式所需要的計算機時間。
空間複雜度
(Space Complexity)
程式所需要的空間為下列各項之和:
(1). 固定的空間需求:
固定空間需求包括指令空間(用以儲存程式碼的空間),用來存簡單變數,固定大小的
結構變數,和常數的空間。
17
(2). 可變的空間需求:
包含大小與所要解決的問題中特定實體I相關的結構化變數所需要的空間。還包
含了當函數採用遞迴方式呼叫所需要的空間。
範例:一連串的數值相加。雖然輸出為單一數值,輸入卻包含了一個陣列。因此,
可變的空間需求視陣列如何傳遞到函數而定。
18
line
1
2
3
4
5
6
line
1
2
3
4
float Sum (float *a , const int n)
{
float s = 0;
for(int i = 0; i <n ; i++)
s += a[i] ;
return s;
}
float Rsum(float *a , const int n)
{
if (n <= 0) return 0 ;
else return (Rsum (a, n-1) + a[n-1]) ;
}
19
遞迴式程式比其相對的迴路式程式有較多的負擔 (overhead) 。
時間複雜度 (Time Complexity)
定義:程式步驟是語法上或語意上有特別意義的程式段落,他的執行時間和實體的
特性時無關的。
應注意的是,一個程式步驟中所代表的運算次數可能和另外一個程式步驟所代表的
運算次數不相同。
例如:a=2算一個步驟,而a=2*b+3*c/d-e+f/g/a/b/c也可以算他為一個步驟。
一個程式或函數解決特定問題所需要的步驟數目,我們可以產生一個總體變數count
來決定,初值設為0。
20
float Sum (float *a, const int n)
{
float s = 0; count++ ; // count是全域變數
for (int i = 0; i <n ; i++) {
count++ ; // 因為for
s += a[i] ;
count++ ; // 因為指派
}
count++ ; // 因為最後一次for的判斷
count++ ; // 因為return
return s ;
}
步驟計數只告訴我們有多少步驟被執行,我們無法知道每一個步驟用了多少時
間。
21
另一種獲得計數的方法是使用表列法。
首先要決定每一個指令的步驟計數。我們稱之為step/execution,或s/e。其次,
找出每一個指令被執行的次數,稱之為頻率。不可執行的指令的頻率為0。將
s/e*頻率=每一指令全部的步驟。將各指令全部的步驟相加就是整個函數所需
的步驟計數。
敘述
s/e
float sum( float list [ ] , int n )
{
float temmpsum = 0;
int I ;
for (i=0 ; i<=n ; i++)
tempsum
+= list [ I ];
return tempsum;
}
0
0
1
0
1
1
1
0
total
頻率
0
0
1
0
n+1
n
1
0
步驟計數
0
0
1
0
n+1
n
1
0
2n+3
22
摘要
程式的時間複雜度是以用來執行函數的程式所發生的步驟之數目來表示。步驟之數
目為實體特性之函數。
步驟是以這些特性的部份集合之函數來表示。
在程式的步驟技術可以決定之前,我們必須確知要使用問題實體的哪個特性。這可
以定義用來表示步驟技術函數的變數。
一但選擇了有關的特性,就可以定義步驟為何嗎?步驟是任何與特性無關的計算單
元。所以,10個加法和100個加法都可以是同一個步驟;但n個加法就不可以。同樣
的,m/2個加法,p+q個減法等等,都不能算成一個步驟。
23
最佳狀況(best case )步驟技術是對指定的參數所能執行的最小步驟數。
最差狀況(worst case )步驟技術是對指定的參數所能執行的最多步驟數。
平均值(average )步驟技術是以指定的參數在實體上執行的平均步驟。
漸進式表示法O , 
,  ( Asymptotic Notation )
我們之所以要決定步驟技術的動機是為了能夠比較計算相同函數的兩個程式之時間
複雜度,以及能夠預測當實體特性改變時,執行時間增長的情形。
因為步驟所代表的函意之不確定性,當以比較兩個程式為目標時,確實的步驟計數
不是很有用的。
24
如果想要決定確實的步驟計數,則應使用這些表示法中的哪一個? 對於這個問
題的答案是漸進式複雜度。通常的做法是先決定程式中每一個指令(或一組指令)
的漸進式複雜度,而後再將它們加總而得。
範例:
敘述
漸進式複雜度
void add( int a[ ] [ MAX_SIZE ]…. )
{
int i j;
for ( i=0;i<rows;i++ )
for ( j=0;j<cols;j++)
c[ i ][ j =a[ i ][ j ]]+b[ i ][ j ];
}
0
0
0
total
(rows, cols)
(rows)
(rows, cols)
(rows, cols)
0
25
上圖不再使用確實的步驟計數,而以漸進式複雜度取代之。對於不執行的指令,其步
驟計數為0,並且較為簡單。
要獲得一個函數的漸進式複雜度,只要將每一個別程式行的漸進式複雜度相加即可。
另一種方法,因為程式的行數是固定的,我們只要取得最大的指令行複雜度即可。不
論怎樣, (rows, cols) 我們將得到為其漸進式複雜度。
範例:計算二分搜尋法函數binsearch的時間複雜度。
因為使用漸進式分析,我們不需要如此精確的最差狀況下迴路執行次數之計算。除了
最後一次,每一次迴路執行時會將必須搜尋的串列範圍減半。也就是說,right_left+1
的值在每一次迴路中以2為因數遞減一半。因此,此迴圈在最差情況下執行
(log n)次。
26