1. 程式人生 > 其它 >資料結構(才學一點點)《大話資料結構》 學習筆記 2002/5/15

資料結構(才學一點點)《大話資料結構》 學習筆記 2002/5/15

研讀《大話資料結構》書中有許多錯誤,但不妨為小白入門資料結構的書,有許多博主專門為其勘錯,結合別人的糾錯看,萬萬不可建立錯誤的理念,全書可以不看完,之後去研讀資料結構大作。(懶得傳圖,附帶連線有圖版)

本文有圖純享版:

連結:https://pan.baidu.com/s/1DhKA159oIeb28QNRvOZ7nw?pwd=Date
提取碼:Date

《大話資料結構》2011版:

連結:https://pan.baidu.com/s/1G6hPJyw0kwgVMpeJcm8p_w?pwd=Date
提取碼:Date

目錄

資料結構

序言

![螢幕截圖 2022-05-12 144302](image/DateStract/螢幕截圖 2022-05-12 144302.png)

資料結構是互相之間存在的一種或者多種特定關係的資料元素集合。同樣是結構,從不同角度討論會有不一樣的分類:

![](image/DateStract/螢幕截圖 2022-05-12 144704.png)

讀後感:

​ 順序儲存結構與連線儲存結構在操作上十分相似,不同的是在於記憶體擴充套件的容易程度,在操作上,順序儲存結常利用下標,而連結儲存結構利用下一結點的頭結點地址。

​ 固定儲存空間大小有好處有壞處,在多個空間大小相似時,可以節省時間開銷。動態空間大小在多個空間大小相差較大時節省儲存空間,但是有更多系統開銷,時間開銷更多。

演算法的時間複雜度 O( )

大寫O()是演算法時間複雜度記法(大O記法)。

推導大O階:

  1. 用常數1取代執行時間中的所有加法常數。
  2. 在修改後的執行次數函式中只保留最高階項。
  3. 如果最高項存在且不是1,則去除與這個項相乘的常數,得到的結果就是大O階。

其實理解大0推導不算難,難的是對數列的一些相關運算,這更多的是考察你的數學知識和能力



常數階O(1):

int sum = O,n- 100;
sum = (1+n) *n/2;
printf ("&d", sum);

​ 這個演算法的執行次數函式式f = (3)。現將3化為1。保留最高項時發現沒有最高項,所以時間複雜度為O(1)

int sum = 0, n= 100;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
sum = (1+n) *n/2;
printf ("&d", sum);

他的時間複雜度為O(1)


線性階O(n)

for(int i = 0; i < n ; i++){
    printf("A");//時間複雜度為o(1)步驟
}

迴圈體內程式碼執行n此,得到時間複雜度為O(n)。

對數階O(log n)

int a = 1;
while(a < n){
	a = a * 2;//時間複雜度為o(1)步驟
}

迴圈次數x : a*2x == n; 所以 x = log n。 所以他的時間複雜度為O(log n )。

平方階O(n2)

int i,i;
for (i= 0; i < n; i++){
	for (j = o; j < n; j++){
		/*時間複雜度為O(1)的程式步驟序列*/
	}
}

迴圈次數: n * n 。 故時間複雜度為O(n2)

int i,j;
for (i = 0; i < n; i++){
    for (j=i; j < n: j++) {
        /*時間複雜度為O(1)的程式步驟序列*/
    }
}

次數: n +(n - 1) + (n - 2)+... + 1 = n*(n+1)/2 = n2/2 + n/2

故時間複雜度為O(n2)


平均執行時間是所有情況中最有意義的,因為他是期望的執行時間,但一般都是通過執行一定數量得到的。

在沒有特殊說明的情況下,都指的是最壞時間複雜度。


空間複雜度S(n)

在一些計算演算法中,可以犧牲一些空間開銷換取計算時間。

一般情況下,一個程式在機器上執行時,除了需要儲存程式本身的指令、常數、變數和輸入資料外,還需要儲存對資料操作的儲存單元。若輸入資料所佔空間只取決於問題本身,和演算法無關,這樣只需要分析該演算法在實現時所需的輔助單元即可。若演算法執行時所需的輔助空間相對於輸入資料量而言是個常數,則稱此演算法為原地工作,空間複雜度為0(1)。

略........


線性表

零個或者多個具有相同型別的資料元素的有限序列,連結串列,棧,佇列......都是其的子類。

線性表的順序儲存結構(地址連續)


線性表的連結儲存結構(地址不連續)


靜態連結串列

若之後遍歷陣列,不根據下標,而是根據cur儲存的數字進行遍歷,下標確定資料物件的位置,cur決定資料物件的順序,最後一位下標儲存開頭的cur,若想訪問開頭的car,必須訪問 陣列長度-1 下標位置的car。(本質上是有些語言沒有指標,需要自己定義,可以憑藉陣列完成這樣的操作,建立自己的靜態連結串列,不僅有下標訪問,同時資料物件還儲存類似於指標的cur(int) 儲存下一個你想讓他指向的資料物件的下標)是一種非常巧妙的辦法。

靜態連結串列原理實現:


末端插入實現:

StaticList[0].cur = 7

是因為之後有末端插入工作,將第一個空閒空間下標儲存在這,插入時直接根據此找到位置,無需遍歷陣列。


中間插入實現,申請結點的malloc ()

由於cur儲存下一個資料物件的下標位置(相當於動態連結串列的指標),故可以不在儲存空間順序排列,直接加在儲存空間末尾,修改插入位置前後的car,即可實現不在空間上挪動資料,邏輯上插入。(與前面動態連結串列原理相似,只不過提前開闢了記憶體空間)

實現:


刪除資料物件實現(釋放結點的 free() )

​ 理解了前面插入資料物件後,刪除物件十分簡單

Status ListDelect(StaticLinkList L, int i)
{
    int j,k;
	if(i < 1 || i > ListLength(L))
        return ERROR
	k = MAX_SIZE - 1;
    for(j = 1; j <= i-1, j++)
        k = L[k].cur;
    j = L[k].cur;
    L[k].cur = L[j].cur;
    Free_SSL(L,j);
    return OK;
}

暫且跳過,沒看懂


迴圈連結串列

​ 將單鏈表中終端結點的指標端由空指標改為指向頭指標,使整個單鏈表形成一個環,這種頭尾相接的單鏈表成為單迴圈連結串列,簡稱迴圈連結串列(circular linked list)

迴圈連結串列解決了

無法找到他的前驅結點的問題,同時將頭結點作為終止條件即可遍歷陣列,可以從任意一結點出發,訪問連結串列全部結點。

同時還能連線兩迴圈連結串列:


雙向連結串列

用空間換取時間


棧與佇列

棧是一種特殊的線性表


棧的順序儲存結構


Push操作:


pop操作:


相同資料型別的棧,一個滿一個空時,兩者可以共享記憶體。


棧的鏈式儲存結構


push:


pop:


棧的作用

​ 棧的引入簡化了程式設計的問題,劃分了不同關注層次,使得思考範圍縮小,更加聚焦於我們要解決的問題核心。反之,像陣列等,因為要分散精力去考慮陣列的下標增減等細節問題,反而掩蓋了問題的本質。所以現在的許多高階語言,比如Java、C#等都有對棧結構的封裝,你可以不用關注它的實現細節,就可以直接使用Stack的push和pop方法,非常方便。

​ 結合下面的應用,個人理解棧:

棧是一種特殊的線性表,相比於前面的連結串列,他省略了下標處理等一系列操作,更加關注與問題的解決,例如四則運算,只專注於如何處理表達式。類似於捨棄了連結串列原先的方法,寫入pop,push操作來處理資料。


棧的應用——遞迴

​ 選代和遞迴的區別是:選代使用的是迴圈結構,遞迴使用的是選擇結構。遞迴能使程式的結構更清晰、更簡潔、更容易讓人理解,從而減少讀懂程式碼的時間。但是大量的遞迴呼叫會建立函式的副本,會耗費大量的時間和記憶體。選代則不需要反覆呼叫函式和佔用額外的記憶體。因此我們應該視不同情況選擇不同的程式碼實現方式。

​ 對於高階語言,遞迴問題的細節實現不需要使用者管理棧。但遞迴時就是根據一個外在條件不斷判斷是否結束,期間棧中不斷push入一段計算結果的程式碼(棧處於不斷push入的狀態),最後達到終止條件時,計算結果的程式碼全部pop,對應的結果不斷更新至得到答案。


——四則運算

字尾(逆波蘭)表達法定義
中綴表示式轉字尾表示式

中綴表示式: 9 + (3 - 1) * 3 + 10 / 2

字尾表示式: 931-3*+102/+

兩步:

  1. 將中綴表示式轉化為字尾表示式(棧用來進出運算子號)
  2. 將字尾表示式進行運算得出結果(棧用來進出運算數字)

第一步:

9 + (3 - 1) * 3 + 10 / 2

從左到右遍歷整個表示式,若是數字直接輸出,push符號,當即將push的符號優先順序低於top的符號優先順序則輸出棧內所有符號,若表示式輸出完畢,輸出所有棧內符號。遇到括號(時,先正常處理,直到遇到)閉合後,將()內全部符號輸出,:

輸出9 , push + , 下一個遇到了( 由於未閉合,繼續push - 直到遇到)閉合,將()內符號輸出。931-

後 push * 因為 + 優先順序低於 * 不輸出,繼續輸出 3

由於下一個符號 + 的優先順序小於 * , pop * , pop +

(先檢查符號,後進行push, pop)


佇列queue

佇列在程式設計中用得非常頻繁。比如用鍵盤進行各種字母或數字的輸入,到顯示器上如記事本軟體上的輸出,其實就是佇列的典型應用

queue的抽象資料型別


串(字串)

​ 計算機中的常用字元是使用標準的ASCII編碼,更準確一點,由7位二進位制數表示一個字元,總共可以表示128個字元。後來發現一些特殊符號的出現,128個不夠用,於是擴充套件ASCII碼由8位二進位制數表示一個字元,總共可以表示256個字元,這已經足夠滿足以英語為主的語言和特殊符號進行輸入、儲存、輸出等操作的字元需要了。可是,單我們國家就有除漢族外的滿、回、藏、蒙古、維吾爾等多個少數民族文字,換作全世界估計要有成百上千種語言與文字,顯然這256個字元是不夠的,因此後來就有了Unicode編碼,比較常用的是由16位的二進位制數表示一個字元,這樣總共就可以表示216個字元,約是65萬多個字元,足夠表示世界上所有語言的所有字元了。當然,為了和ASCII碼相容, Unicode的前256個字元與ASCII碼完全相同

串的順序儲存結構

​ 串的順序儲存結構是用一組地址連續的儲存單元來儲存串中的字元序列的。按照預定義的大小,為每個定義的串變數分配一個固定長度的儲存區。一般是用定長陣列來定義。既然是定長陣列,就存在一個預定義的最大串長度,一般可以將實際的串長度值儲存在陣列的0下標位置,有的書中也會定義儲存在陣列的最後一個下標位置。但也有些程式語言不想這麼幹,覺得存個數字佔個空間麻煩。它規定在串值後面加一個不計入串長度的結束標記字元,比如“\0”來表示串值的終結,這個時候,你要想知道 串的長度,只需要遍歷計算一下。

​ 對於串的順序儲存,有一些變化,串值的儲存空間可在程式執行過程中動態分配而得。比如在計算機中存在一個自由素者區,叫做“堆”。這個堆可由C 語言的動態分配函式malloc ()和free ()n

串的鏈式儲存結構

串的樸素模式匹配演算法


KMP模式匹配演算法

通過這種匹配,減少匹配次數

略。。。。。。之後再看


子樹的個數沒有限制,但他們一定是互不相交的

樹的抽象資料型別

樹的儲存結構

對於樹的儲存結構,如果直接用順序儲存結構,無法反應各個結點的邏輯關係,但是我們可以應用順序儲存結構與鏈式儲存結構的特點來實現儲存

雙親表示法(結點存入雙親地址/陣列下標,順序結構)

加入長子域:(第一個孩子的下標,若沒有則設為-1)

加入兄弟域:(有了長子域後再加入次子域,若沒有設為-1)

孩子表示法(連結串列)

雙親孩子表達法

也可以結合兩個方法的優缺點

孩子兄弟表達法

這個方法相比於前面,更能快速找到某個結點的孩子。前面的方法如果要進入下一結點,需要經過兄弟,但是孩子兄弟表達法可以從一個結點的下標二,快速到該節點的孩子,但這不是最重要的好處。

前面的方法是以樹的層為劃分,而二叉樹是以結點為研究物件


二叉樹

引子:

現在我們來做個遊戲,我在紙上已經寫好了一個100以內的正整數數字,請大家想辦法猜出我寫的是哪一個?注意你們猜的數字不能超過7個,我的回答只會告訴你是“大了”或“小了”。

特殊的樹狀結構,二叉樹

二叉樹的特點

Top To Bottom

目錄