線性表的定義
線性表: 零個或多個資料元素的有限序列
線性表的順序儲存結構
順序儲存結構的定義
線性表的兩種物理結構的第一種一一順序儲存結構。
線性表的順序儲存結構,指的是用一段地址連續的儲存單元依次儲存線性表的資料元素。
順序儲存方式
就是在記憶體中找了塊地,通過佔地的形式,把一定記憶體空間給佔了,然後把相同資料型別的資料元素依次存放在這塊空地中。
資料長度與線性表長度的區別
兩個概念"陣列的長度"和"續性表的長度"需要區分一下。 陣列的長度是存放線性表的儲存空間的長度,儲存分配後這個量是一般是不變的。有個別同學可能會問,陣列的大小一定不可以變嗎?我怎麼看到有書中談到可以動態分配的一維陣列。是的,一般高階語言,比如C、VB、 C++都可以用程式設計手段實現動態分配陣列,不過這會帶來效能上的損耗。
線性表的長度是線性表中資料元素的個數,隨著線性表插入和刪除操作的進行,這個量是變化的。 在任意時刻,線性表的長度應該小於等於陣列的長度。
地址計算方法
由於我們數數都是從 1 開始數的,線性表的定義也不能免俗,起始也是 1, 可 C 語言中的陣列卻是從 0 開始第一個下標的,於是線性表的第 i 個元素是要儲存在陣列下標為 i-1 的位置,即資料元素的序號和存放它的陣列下標之間存在對應關係 (如下圖所示)。
用陣列儲存順序表意味著要分配固定長度的陣列空間,由於線位表中可以進行插 入和刪除操作,因此分配的陣列空間要大於等於當前線性表的長度。
其實,記憶體中的地址,就和圖書館或電影院裡的座位一樣,都是有編號的。 儲存器中的每個儲存單元都有自己的編號,這個編號稱為地址。當我們佔座後,佔座的第一個位置確定後,後面的位置都是可以計算的。 試想一下,我是班級成績第五名,我後面的 10 名同學成績名次是多少呢?當然是 6, 7 ,…、 15 ,因為 5 + 1 , 5 + 2,…, 5 + 10。由於每個資料元素,不管它是整型、實型還是字元型,它都是需要佔用一定的儲存單元空間的。假設佔用的是c個儲存單元,那麼線性表中第 i+1 個資料元素的儲存位置和第 i 個資料元素的儲存位置滿足下列關係 (LOC 表示獲得儲存位置的函式)。
通過這個公式,你可以隨時算出線性表中任意位置的地址,不管它是第一個還是 最後一個,都是相同的時間。 那麼我們對每個線性表位置的存入或者取出資料, 對於計算機來說都是相等的時間, 也就是一個常數,因此用我們演算法中學到的時間複雜度的概念來說,它的存取時間效能為 0(1)。我們通常把具有這一特點的儲存結構稱為隨機存取結構。
順序儲存結構的插入與刪除
插入演算法的思路;
- 如果插入位置不合理,丟擲異常;
- 如果線性表長度大於等於陣列長度,則丟擲異常或動態增加容量;
- 從最後一個元素開始向前遍歷到第 i 個位置,分別將它們都向後移動一個位 置;
- 將要插入元素填入位置 i 處;
- 表長加 1。
刪除演算法的思路:
- 如果刪除位置不合理,丟擲異常i
- 取出刪除元素;
- 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們都向前移動一 個位置;
- 表長減 1。
插入與刪除的時間複雜度
線性表順序儲存口結構的優缺點
線性表的鏈式儲存結構
順序儲存結構不足的解決辦法
我們反正也是要讓相鄰元素間留有足夠餘地,那乾脆所有的元素都不要考慮相鄰位置了,哪有空位就到哪裡,而只是讓每個元素知道它下一個元素的位置在哪裡,這樣,我們可以在第一個元素時,就知道第二個元素的位置(記憶體地址) , 而找到它; 在第二個元素時,再找到第三個元素的位置(記憶體地址)。這樣所有的元素我們就都可以通過遍歷而找到。
線性錶鏈式儲存結構定義
單線索,無分支的情況。 線性表的鏈式儲存結構的特點是用一組任意 的儲存單元儲存線性表的資料元素,這組儲存單 元可以是連續的,也可以是不連續的。這就意味 著,這些資料元素可以存在記憶體未被佔用的任意位置 (如下圖所示)。
在順序結構中,每個資料元素只需要存資料元素資訊就可以了。現在鏈式結構中,除了要存資料元素資訊外, 還要儲存它的後繼元素的儲存地址。
有時,我們為了更加方便地對連結串列進行操作,會在單連結串列的第一個結點前附設一個結點,稱為頭結點。頭結點的資料域可以不儲存任何資訊,誰叫它第一個呢,有這個特權。也可以儲存如線性表的長度等附加資訊,頭結點的指標域儲存指向第一個結點的指標,如下圖所示
頭指標與頭結點的異同
線性錶鏈式儲存結構程式碼描述
單連結串列的讀取
獲得連結串列第i個資料的演算法思路:
- 宣告一個結點p指向連結串列第一個結點,初始化j從1開始;
- 當j<i時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一個結點,j累加1;
- 若到連結串列末尾p為空,則說明第i個元素不存在;
說白了,就是從頭開始找,直到第 i 個元素為止。由於這個演算法的時間複雜度取 決於 i 的位置,當 i=l 肘,則不幡遍歷,第一個就取出資料了,而當 i=n 時則遍歷 n-1 次才可以。 因此最壞情況的時間複雜度是 O(n)。
由於單鏈袤的結構中沒有定義表長,所以不能事先知道要迴圈多少次,因此也就 不方便使用 for 來控制迴圈。其主要核心思想就是 "工作指標後移”,這其實也是很多 演算法的常用技術。
單連結串列的插入與刪除
單連結串列的插入
單連結串列第i個資料插入結點的演算法思路:
- 宣告一結點p指向連結串列第一個結點,初始化j從1開始;
- 當j<1時,就遍歷連結串列,讓p的指標向後移動,不斷指向下一個結點,j累加1;
- 若到連結串列末尾p為空,則說明第i個元素不存在;
- 否則查詢成功,在系統中生成一個空結點s;
- 將資料元素 e 賦值給 s->data ;
- 單連結串列的插入標準語旬 s->next=p->next ;p->next=s;
- 返回成功。
單連結串列的刪除
單連結串列第 i 個資料刪除結點的演算法思路:
- 宣告一結點 p 指向連結串列第一個結點 , 初始化 j 從 1 開始j
- 當j<1時, 就遍歷連結串列,讓 p 的指標向後移動,不斷指向下一個結點 ,j累加1;
- 若連結串列末尾p為空,則說明第i個元素不存在;
- 否則查詢成功,將欲刪除的結點p->next賦值給q;
- 單連結串列的刪除標準p->next=q->next;
- 將q結點中的資料賦值給e,作為返回;
- 釋放q結點;
分析一下剛才我們講解的單連結串列插入和刪除演算法,我們發現,官們其實都是由兩部分組成;第一部分就是遍歷查詢第 i 個元素;第二部分就是插入和刪除元素。 從整個演算法來說,我們很容易推匯出:它們的時間複雜度都是 O(n)。如果在我們不知道第 i 個元素的指標位置,單連結串列資料結構在插入和刪除操作上,與線性表的順序儲存結構是沒有太大優勢的。但如果,我們希望從第 i 個位置,插入 10 個元素,對於順序儲存結構意味著,每一次插入都需要移動 n一i 個元素,每次都是 O(n)。而單連結串列,我們只需要在第一次時,找到第 i 個位置的指標,此時為 O(n),接下來只是簡單地通過賦值移動指標而已,時間複雜度都是 0(1)。顯然,對於插入或刪除資料越頻繁的操作,單連結串列的效率優勢就越是明顯。
單連結串列的整表建立
回顧一下, 順序儲存結構的建立,其實就是一個陣列的初始化,即宣告一個型別和大小的陣列並賦值的過程。而單連結串列和順序儲存結構就不一樣,它不像順序儲存結構這麼集中,它可以很散,是一種動態結構。對於每個連結串列來說,它所佔用空間的大小和位置是不需要預先分配劃定的,可以根據系統的情況和實際的需求即時生成。 所以建立單連結串列的過程就是一個動態生成連結串列的過程。即從"空表"的初始狀態起,依次建立各元素結點,並逐個插入鏈衰。
單連結串列整表建立的演算法思路:
- 宣告一結點 p 和計數器變數 i;
- 初始化一空連結串列 L;
- 讓 L 的頭結點的指標指向 NULL,即建立一個帶頭結點的單連結串列;
- 迴圈:
- 生成一新結點賦值給 p;
- 隨機生成一數字賦值給 p 的資料域 p->data;
- 將 p 插入到頭結點與前一新結點之間。
頭插法:就是始終讓新結點在第一的位置。
尾插法:我們把每次新結點都插在終端結點的後面。
單連結串列的整表刪除
單連結串列整表刪除的演算法思路如下:
- 宣告一結點 p和 q;
- 將第一個結點賦值給 p;
- 迴圈:
- 將下一結點賦值給 q;
- 釋放 p;
- 將 q 賦值給 p。
單連結串列結構與順序儲存結構優缺點
簡單地對單連結串列結構和順序儲存結構做對比:
- 儲存分配方式
- 順序儲存結構用一段連續的儲存單元依次儲存線性表的資料元素
- 單連結串列採用鏈式儲存結構,用一組任意的儲存單元存放線性表的元素
- 時間效能
- 查詢 - 順序儲存結構O(1) - 單連結串列O(n)
- 插入和刪除 - 順序儲存結構需要平均移動表長一半的元素,時間為O(n) - 單連結串列線上出某位置的指標後,插入和刪除時間僅為O(1)
- 空間效能
- 順序儲存結構需要預分配儲存空間,分大了,浪費,分小了易發生上溢
- 單連結串列不需要分配儲存空間,只要有就可以分配,元素個數也不受限制
通過上面的對比,我們可以得出一些經驗性的結論:
- 若線性表需要頻繁查詢,很少進行插入和刪除操作時,宜採用順序儲存結構。若需要頻繁插入和刪除時,宜採用單連結串列結構。
- 當線性表中的元素個數變化較大或者根本不知道有多大時,最好用單連結串列結構,這樣可以不需要考慮儲存空間大小的問題。而如果事先知道線性表的大致長度,比如一年十二個月,一週就是七天,這種順序儲存結構效率會高很多。
總之,線性表的順序儲存結構和單連結串列結構各有其優缺點,不能簡單的說哪個好,哪個不好,需要根據實際情況,來綜合平衡採用哪種資料結構更能滿足和達到需求和效能。
靜態連結串列
首先我們讓陣列的元素都是由兩個資料域組成, data和cur。也就是說,陣列的每個下標都對應一個也data和一個cur。資料域data,用來存放資料元素, 也就是通常我們要處理的資料;而遊標cur相當於單連結串列中的next指標,存放該元素的後繼在陣列中的下標。
我們把這種用陣列描述的連結串列叫做靜態連結串列,這種描述方法還有起名叫做遊標實現法。
為了我們方便插入資料,我們通常會把陣列建立得大一些,以便有一些空閒空間可以便於插入時不至於溢位。
另外我們對陣列第一個和最後一個元素作為特殊元素處理,不存資料。我們通常把未被使用的陣列元素稱為備用連結串列。而陣列第一個元素,即下標為0的元素的cur就存放備用連結串列的第一個結點的下標;而陣列的最後一個元素的cur則存放第一個有數值的元素的下標,相當於單連結串列中的頭結點作用,當整個連結串列為空時,則為O²。如下圖所示
假設我們已經將資料存入靜態鏈襲,比如分別存放著"甲"、 "乙"、 "丁"、"戊"、 "己"、"庚" 等資料
此時"甲"這裡就存有下一元素"乙" 的遊標2,"乙"則存有下一元素"丁'的 下標 3。而"庚"是最後一個有值元素,所以它的 cur 設定為 0。而最後一個元素的 cur 則因"甲'是第一有值元素而存有它的下標為 1。 而第一個元素則因空閒空間的第一個元素下標為7 ,所以它的 cur 存有 7。
靜態連結串列的插入操作
靜態連結串列中要解決的是: 如何用靜態模擬動態連結串列結構的儲存空間的分配,需要時申請, 無用時釋放。
我們前面說過,在動態連結串列中,結點的申請和釋放分別借用 malloc ()和 free()兩個函式來實現。在靜態連結串列中,操作的是陣列,不存在像動態連結串列的結點申請和釋放問題,所以我們需要自己實現這兩個函式,才可以做插入和刪除的操作。
為了辨明陣列中哪些分量未被使用,解決的辦法是將所有未被使用過的及已被刪除的分量用遊標鏈成一個備用的連結串列, 每當進行插入時,便可以從備用連結串列上取得第一個結點作為待插入的新結點。
靜態連結串列的刪除操作
靜態連結串列優缺點
總結一下靜態連結串列的優缺點
優點:
- 在插入和刪除操作時,只需要修改遊標,不需要移動元素,從而改進了順序儲存結構中的插入和刪除操作需要移動大量元素的缺點。
缺點:
- 沒有解決連續儲存分配帶來的表長難以確定的問題
- 失去了順序儲存結構隨機存取的特性
總的來說,靜態連結串列其實是為了給沒有指標的高階語言設計的一種實現單連結串列能力的方法。儘管大家不一定會用得上,但這樣的思考方式是非常巧妙的,應該理解其思想,以備不時之需。
迴圈連結串列
將單連結串列中終端結點的指標端由空指標改為指向頭結點,就使整個單連結串列形成一個環,這種頭尾相接的單連結串列稱為單迴圈連結串列,簡稱迴圈連結串列 (circular linked list) 。
為了使空連結串列與非空連結串列處理一致,我們通常設一個頭結點,當然 ,這並不是說,迴圈連結串列一定要頭結點,這需要注意。迴圈連結串列帶有頭結點的空連結串列如圖 3-13-3 所示 :
對於非空的迴圈連結串列就如圖 3-13-4 所示。
其實迴圈連結串列和單連結串列的主要差異就在於迴圈的判斷條件土,原來是判斷 p->next 是否為空,現在則是 p -> next 不等於頭結點,則迴圈未結束。 在單連結串列中,我們有了頭結點時,我們可以用 0(1)的時間訪問第一個結點,但對於要訪問到最後一個結點,卻需要 O(n)時間,因為我們需要將單連結串列全部掃描一遍。 有沒有可能用 0(1)的時間由連結串列指標訪問到最後一個結點呢?當然可以。 不過我們需要改造一下這個迴圈連結串列,不用頭指標,而是用指向終端結點的尾指標來表示迴圈連結串列(如圖 3.13.5 所示) ,此時查詢開始結點和終端結點都很方便了。
從上圖中可以看到,終端結點用尾指標 rear 指示,則查詢終端結點是 0(1) ,而開始結點,其實就是 rear->next->next,其時間複雜也為 0(1)。
舉個程式的例子,要將兩個迴圈鏈襲合併成一個表時,有了尾指標就非常簡單了。 比如下面的這兩個迴圈鏈衰,它們的尾指標分別是 rearA 和 rearB,如圖 3-13-6 所示。
雙向連結串列
雙向連結串列 (也uble linked List) **是在單連結串列的每個結點中,再設定一個指向其前驅結點的指標域。**所以在雙向連結串列中的結點都有兩個指標域, 一個指向直接後繼,另一個指向直接前驅。
既然單連結串列也可以有迴圈鏈衰,那麼雙向連結串列當然也可以是迴圈衰。 雙向連結串列的迴圈帶頭結點的空連結串列如圖3-14-3 所示。
由於這是雙向連結串列,那麼對於連結串列中的某一個結點 p,它的後繼的前驅是誰?當然還是它自己。它的前驅的後繼自然也是它自己,即: p->next->prior = p = p->prior->next
插入操作時,其實並不複雜,不過順序很重要,千萬不能寫反了。 我們現在假設儲存元素 e 的結點為:S,要實現將結點 s 插入到結點 p 和 p -> next 之間需要下面幾步,如圖 3-14-5 所示。
關鍵在於它們的順序,由於第 2 步和第 3 步都用到了 p->next。如果第 4 步先執行,則會使得p->next 提前變成了 S,使得插入的工作完不成。 所以我們不妨把上面這張圖在理解的基礎上記憶,順序是先搞定 s 的前驅和後繼,再搞定後結點的前驅,最 後解決前結點的後繼。 如果插入操作理解了,那麼刪除操作,就比較簡單了。 若要刪除結點 p,只需要下面兩步驟,如圖 3-14-6 所示。
線性表的這兩種結構
參考:大話資料結構
感謝你花時間讀到結尾!:D
後端一枚,默默搬磚擼程式碼,如果覺得不錯歡迎關注我的公眾號