8 數據結構
數據結構是程序設計的重要理論和技術基礎,它所討論的內容和技術,對從事軟件項目的開發有重要作用。學習數據結構要達到的目標是:學會總問題出發,分析和研究計算機加工的數據結構的特性,以便爲應用所涉及的數據選擇適當的邏輯結構、存儲結構及其相應的操作方法,爲提高應用計算解決問題的效率服務。
數據結構是指數據元素的集合(或數據對象)及元素間的相互關係和構造方法。
元素之間的相互關係是數據的邏輯結構,數據元素及元素之間關係的存儲形式稱爲存儲結構(或物理結構)。
數據結構按照邏輯關係的不同分爲線性結構和非線性結構兩大類,其中非線性結構又可分爲樹結構和圖結構。
算法與數據結構緊密相關,數據結構是算法設計的基礎,合理設計的數據結構可使算法簡單而高效。
8.1 線性結構
線性結構是一種基本的數據結構,主要用於對客觀世界中具有單一前驅和後繼的數據關係進行描述。
線性結構的特點是數據元素之間呈現一種線性關係,即元素“ 一個接一個地排列 ”。
8.1.1 線性表
線性表是最簡單、最基本、也是最常用的一種線性結構。
它有兩種存儲方法:
順序存儲 和 鏈式存儲,主要的基本操作是插入、刪除和查找等。
1、線性表的定義
一個線性表是 n (n≥0)個元素的有限序列,通常表示爲(a1,a2,... an)。
非空線性表的特點如下。
(1)存在唯一的一個稱作“ 第一個 ”的元素。
(2)存在唯一的一個稱作“ 最後一個 ”的元素。
(3)除第一個元素外,序列中的每個元素均只有一個直接前驅。
(4)除最後一個元素外,序列中的每個元素均只有一個直接後繼。
2、線性表的存儲結構
線性表的存儲結構分爲順序存儲和鏈式存儲。
1)線性表的順序存儲
線性表的順序存儲是指用一組地址連續的存儲單元依次存儲線性表中的數據元素,從而使得邏輯上相鄰的兩個元素在物理位置上也相鄰,如圖:
在這種存儲方式下,元素件的邏輯關係無需佔用額外的空間來存儲。
一般地,以 LOC(ai)表示線性表中第一個元素的存儲位置,在順序存儲結構中,第 i 個元素 a i 的存儲位置爲:
其中,L 是表中每個數據元素所佔空間的大小。根據該計算關係,可隨機存取表中的任一個元素。
線性表採用順序存儲結構的優點是可以隨機存取表中的元素,缺點是插入和刪除操作需要移動元素。
插入元素前要移動元素以挪出空的存儲單元,然後再插入元素;刪除元素時同樣要移動元素,以填充被刪除的元素空出來的存儲單元。
在表長爲 n 的線性表中插入新元素時,共有 n +1 個插入位置,在位置 1,(元素 a1 所在位置)插入新元素,表中原有的 n 個元素都需要移動,在位置 n+1 (元素 a n所在位置之後)插入新元素時不需要移動任何元素,因此,等概率下(即新元素在 n+1 個位置插入的概率相同時)插入一個新元素需要移動元素的個數 E insert 爲:
其中,Pi 表示在表中的位置 i 插入新元素的概率。
在表長爲 n 的線性表中刪除元素時,共有 n 個可刪除的元素,刪除元素 a1 時需要移動 n-1 個元素,刪除元素 an 是不需要移動元素,因此,等概率下刪除元素時需要移動元素的個數 E delete 爲:
其中,qi表示刪除元素 ai的概率。
2)線性表的鏈式存儲
線性表的鏈式存儲是用節點來存儲數據元素,基本的節點結構如下所示:
其中,數據域用於存儲數據元素的值,指針域則存儲當前元素的直接前驅和直接後繼信息,指針域中的信息稱爲指針(或鏈)。
存儲各數據元素的節點的地址並不要求是連續的,因此存儲數據元素的同時必須存儲元素之間的邏輯關係。
另外,節點空間只有在需要的時候才申請,無需事先分配。
節點之間通過指針域構成一個鏈表,若節點中只有一個指針域,則稱爲線性鏈表(或單鏈表),如圖所示:
設線性表中的元素時整型,則單鏈表節點類型的定義爲:
在鏈表的存儲結構中,只需要一個指針(稱爲頭指針,如上圖中的 head)指向第一個節點,就可以順序訪問到表中的任一個元素。
在鏈式存儲結構下進行插入和刪除,其實質都是對相關指針的修改。
在單鏈表中,若在 p 所指節點後插入新元素節點 (s 所指節點,已經生成),如圖 8-3(a)所示,其基本步驟如下。
(1)s-> link = p-> link;
(2)p-> link = s;
即先將 p 所指節點的後繼節點指針賦給 s 所指節點的指針域,然後將 p 所指節點的指針域修改爲指向 s 所指節點。
同理,在單鏈表中刪除 p 所指節點的後繼節點時(圖(b)),步驟如下:
(1)q = p->link
(2)p->link = p-link->link
(3)free(q)
即先令臨時指針 q 指向待刪除的節點,然後修改 p 所指節點的指針域爲指向 p 所指節點的後繼節點,從而將元素 b 所在的節點從鏈表中摘除,最後釋放 q 所指節點的空間。
實際應用中,爲了簡化對鏈表的狀態的判斷和處理,特別引用一個不存儲數據元素的節點,稱爲頭節點,將其作爲鏈表的第一個節點並令頭指針指向該節點。
下面給出單鏈表上查找、插入和刪除運算的實現過程。
1、【函數】單鏈表的查找預算。
《Code》
2、【函數】單鏈表的插入運算。
《Code》
3、【函數】單鏈表的刪除運算。
《Code》
線性表採用鏈表作爲存儲結構時,不能對數據元素進行隨機訪問,但是插入和刪除操作不需要移動元素。
根據節點中指針域的設置方式,還有其他幾種鏈表結構。
- 雙向鏈表。每個節點包含兩個指針,分別指出當前節點元素的直接前驅和直接後繼。
其特點是可從表中任意的元素節點出發,從兩個方向遍歷鏈表。
- 循環鏈表。在單項鍊表(或雙向鏈表的基礎上),令表尾節點的指針指向表中的第一個節點,構成循環鏈表。
其特點是從表中任意節點開始遍歷整個鏈表。
- 靜態鏈表。藉助數組來描述線性表的鏈式存儲結構。
若雙向鏈表中刪除節點時指針的變化情況如圖8-4(b)所示,其操作過程可表示爲:
(1)p->front -> next = p-> next;
(2)p->front -> next = s;,或者表示爲 s-> front -> next = s;
(3)s-> next = p;
(4)p-> front = s;
在雙向鏈表中刪除節點時指針的變化情況 如圖 8-4(b)所示,其操作過程可表示爲:
(1)p-> front -> next = p-> next;
(2)p-> next -> front = p-> front;
8.1.2 棧和隊列
棧和隊列式軟件設計中常用的兩種數據結構,它們的邏輯結構和線性表相同。其特點在於運算有所限制:棧按“ 後進先出 ”的規則進行操作,隊列按“ 先進先出 ”的規則進行操作,故稱運算受限的線性表。
1、棧
1)棧的定義及其基本運算
(1)棧的定義
棧是隻能通過訪問它的一端來實現數據存儲和檢索的一種線性數據結構。
換句話說,棧的修改時按先進後出的原則進行的。因此,棧又稱爲後進先出(Last In First Out,LIFO)的線性表。
在棧中進行插入和刪除操作的一端稱爲棧頂(top),相應地,另一端稱爲棧底(bottom)。
不含數據元素的棧稱爲空棧。
(2)棧的基本運算。
① 初始化棧 InitStack(S):創建一個空棧(S)。
② 判棧空 StackEmpty(S):當棧 S 爲空時返回“ 真 ”值,否則返回“ 假 ”值。
③ 入棧 Push(S,x):將元素 x 加入棧頂,並更新棧頂指針。
④ 出戰 Pop(S):將棧頂元素從棧中刪除,並更新棧頂指針。若需要得到棧頂元素的值,可將 Pop(S)定義爲一個返回棧頂元素值的函數。
⑤ 讀棧頂元素 Top(S):返回棧頂元素的值,但不修改棧頂指針。
應用中常使用上述 5 中基本運算實現基於棧結構的問題求解。
2)棧的存儲結構
(1)順序存儲。
棧的順序存儲是指用一組地址連續的存儲單元依次存儲自棧頂到棧底的數據元素,同時附設指針 top 指示棧頂元素的位置。
採用順序存儲結構的棧也稱爲順序棧。
在該存儲方式下,需要預先定義(或申請)棧的存儲空間,也就是說,棧空間的容量是有限的。因此,在順序棧中,當一個元素入棧時,需要判斷是否棧滿(棧空間中沒有空閒單元),若棧滿,則元素入棧發生上溢現象。
(2)棧的鏈式存儲。
爲了克服順序存儲的棧可能存在上溢的不足,可以用鏈表存儲棧中的元素。用鏈表作爲存儲結構的棧也稱爲鏈棧。由於棧中元素的插入和刪除僅在棧頂一端進行,因此不必設置頭節點,鏈表的頭指針就是棧頂指針。
如圖 :
(3)棧的應用。
棧的典型應用包括表達式求值、括號匹配等,在計算機語言的實現以及遞歸過程轉變爲非遞歸過程的處理中,棧有重要作用。
2、隊列
1)隊列的定義及其基本運算。
(1)隊列的定義。
隊列式一種先進先出(First In First Out,FIFO)的線性表,它只允許在表的一端插入元素,而在表的另一端刪除元素。
在隊列中,允許插入元素的一端稱爲隊尾(rear),允許刪除元素的一端稱爲隊頭(front)。
(2)隊列的基本運算。
① 初始化隊 InitQueue(Q):創建一個空的隊列 Q。
② 判隊空 Empty(Q):當隊列爲空時返回“ 真 ”值,否則返回“ 假 ”值。
③ 入隊 EnQueue(Q,x):將元素 x 加入到隊列Q的隊尾,並更新隊尾指針。
④ 出隊 DeQueue(Q):將隊頭元素從隊列Q中刪除,並更新隊頭指針。
⑤ 讀取隊頭元素FrontQue(Q):返回隊頭元素的值,但不更新隊頭指針。
2)隊列的存儲結構
(1)隊列的順序存儲
隊列的順序存儲結構又稱爲順序隊列,它也是利用一組地址連續的存儲單元存放隊列中的元素。
由於隊列中元素的插入和刪除限定在表的兩端進行,因此設置隊頭和隊尾指針,分別指示出當前的隊首元素和隊尾元素。
下面設順序隊列 Q 的容量爲 6,其隊頭指針 爲 front,隊尾指針爲 rear,頭、尾指針和隊列中的元素之間的關係如圖8-6所示。
在順序隊列中,爲了降低運算的複雜度,元素入隊時只修改隊尾指針,元素出隊時只修改隊頭指針。
由於順序隊列的存儲空間是提前設定的,所以隊尾指針會有一個上限值,當隊尾指針達到該上限時,就不能只通過修改隊尾指針來實現新元素的入隊操作了。
此時,可通過除取餘運算順序隊列假想成一個環狀結構,如圖 8-7 所示,稱之爲循環隊列。
設置循環隊列Q的容量 MAXSIZE,初始時隊列爲空,且Q.rear 和 Q.front 都等於 0,如圖 8-(a)所示。
元素入隊時,修改隊尾指針 Q.rear = (Q.rear+1)%MAXSIZE,如圖 8-8 (b)所示。
元素出隊時,修改隊頭指針 Q.front = (Q.front+1)%MAXSIZE,如圖 8-8(c)所示。
在隊列空和隊列滿的情況下,循環隊列的隊頭、隊尾指針指向的位置是相同的,此時僅僅根據 Q.rear 和 Q.front之間的關係無法判定隊列的狀態。
爲了區別隊空和隊滿的情況,可採用以下兩種處理方式:
其一,是設置一個標誌位,以區別頭、尾指針的值相同時隊列式空還是滿;
其二,是犧牲一個存儲單元,約定以“ 隊列的尾指針所指位置的下一個位置是隊頭指針 ”表示隊列滿,如圖 8-8(f)所示,而頭、尾指針的值相同時表示隊列爲空。
設隊列中的元素類型爲整型,則循環隊列的類型定義爲。
#define MAXSIZE 100
typedef struct {
int *base; /* 循環隊列的存儲空間 */
int front; /* 指示隊頭元素 */
int rear; /* 指示隊尾元素 */
} SqQueue;
【函數】創建一個空的循環隊列。
《Code》
【函數】元素入循環隊列。
《Code》
【函數】
《Code》
(2)隊列的鏈式存儲。
隊列的鏈式存儲也稱爲鏈隊列。這裏爲了便於操作,給鏈隊列添加一個頭節點,並令頭指針指向頭節點。
因此,隊列爲空的判定條件是:
頭指針和尾指針的值相同,且均指向頭節點。隊列的鏈式存儲結構如圖 8-9所示。
3)隊列的應用
隊列結構常用於處理需要排隊的場合,如操作系統中處理打印任務的打印隊列、離散事件的計算機模擬等。
8.1.3 串
串(字符串)是一種特殊的線性表,其數據元素爲字符。
計算機中非數值問題處理的對象經常是字符串數據。
例如,在彙編和高級語言的編譯程序中,源程序和目標程序都是字符串數據;在事務處理程序中,姓名、地址等一般也是作爲字符串處理的。
串具有自身的特性,運算時常常把一個串作爲一個整體來處理。這裏介紹串的定義、基本運算、存儲結構及串的模式匹配算法。
1、串的定義及基本運算
1)串的定義
串是僅由字符構成的有限序列,是取值範圍受限的線性表。一般記爲 S=‘a1a2...an’,其中 S 是串名,單引號括起來的字符序列是串值。
2)串的幾個基本概念
- 空串:長度爲零的串,空串不包含任何字符。
- 空格串:由一個或多個空格組成的串。雖然空格是一個空白字符,但它也是一個字符,計算串長度時要將其計算在內。
- 子串:由串中任意長度的連續字符構成的序列稱爲子串。含有子串的串稱爲主串。子串在主串中的位置是指子串首次出現時,該子串的第一個字符在主串中的位置。空串是任意串的子串。
- 串相等:指兩個串長度相等且對應位置上的字符也相同。
- 串比較:兩個串比較大小時以字符的ASCII碼值(或其他字符編碼集合)作爲依據。
實質上,比較操作從兩個串的第一個字符開始進行,字符的碼值大者所在的串爲大;
若其中一個串先結束,則以串長較大者爲大。
3)串的基本操作
(1)賦值操作 StrAssign(s,t):將串 s 的值賦給串 t。
(2)聯接操作 Concat(s,t):將串 t 接續在 s 的尾部,形成一個新串。
(3)求串長 StrLength(s):返回串 s 的長度。
(4)串比較 StrCompare(s,t):比較兩個串的大小。返回值 -1、0 和 1 分別表示 s<t、s=t 和 s>t 三種情況。
(5)求子串 SubString(s,start,len):返回串 s 中從 start 開始的、長度爲 len 的字符串序列。
以上 5 種最基本的串操作構成了串的最小操作子集,利用它們可以實現串的其他運算。
2、串的存儲結構
(1)串的定長存儲結構。
串的靜態存儲結構就是串的順序存儲結構,用一組地址連續的存儲單元來存儲串值的字符序列。由於串中的元素爲字符,所以可通過程序語言提供的字符數組定義串的存儲空間,也可以根據串長的需要動態申請字符串的空間。
(2)串的鏈式存儲。
字符串也可以採用鏈表作爲存儲結構,當鏈表存儲串中的字符時,每個節點中可以存儲一個字符,也可以存儲多個字符,此時要考慮存儲密度問題。
在鏈式存儲結構中,節點大小的選擇和順序存儲方法中數組空間大小的選擇一樣重要,它直接影響對串的處理效率。
3、串的模式匹配
子串的定位操作通常稱爲串的模式匹配,它是各種串處理系統中最重要的運算之一。
子串也稱爲模式串。
1)樸素的模式匹配算法
該算法也稱爲布魯特-福斯算法,其基本思想是從主串的第一個字符起與模式串的第一個字符比較,若相等,則繼續逐對字符串後續的比較,否則從主串第二個字符起與模式串的第一個字符重新比較,直至模式串中每個字符依次和主串中一個連續的字符序列相等時爲止,此時稱爲匹配成功。如果不能在主串中找到與模式串相同的子串,則匹配失敗。
【函數】以字符數組存儲字符串,實現樸素的模式匹配算法。
《Code》
假設主串和模式串的長度分別爲 n 和 m,位置序號 從 0 開始計算,下面分析樸素模式匹配算法的時間複雜度。
設從主串的第 i 個位置開始與模式串匹配成功,在前 i 趟匹配中(位置0~i-1),每趟不成功的匹配都是模式串的第一個字符與主串中相應的字符不相同,則在前 i 趟匹配中,字符的比較共進行了 i 次,而第 i+1 趟(從位置 i 開始)成功匹配的字符比較次數爲 m,所以總的字符比較次數爲i+m(0<i<n-m)。
若在主串的 n-m 個起始位置上匹配成功的概率相同,則在最好情況下,匹配成功時字符間的平均次數爲:
因此,在最好情況下匹配算法的時間複雜度爲Ο(n+m)。
而在最壞情況下,每一趟不成功的匹配都是模式串的最後一個字符與主串中響應的字符不相等,則主串中新一趟的起始位置爲 i-m+2 。
若設從主串的第 i 個字符開始匹配時成功,則前 i 趟不成功的匹配中,每趟都比較了 m 次,總共比較了 i × m次。
因此,最壞情況下的平均比較次數爲:
由於 n>>m,所以該算法在最壞情況下的時間複雜度爲Ο(n×m)。
2)改進的模式匹配算法
改進的模式匹配算法又稱爲 KMP 算法,其改進之處在於:每當匹配過程中出現相比較的字符不相等時,不需要回溯主串的字符串位置指針,而是利用已經得到的“ 部分匹配 ”結果,將模式串向右“ 滑動 ”儘可能遠的距離,再繼續進行比較。
設模式串爲“ P0... Pm-1 ”,KMP 匹配算法的思想是:當模式串中的字符 Pj 與主串中相應的字符 Si不相等時,因其前 j個字符(“P0...Pj-1”)已經獲得了匹配,所以若模式串中的“P0...Pk-1”與“Pj-k...Pj-1”相同,這時可令Pk與Si進行比較,從而是 i 無需回退。
在 KMP 算法中,依據模式串的 next 函數值實現了子串的滑動。
若令 next[j] = k,則 next[j] 表示當模式串中的Pj 與主串中相應字符不相等時,令模式串的 Pk 與主串的相應字符進行比較。
next 函數的定義如下:
【函數】求模式串的 next 函數。
《Code》
【函數】KMP 模式匹配算法,設模式串第一個字符的下標爲 0。