1. 程式人生 > >資料結構與演算法之陣列篇

資料結構與演算法之陣列篇

Q1:為什麼很多程式語言中,陣列都從0開始編號?

     舉例說明:

     從陣列儲存的記憶體模型上來看,"下標"最確切的定義應該是"偏移(offset)"。前面也講到,如果用a來表示陣列的首地址,a[0]就是偏移為0的位置,也就是首地址,a[k]就表示k個type_size的位置,所以計算a[k]的記憶體地址只需要用這個公式:

    但是,如果陣列從1開始計數,那我們計算陣列元素a[k]的記憶體地址就會變為:

    對比這兩個公式我們,可以發現,從1開始編號,每次隨機訪問陣列元素都多了一次減法運算,對於CPU來說,就是多了一次減法指令。

      由上述例子我們可知:

      (1)、陣列作為非常基礎的資料結構,通過下標隨機訪問陣列元素又是非常基礎的程式設計操作,效率的優化就要可能做到極致。所以為了減少一次減去的操作,陣列選擇了從0開始編號,而不是從1開始。

     (2)、歷史原因;C語言的設計者用0開始計數陣列下標,之後的語言都效仿了C語言;

2、Q2:如何實現隨機訪問?

      首先解釋一下陣列:陣列是一種線性表資料結構;

      (1)、線性表(Linear List)。顧名思義,線性表就是資料排成像一條線一樣的結構。每個線性表上的資料最多隻有前和後兩個方向。其實除了陣列,連結串列、佇列、棧等也是線性表結構;

 

      與之相對應的是非線性表;比如二叉樹、堆、圖等;之所以叫非線性,是因為在非線性表中,資料之間並不是簡單的前後關係。

 

 

    (2)、連續的空間儲存和相同型別的資料。正是因為這兩個限制,它才有一個堪稱"殺手鐗"的特性:"隨機訪問"。但是因為這個特性,限制了陣列的很多操作變得非常低效。

3、陣列是如何實現根據下標隨機訪問陣列元素

       舉例說明:

     拿一個長度為10的int型別的陣列int[]a=new int[10]來舉例。在下圖中,計算機給陣列a[10],分配了一塊連續記憶體空間1000~1039,其中,記憶體塊的首地址為base_address=1000。

 

 

     我們知道,計算機會給每一個記憶體單元分派一個地址,計算機通過地址來訪問記憶體中的資料。當計算機需要隨機訪問陣列中的每個元素時,它會首先通過下面的定址公式,計算出該元素儲存的記憶體地址:

     注:data_type表示每個元素的大小。 

    *低效的"插入"和"刪除"

前面的概念部分我們提到,陣列為了保持記憶體資料的連續性,會導致插入、刪除這兩個操作比較低效。

接下來詳細說一下:

(1)、插入操作假設陣列的長度為N,現在,如果我們需要將一個數據插入到陣列中的第K個位置。為了把第K個位置騰出來,給新來的資料,我們需要將第K~n這部分的元素都順序地往後挪一位。那插入操作的時間複雜度是多少呢?

分析:如果在陣列的末尾插入元素,那就不需要移動資料了,這時的時間複雜度為0(1)。但如果在陣列的開頭插入元素,那麼所有的資料都需要依次往後移一位,所以最壞時間複雜度O(n)。因為我們在每個位置插入元素的概率是一樣的,所以平均情況時間複雜度為(1+2+…….+n)/n=O(n).

 如果陣列中的資料都是有序的,我們在某個位置插入一個新的元素時,就必須按照剛才的方法搬移K之後的資料。但是,如果陣列中儲存的資料並沒有任何規律,陣列只是被當成作一個數據的集合。在這種情況,如果要將某個陣列插入到第K個位置,為了避免大規模的資料搬遷,我們還有一個簡單的方法就是,直接將第K位的資料搬到陣列的最後,把新的元素直接放到第K個位置。

為了更好地理解,舉例說明,假設陣列a[10]中儲存瞭如下5個元素:a,b,c,d,e。

我們現在需要將元素X插入到第三個位置。我們只需要將C放入到a[5],再將a[2]賦值為即可。

最後,陣列中的元素如下:a,b,c,d,e,c。

 

(2)、刪除操作

跟插入類似,如果我們要刪除第k個位置的資料,為了記憶體的連續性,也需要搬移資料,不然中間就會出現空洞,記憶體就不連續了。

如果刪除末尾的資料,則最好情況時間複雜度為O(1);如果刪除開頭的資料,則最壞時間複雜度為O(n);平均時間複雜度也為O(n)。

實際上,在某些特殊場景下,我們並不一定追求陣列中資料的連續性。如果我們將多次刪除操作集中在一起執行,刪除的效率會得到提高。

我們將繼續來看例子,陣列a[10]中儲存了8個元素:a,b,c,d,e,f,g,h。

現在,我們要依次刪除a,b,c三個元素。

 

為了避免 d,e,f,g,h這幾個資料會被搬移三次,我們可以先記錄下已經刪除的資料。每次的刪除操作並不是真正地搬移資料,只是記錄資料已經被刪除。當陣列沒有更多空間儲存資料時,我們再觸發執行一次真正的刪除操作,這樣就大大減少了刪除操作導致的資料搬移。

4、警惕陣列的訪問越界問題

     舉例說明:

這段程式碼的執行結果並非是列印三行"hello word",而是會無限列印"hello world",這是為什麼呢?

因為,陣列大小為3,a[0],a[1],a[2],而我們的程式碼因為內書寫錯誤,導致for迴圈的結束條件錯寫為了i<3,所以當i=3時,陣列a[3]訪問越界。

5、容器能否完全替代陣列?

     針對陣列型別的,很多語言都提供了容器類,比如Java中的ArrayList、C++STL中的vector,在專案開發中,什麼時候合適用陣列,什麼時候用容器?

容器的最大優勢就是可以將很多陣列的操作細節封裝起來,還有一個優勢就是支援動態擴容。

作為高階語言程式設計者,是不是陣列就無用武之地呢?當然不是,有些時候,用陣列更合適些,以下是幾點總結。

    (1)、若果特別關注效能,或者希望使用基本型別,就可以選用陣列。

    (2)、如果資料大小事先已知,並且對資料的操作非常簡單,用不到容器提供的大部分方法,也可以直接使用陣列。

    (3)、當表示多維陣列時,用陣列往往會更加直觀。比如Object[][],而用容器表示的話則需要這樣定義:ArrayList<ArrayList>array;


謝謝大家關注我的微信公眾號:

                                                                             Change,There is no better way!