1. 程式人生 > >演算法一看就懂之「 陣列與連結串列 」

演算法一看就懂之「 陣列與連結串列 」

資料結構是我們軟體開發中最基礎的部分了,它體現著我們程式設計的內功。大多數人在正兒八經學習資料結構的時候估計是在大學計算機課上,而在實際專案開發中,反而感覺到用得不多。

其實也不是真的用得少,只不過我們在使用的時候被很多高階語言和框架元件封裝好了,真正需要自己去實現的地方比較少而已。但別人封裝好了不代表我們就可以不關注了,資料結構作為程式設計師的內功心法,是非常值得我們多花時間去研究的,我這就翻開書複習複習:

本文就先從大家最經常使用的「 陣列 」和「 連結串列 」聊起。不過在聊陣列和連結串列之前,咱們先看一下資料的邏輯結構分類。通俗的講,資料的邏輯結構主要分為兩種:

  • 線性的:就是連成一條線的結構,本文要講的陣列和連結串列就屬於這一類,另外還有 佇列、棧 等

  • 非線性的:顧名思義,資料之間的關係是非線性的,比如 堆、樹、圖 等

知道了分類,下面我們來詳細看一下「 陣列 」和「 連結串列 」的原理。

一、「 陣列 」是什麼?

陣列是一個有限的、型別相同的資料的集合,在記憶體中是一段連續的記憶體區域。

如下圖:

陣列的下標是從0開始的,上圖陣列中有6個元素,對應著下標依次是0、1、2、3、4、5,同時,數組裡面存的資料的型別必須是一致的,比如上圖中存的都是數字型別。陣列中的全部元素是“連續”的儲存在一塊記憶體空間中的,如上圖右邊部分,元素與元素之間是不會有別的儲存隔離的。另外,也是因為陣列需要連續的記憶體空間,所以陣列在定義的時候就需要提前指定固定大小,不能改變。

  • 陣列的訪問:

    陣列在訪問操作方面有著獨特的效能優勢,因為陣列是支援隨機訪問的,也就是說我們可以通過下標隨機訪問陣列中任何一個元素,其原理是因為陣列元素的儲存是連續的,所以我們可以通過陣列記憶體空間的首地址加上元素的偏移量計算出某一個元素的記憶體地址,如下:

    array[n]的地址 =  array陣列記憶體空間的首地址 + 每個元素大小*n

    通過上述公式可知:陣列中通過下標去訪問資料時並不需要遍歷整個陣列,因此陣列的訪問時間複雜度是 O(1),當然這裡需要注意,如果不是通過下標去訪問,而是通過內容去查詢陣列中的元素,則時間複雜度不是O(1),極端的情況下需要遍歷整個陣列的元素,時間複雜度可能是O(n),當然通過不同的查詢演算法所需的時間複雜度是不一樣的。

  • 陣列的插入與刪除:

    同樣是因為陣列元素的連續性要求,所以導致陣列在插入和刪除元素的時候效率比較低。

    如果要在陣列中間插入一個新元素,就必須要將要相鄰的後面的元素全部往後移動一個位置,留出空位給這個新元素。還是拿上面那圖舉例,如果需要在下標為2的地方插入一個新元素11,那就需要將原有的2、3、4、5幾個下標的元素依次往後移動一位,新元素再插入下標為2的位置,最後形成新的陣列是:

    23、4、11、6、15、5、7

    如果新元素是插入在陣列的最開頭位置,那整個原始陣列都需要向後移動一位,此時的時間複雜度為最壞情況即O(n),如果新元素要插入的位置是最末尾,則無需其它元素移動,則此時時間複雜度為最好情況即O(1),所以平均而言陣列插入的時間複雜度是O(n)

    陣列的刪除與陣列的插入是類似的。

所以整體而言,陣列的訪問效率高,插入與刪除效率低。不過想改善陣列的插入與刪除效率也是有辦法的,來來來,下面的「 連結串列 」瞭解一下。

二、「 連結串列 」是什麼?

連結串列是一種物理儲存單元上非連續、非順序的儲存結構,資料元素的邏輯順序是通過連結串列中的指標連結次序實現的,一般用於插入與刪除較為頻繁的場景。

上圖是“單鏈表”示例,連結串列並不需要陣列那樣的連續空間,它只需要一個個零散的記憶體空間即可,因此對記憶體空間的要求也比陣列低。

連結串列的每一個節點通過“指標”連結起來,每一個節點有2部分組成,一部分是資料(上圖中的Data),另一部分是後繼指標(用來儲存後一個節點的地址),在這條鏈中,最開始的節點稱為Head,最末尾節點的指標指向NULL。

「 連結串列 」也分為好幾種,上圖是最簡單的一種,它的每一個節點只有一個指標(後繼指標)指向後面一個節點,這個連結串列稱為:單向連結串列,除此之外還有 雙向連結串列、迴圈連結串列 等。

雙向連結串列:

雙向連結串列與單向連結串列的區別是前者是2個方向都有指標,後者只有1個方向的指標。雙向連結串列的每一個節點都有2個指標,一個指向前節點,一個指向後節點。雙向連結串列在操作的時候比單向連結串列的效率要高很多,但是由於多一個指標空間,所以佔用記憶體也會多一點。

迴圈連結串列:

其實迴圈連結串列就是一種特殊的單向連結串列,只不過在單向連結串列的基礎上,將尾節點的指標指向了Head節點,使之首尾相連。

  • 連結串列的訪問

    連結串列的優勢並不在與訪問,因為連結串列無法通過首地址和下標去計算出某一個節點的地址,所以連結串列中如果要查詢某個節點,則需要一個節點一個節點的遍歷,因此連結串列的訪問時間複雜度為O(n)

  • 連結串列的插入與刪除

    也正式因為連結串列記憶體空間是非連續的,所以它對元素的插入和刪除時,並不需要像陣列那樣移動其它元素,只需要修改指標的指向即可。

    例如:刪除一個元素E:

    例如:插入一個元素:

既然插入與刪除元素只需要改動指標,無需移動資料,那麼連結串列的時間插入刪除的時間複雜度為O(1)不過這裡指的是找到節點之後純粹的插入或刪除動作所需的時間複雜度。

如果當前還未定位到指定的節點,只是拿到連結串列的Head,這個時候要去刪除此連結串列中某個固定內容的節點,則需要先查詢到那個節點,這個查詢的動作又是一個遍歷動作了,這個遍歷查詢的時間複雜度卻是O(n),兩者加起來總的時間複雜度其實是O(n)的。

其實就算是已經定位到了某個要刪除的節點了,刪除邏輯也不簡單。以“刪除上圖的E節點”為例,假如當前連結串列指標已經定位到了E節點,刪除的時候,需要將這個E節點的前面一個節點H的後繼指標改為指向A節點,那麼E節點就會自動脫落了,但是當前連結串列指標是定位在E節點上,如何去改變H節點的後續指標呢,對於“單向連結串列”而言,這個時候需要從頭遍歷一遍整個連結串列,找到H節點去修改其後繼指標的內容,所以時間複雜度是O(n),但如果當前是“雙向連結串列”,則不需要遍歷,直接通過前繼指標即可找到H節點,時間複雜度是O(1),這裡就是“雙向連結串列”相當於“單向連結串列”的優勢所在。

三、「 陣列和連結串列 」的演算法實戰?

通過上面的介紹我們可以看到「 陣列 」和「 連結串列 」各有優勢,並且時間複雜度在不同的操作情況下也不相同,不能簡單一句O(1)或O(n)。所以下面我們找了個常用的演算法題來練習練習。

演算法題:反轉一個單鏈表
輸入: 1->2->3->4->5->NULL
輸出: 5->4->3->2->1->NULL

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        //定義一個前置節點變數,預設是null,因為對於第一個節點而言沒有前置節點
        ListNode pre = null;
        //定義一個當前節點變數,首先將頭節點賦值給它
        ListNode curr = head;
        //遍歷整個連結串列,直到當前指向的節點為空,也就是最後一個節點了
        while(curr != null){
            //在迴圈體裡會去改變當前節點的指標方向,本來當前節點的指標是指向的下一個節點,現在需要改為指向前一個節點,但是如果直接就這麼修改了,那鏈條就斷了,再也找不到後面的節點了,所以首先需要將下一個節點先臨時儲存起來,賦值到temp中,以備後續使用
            ListNode temp = curr.next;
            //開始處理當前節點,將當前節點的指標指向前面一個節點
            curr.next = pre;
            //將當前節點賦值給變數pre,也就是讓pre移動一步,pre指向了當前節點
            pre = curr;
            //將之前儲存的臨時節點(後面一個節點)賦值給當前節點變數
            curr = temp;
            //迴圈體執行連結串列狀態變更情況:
            //NULL<-1  2->3->4->5->NULL
            //NULL<-1<-2  3->4->5->NULL
            //NULL<-1<-2<-3  4->5->NULL
            //NULL<-1<-2<-3<-4  5->NULL
            //NULL<-1<-2<-3<-4<-5
            //迴圈體遍歷完之後,pre指向5的節點
        }
        //完成,時間複雜度為O(n)
        return pre;
    }
}

以上,就是對「 陣列與連結串列 」的一些思考。

碼字不易啊,喜歡的話不妨轉發朋友吧。