基本概念和術語
1、數據(Data)
數據是外部世界信息的載體,它能夠被計算機識別、存儲和加工處理,是計算機程序加工的原料。計算機程序處理各種各樣的數據,可以是數值數據,如整數、實數或復數;也可以是非數值數據,如字符、文字、圖形、圖像、聲音等。
2、數據元素(Data Element)和數(DataItem) 數據元素是數據的基本單位,在計算機程序中通常被作為一個整體進行考慮和處理。數據元素有時也被稱為元素、結點、頂點、記錄等。一個數據元素可由若干個數據項(Data Item)組成。數據項是不可分割的、含有獨立意義的最小數據單位,數據項有時也稱為字段(Field)或域(Domain)。例如,在數據庫信息處理系統中,數據表中的一條記錄就是一個數據元素。這條記錄中的學生學號、姓名、性別、籍貫、出生年月、成績等字段就是數據項。數據項分為兩種,一種叫做初等項,如學生的性別、籍貫等,在處理時不能再進行分割;另一種叫做組合項,如學生的成績,它可以再分為數學、物理、化學等更小的項。
3、數據對象(Data Object)數據對象是性質相同的數據元素的集合,是數據的一個子集。例如,整數數據對象是{0,±1,±2,±3,…},字符數據對象是{a,b,c,…}。
4、數據類型(Data Type)
數據類型是高級程序設計語言中的概念,是數據的取值范圍和對數據進行操作的總和。數據類型規定了程序中對象的特性。程序中的每個變量、常量或表達式的結果都應該屬於某種確定的數據類型。例如,C#語言中的字符串類型(String,經常寫為string)。一個String表示一個恆定不變的字符序列集合,所有的字符序列集合構成String的取值范圍。我們可以對String進行求長度、復制、連接兩個字符串等操作。
數據類型可分為兩類:一類是非結構的原子類型,如C#語言中的基本類型(整型、實型、字符型等);另一類是結構類型,它的成分可以由多個結構類型組成,並可以分解。結構類型的成分可以是非結構的,也可以是結構的。例如,C#語言中數組的成分可以是整型等基本類型,也可以是數組等結構類型。
5、數據結構(Data Structure)
數據結構是相互之間存在一種或多種特定關系的數據元素的集合。在任何問題中,數據元素之間都不是孤立的,而是存在着一定的關系,這種關系稱為結構(Structure)。根據數據元素之間關系的不同特性,通常有4類基本數據結構:
(1) 集合(Set):如圖1.1(a)所示,該結構中的數據元素除了存在“同屬於一個集合”的關系外,不存在任何其它關系。
(2) 線性結構(Linear Structure):如圖1.1(b)所示,該結構中的數據元素存在着一對一的關系。
(3) 樹形結構(Tree Structure):如圖1.1(c)所示,該結構中的數據元素存在着一對多的關系。
(4) 圖狀結構(Graphic Structure):如圖1.1(d)所示,該結構中的數據元素存在着多對多的關系。
由於集合中的元素的關系極為松散,可用其它數據結構來表示,所以本書不做專門介紹。關於集合的概念在1.3.1小節中有介紹。
數據結構的形式化定義為:數據結構(Data Structure)簡記為DS,是一個二元組,
DS = (D,R)
其中:D是數據元素的有限集合,
R是數據元素之間關系的有限集合。
下面通過例題來進一步理解后3類數據結構。
【例1-1】 學生信息表(如表1.1所示.)是一個線性的數據結構,表中的每一行是一個記錄(在數據庫信息處理系統中,表中的一個數據元素稱為一個記錄)。一條記錄由學號、姓名、行政班級、性別和出生年月等數據項組成。表中數據元素之間的關系是一對一的關系。
表1.1 學生信息表
【例1-2】 家族關系是典型的樹形結構,圖1.2是一個三代的家族關系。在圖中,爺爺、兒子、女兒、孫子、孫女或外孫女是一個結點(在樹形結構中,數據元素稱為結點),他們之間是一對多的關系。其中,爺爺有兩個兒子和一個女兒,這是一對三的關系;一個兒子有兩個兒子(爺爺的孫子),這是一對二的關系;另一個兒子有一個兒子(爺爺的孫子)和一個女兒(爺爺的孫女),這是一對二的關系;女兒有三個女兒(爺爺的外孫女),這是一對三的關系。樹形結構具有嚴格的層次關系,爺爺在樹形結構的最上層,中間層是兒子和女兒,最下層是孫子、孫女和外孫女。不能把這種關系倒過來,因為絕對不會先有兒子或女兒再有爺爺,也不會先有孫子或孫女再有兒子、先有外孫女再有女兒。
【例1-3】 圖1.3是四個城市的公路交通圖,這是一個典型的圖狀結構。在圖中,每個城市是一個頂點(在圖狀結構中,數據元素稱為頂點),它們之間是多對多的關系。成都與都江堰、雅安直接通公路,都江堰與成都、青城山直接通公路,青城山與都江堰、成都及雅安直接通公路,雅安與成都、青城山直接通公路。這些公路構成了一個公路交通網,所以,又把圖狀結構稱為網狀結構(Network Structure)
從數據類型和數據結構的概念可知,二者的關系非常密切。數據類型可以看作是簡單的數據結構。數據的取值范圍可以看作是數據元素的有限集合,而對數據進行操作的集合可以看作是數據元素之間關系的集合。
數據結構包括數據的邏輯結構和物理結構。上述數據結構的定義就是數據的邏輯結構(Logic Structure),數據的邏輯結構是從具體問題抽象出來的數學模型,是為了討論問題的方便,與數據在計算機中的具體存儲沒有關系。然而,我們討論數據結構的目的是為了在計算機中實現對它的操作,因此還需要研究在計算機中如何表示和存儲數據結構,即數據的物理結構(Physical Structure)。數據的物理結構又稱為存儲結構(Storage Structure),是數據在計算機中的表示(又叫映像)和存儲,包括數據元素的表示和存儲以及數據元素之間關系的表示和存儲。
數據的存儲結構包括順序存儲結構和鏈式存儲結構兩種。順序存儲結構(Sequence Storage Structure)是通過數據元素在計算機存儲器中的相對位置來表示出數據元素的邏輯關系,一般把邏輯上相鄰的數據元素存儲在物理位置相鄰的存儲單元中。在C#語言中用數組來實現順序存儲結構。因為數組所分配的存儲空間是連續的,所以數組天生就具有實現數據順序存儲結構的能力。鏈式存儲結構(Linked Storage Structure)對邏輯上相鄰的數據元素不要求其存儲位置必須相鄰。鏈式存儲結構中的數據元素稱為結點(Node),在結點中附設地址域(Address Domain)來存儲與該結點相鄰的結點的地址來實現結點間的邏輯關系。這個地址稱為引用(Reference),這個地址域稱為引用域(Reference Domain)。
從20世紀60年代末到70年代初,出現了大型程序,軟件也相對獨立,人們越來越重視數據結構,認為程序設計的實質是確定數據結構,加上設計一個好的算法,這就是人們常說的“程序=數據結構+算法”。下一節談談算法的問題。
算法的特性
算法(Algorithm)是對某一特定類型的問題的求解步驟的一種描述,是指令的有限序列。其中的每條指令表示一個或多個操作。一個算法應該具備以下5個特性:
1、有窮性(Finity):一個算法總是在執行有窮步之后結束,即算法的執行時間是有限的。
2、確定性(Unambiguousness):算法的每一個步驟都必須有確切的含義,即無二義,並且對於相同的輸入只能有相同的輸出。
3、輸入(Input):一個算法具有零個或多個輸入。它即是在算法開始之前給出的量。這些輸入是某數據結構中的數據對象。
4、 輸出(Output):一個算法具有一個或多個輸出,並且這些輸出與輸入之間存在着某種特定的關系。
5、 能行性(realizability):算法中的每一步都可以通過已經實現的基本運算的有限次運行來實現。
算法的含義與程序非常相似,但二者有區別。一個程序不一定滿足有窮性。例如操作系統,只要整個系統不遭破壞,它將永遠不會停止。還有,一個程序只能用計算機語言來描述,也就是說,程序中的指令必須是機器可執行的,而算法不一定用計算機語言來描述,自然語言、框圖、偽代碼都可以描述算法。
對於一個特定的問題,采用的數據結構不同,其設計的算法一般也不同,即使在同一種數據結構下,也可以采用不同的算法。那么,對於解決同一問題的不同算法,選擇哪一種算法比較合適,以及如何對現有的算法進行改進,從而設計出更適合於數據結構的算法,這就是算法評價的問題。評價一個算法優劣的主要標准如下:
1、正確性(Correctness)。算法的執行結果應當滿足預先規定的功能和性能的要求,這是評價一個算法的最重要也是最基本的標准。算法的正確性還包括對於輸入、輸出處理的明確而無歧義的描述。
2、可讀性(Readability)。算法主要是為了人閱讀和交流,其次才是機器的執行。所以,一個算法應當思路清晰、層次分明、簡單明了、易讀易懂。即使算法已轉變成機器可執行的程序,也需要考慮人能較好地閱讀理解。同時,一個可讀性強的算法也有助於對算法中隱藏錯誤的排除和算法的移植。
3、健壯性(Robustness)。一個算法應該具有很強的容錯能力,當輸入不合法的數據時,算法應當能做適當的處理,使得不至於引起嚴重的后果。健壯性要求表明算法要全面細致地考慮所有可能出現的邊界情況和異常情況,並對這些邊界情況和異常情況做出妥善的處理,盡可能使算法沒有意外的情況發生。
4、運行時間(Running Time)。運行時間是指算法在計算機上運行所花費的時間,它等於算法中每條語句執行時間的總和。對於同一個問題如果有多個算法可供選擇,應盡可能選擇執行時間短的算法。一般來說,執行時間越短,性能越好。
5、占用空間(Storage Space)。占用空間是指算法在計算機上存儲所占用的存儲空間,包括存儲算法本身所占用的存儲空間、算法的輸入及輸出數據所占用的存儲空間和算法在運行過程中臨時占用的存儲空間。算法占用的存儲空間是指算法執行過程中所需要的最大存儲空間,對於一個問題如果有多個算法可供選擇,應盡可能選擇存儲量需求低的算法。實際上,算法的時間效率和空間效率經常是一對矛盾,相互抵觸。我們要根據問題的實際需要進行靈活的處理,有時需要犧牲空間來換取時間,有時需要犧牲時間來換取空間。
通常把算法在運行過程中臨時占用的存儲空間的大小叫算法的空間復雜度(Space Complexity)。算法的空間復雜度比較容易計算,它主要包括局部變量所占用的存儲空間和系統為實現遞歸所使用的堆棧占用的存儲空間。
如果算法是用計算機語言來描述的,還要看程序代碼量的大小。對於同一個問題,在用上面5條標准評價的結果相同的情況下,代碼量越少越好。實際上,代碼量越大,占用的存儲空間會越多,程序的運行時間也可能越長,出錯的可能性也越大,閱讀起來也越麻煩。
在以上標准中,本書主要考慮程序的運行時間,也考慮執行程序所占用的空間。影響程序運行時間的因素很多,包括算法本身、輸入的數據以及運行程序的計算機系統等。計算機的性能由以下因素決定:
1、硬件條件。包括所使用的處理器的類型和速度(比如,使用雙核處理器還是單核處理器)、可使用的內存(緩存和RAM)以及可使用的外存等。
2、實現算法所使用的計算機語言。實現算法的語言級別越高,其執行效率相對越低。
3、所使用的語言的編譯器/解釋器。一般而言,編譯的執行效率高於解釋,但解釋具有更大的靈活性。
4、所使用的操作系統軟件。操作系統的功能主要是管理計算機系統的軟件和硬件資源,為計算機用戶方便使用計算機提供一個接口。各種語言處理程序如編譯程序、解釋程序等和應用程序都在操作系統的控制下運行。
算法的時間復雜度
一個算法的時間復雜度(Time Complexity)是指該算法的運行時間與問題規模的對應關系。一個算法是由控制結構和原操作構成的,其執行的時間取決於二者的綜合效果。為了便於比較同一問題的不同算法,通常把算法中基本操作重復執行的次數(頻度)作為算法的時間復雜度。算法中的基本操作一般是指算法中最深層循環內的語句,因此,算法中基本操作語句的頻度是問題規模n的某個函數f(n),記作:T(n)=O(f(n))。其中“O”表示隨問題規模n的增大,算法執行時間的增長率和f(n)的增長率相同,或者說,用“O”符號表示數量級的概念。例如,如)1n(n21)n(T−=,則 )1n(n21−的數量級與n2相同,所以T(n)=O(n2)。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { int n = 4; int x = n; int y = 0; while (y < x) { y = y + 1; // 頻度是n,則程序段的時間復雜度是T(n)=O(4)。 } Console.WriteLine("運行結果:x={0},y={1},循環{2}次", x, y, n); Console.Read(); int n = 4; int a = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { a = i * j; //頻度為4*4,則程序段的時間復雜度為T(n)=O(16)。 } } Console.WriteLine("{0}", a); Console.Read(); int n = 4; int x = n; int y = 0; while (x >= (y + 1) * (y + 1)) { y = y + 1; //頻度是√4,則程序段的時間復雜度是T(n)=O(2)。 } Console.WriteLine("{0}", y); Console.Read(); //for(i=0;i<m;i++) //{ // for(j=0;j<t;j++) // { // for(k=0;k<n;k++) // { // c[i][j]=c[i][j]+a[i][k]*b[k][j]; ① // } // } //} //解:這是三重循環的程序,最外層for循環的循環次數為m,中間層for循環的循環次數為t,最里層for循環的循環次數為t,所以,該程序段中語句①的頻度是m*n*t,則程序段的時間復雜度是T(n)=O(m*n*t)。 } } }