數組與鏈表的應用—數組內存模型

在計算機裏,全部的數據結構本質上能夠歸爲兩類:數組和鏈表java

數組的內存模型面試

1.一維數組數組

  什麼是數組?緩存

  在計算機科學中,數組能夠被定義爲是一組被保存在存儲連續空間中,而且具備相同類型的數據元素的集合。而數組中的每個元素均可以經過索引來進行訪問。數據結構

  例:以java語言中一個例子說明一下數組的內存模型,當定義了一個擁有5個元素的int數組後,看看內存是長什麼樣子?dom

  int[] data = new int[5];函數

  經過上面的聲音,計算機會在內存中分配一段連續的空間給這個data數組。如今假設在一個32位上的機器上運行這段程序,java的int類型的數據佔據了4個字節的空間,同時也假設計算機分配的地址是從0X80000000開始的,整個data數組在計算機內存中分配的模型以下圖所示:學習

                                      

   這種分配連續空間的內存模型同時也揭示了數組在數據結構中的另一個特性,即隨機訪問(Random Access),隨機訪問這個概念在計算機科學中被定義爲:能夠用同等的時間訪問到一組數據中的任意一個元素。這個特性除了和連續的內存空間模型有關之外,其實也和數組如何經過索引訪問到特定的元素有關。this

  在計算機中,爲何在訪問數組中的第一個元素時,程序通常都是表達成如下這樣的:編碼

  data[0]

  也就是說,數組的第一個元素是經過索引「0」來進行訪問的,第二個元素是經過索引「1」來進行訪問的,......,這種從0開始進行索引的編碼的方式被稱爲「Zero-based Indexing」。固然了在計算機世界中,也存在着其餘的編碼方式,像Visual Basic中的某些函數索引採用1-based Indexing的,也就是說第一個元素是經過索引「1」來獲取的,像這種方式就很少說了。等之後有時間慢慢研究。

  爲何數組的第一個元素要用過索引「0」來進行訪問呢?緣由就在於獲取數組元素的方式是按如下的公式來進行獲取的:

  base_address + index(索引) * data_size(數據類型大小)

  索引在這裏能夠看作是一個偏移量(Offset),仍是以上面的例子來進行說明:

                                                                  

  data這個數組被分配到的起始地址是0X80000000,是由於int類型數據佔據了4個字節的空間,若是咱們要訪問第五個元素data[4]的時候,按照上面的公式,只須要取得0X80000000 + 4 * 4 = 0X80000010這個地址的內容就能夠了。隨機訪問的背後原理其實也就是利用這個公式達到了同等的時間訪問到一組數據中的任意元素。

 

2.二維數組

  上面所提到的數組是屬於一維數組的範疇,咱們平時可能還會聽到一維數組的其餘叫法,例如向量(Vector)或者表(Table)。由於在數學上,二維數組能夠很好的用來表達矩陣(Matrix)這個概念,因此不少時候咱們又會將矩陣或者二維數組這種稱呼交替使用。

  若是咱們按照下面的方式聲明一個二維數組:

  int[][] data = new int[2][3];

  在面試中咱們知道了數組的起始地址,在基於上面的二維數組聲明的前提下,data[0][1] 這個元素的內存地址是多少呢?標準答案實際上是「沒法肯定」,什麼?標準答案是沒法肯定,彆着急,由於這個問題的答案其實和二維數組在內存中的尋址方式有關。而這其實涉及到計算機內存究竟是以行優先(Row-Major Order)仍是以列優先(Column-Major Order)存儲的。

  假設如今有一個二維數組,以下圖所示:

                   

  下面咱們就這看看行優先或列優先形成的內存模型會形成什麼樣的區別:   

  (1)行優先

    行優先的內存模型保證的每一行的每一個相鄰元素都保存在了相鄰的連續空間中,對於上面的例子,這個內存模型以下圖所示,假設起止地址是0X80000000:

                                                                                

    能夠看到,在二維數組的每一行中,每一個相鄰的元素都保存在了相鄰的連續內存裏。

    在以行優先存儲的內存模型中,假設咱們要訪問data[i][j]裏的元素,獲取數組的方式是按照如下公式進行獲取的:

    base_address + data_size * (i * number_of_column + j)

    回到一開始的問題裏,當咱們訪問data[0][1]這個值時,能夠套用上面的公式,其獲得的值就是咱們要找的0X80000004地址的值,也就是2。

    0x80000000 + 4 x (0 x 3 + 1) =  0x80000004

                              

   (2)列優先

    列優先的內存模型保證了每一列的每一個相鄰元素都保存在了相鄰的連續內存中,對於上面的例子,這個二維數組的內存模型以下圖所示:

                          

    能夠看到,在二維數組的每一列中,每一個相鄰的元素都保存在了相鄰的連續的內存中。

    在以列優先存儲的內存模型中,假設咱們要訪問data[i][j]裏的元素,獲取數組元素的方式是按照一下公式獲取的:

    base_address + data_size * (i + number_of_row * j)

    當咱們訪問data[0][1]這個值時,能夠套用上面的公式,其獲得的值就是咱們要找的0x80000008地址的值:

    0x80000000 + 4 * (0 + 2 * 1) = 0x80000008

                  

    因此回到一開始那個問題裏,行優先仍是列優先存儲方式會形成data[0][1]元素的內存地址不同。

3.多維數組

  多維數組其實本質上和前面介紹的一維數組和二維數組是同樣的,若是咱們按照下面的方式來聲明一個三位數組:

  int[][][] data = new int[2][3][4]; 

  則能夠把這個數組想象成兩個int[3][4]這樣的二維數組,對於多維數組則能夠以此類推,下面把行優先和列優先的內存尋址方式列出來:

  假設聲明一個data[S1][S2][S3]...[Sn]的多維數組,若是要訪問data[D1][D2][D3]...[Dn]的元素,內存尋址計算方式按照以下方式尋址:

  行優先:

  base_address + data_size * (Dn + Sn * (Dn - 1 + Sn - 1 * (Dn - 2 + Sn - 2 * (... + S2 * D1 )... )))

  列優先:

  base_address + data_size * (D1 + (S1 * (D2 + S2 * (D3 + S3 * (... + Sn - 1 * Dn)...))))

  cpu在讀取內存數據的時後,一般會有一個cpu緩存策略,也就是說再cpu讀取程序指定地址的數值時,cpu會把它地址相鄰的一些數據一併讀取,並放到更高一級的緩存中,好比L1或者L2緩存。當數據存放到這種緩存上的時候,讀取的速度有可能會比直接從內存上讀取的速度快10倍以上。

  在高級語言中經常使用的C/C++和Objective-C都是行優先的內存模型,而Fortran或者Matlab是列優先的內存模型。

「高效」的訪問與「低效」的插入刪除

  從前面的的數組內存模型學習中,咱們知道了訪問一個數組的元素是隨機訪問方式,只須要按照上面講到的尋址方式來獲取相應位置的數值即可,因此訪問數組元素的複雜度是O(1)。

  對於保存基本類型(Primitive Type)數組來講,它們的內存大小在一開始就已經肯定好了,咱們稱他爲靜態數組(Static Array)。靜態數組的大小是沒法改變的,因此咱們沒法對這種數組進行插入和刪除操做。可是在使用高級語言的時候,好比java,咱們知道java中的ArrayList這種Collection提供了像add和remove這樣的API來進行插入和刪除操做,這種數組可稱之爲動態數組(Dynamic Array)。

  咱們一塊兒來看看add和remove函數在java Open-jdk11中的源碼,一塊兒分析他們的時間複雜度:

  在java Connection中,底層的數據結構其實仍是使用的數組,通常在初始化的時候會分配一個比咱們在初始化時設定好的大小更大的空間,以方便之後進行增長元素的操做。

  假設全部的元素都保存在elementData[]這個數組中,add函數的主要時間複雜度來源於如下源碼片斷:

1.add(int index,E element)函數源碼:

  首先來看看add(int index,E element)這個函數的源碼: 

public void add(int index,E element){          
    rangeCheckForAdd(index);
    modCount++;
    final int s;
    Object[] elementData;
    if((s = size) == (elementData = this.element).length){
       elementData = grow();
    }
    System.arraycopy(elementData,index,elementData,index +1,s - index);
    elementData[index] = element;
    size = s + 1;   
}        

  能夠看到add函數調用了一個System.arraycopy的函數進行內存操做,s在這裏表明了ArrayList的size,當咱們調用add函數的時候,函數在實現的過程當中到底發生了什麼?咱們來看一個例子。

  假設elementData裏面存放着如下元素:

                             

  當咱們調用的add(1,4)函數,也就是在index爲1的地方插入4這個元素,在add函數中則會執行System.arraycopy(elementData,1,elementData,2,6 - 2)語句,它的意思是將重elementData數組index爲1的地址開始,複製日後的4個元素到elementData數組爲2的地址位置,以下圖所示:

                                                               

  紅色部分表明執行完System.arraycopy函數的結果,最後執行elementData[1] = 4;這條語句:

                                                              

  由於這裏涉及到每一個元素的複製,平均下來的時間複雜度至關於O(n)。

2.remove(int index)函數源碼:

  

 1 public E remove(int index){
 2  Objects.checkIndex(index,size); 3 final Object[] es = elementData; 4 5 @SuppressWarnings("unchecked") E oldValue = (E) es[index]; 6  fastRemove(es,index); 7 8 return oldValue; 9 } 10 11 private void fastRemove(Object[] es,int i){ 12 modCount++; 13 final int newSize; 14 if((newSize = size -1) > i){ 15 System.arraycopy(es,i+1,es,i,newSize - i); 16  } 17 es[size = newSize] = null; 18 }

  這裏的newSize指原來的elementData的size - 1,當咱們調用remove(1)會發生什麼呢?咱們仍是如下面的例子來解釋。

                                                              

  若是調用remove(1)函數,也就是刪除在index爲1這個地方的元素,在remove函數中則會執行System.arraycopy(elementData,2,elementData,1,2)語句,它的意思是將從elementData數組index爲2的地址開始,複製後面的兩個元素到elementData數組到index爲1的地址位置,以下圖所示:

                                                             

  由於這裏一樣涉及到每一個元素的複製,平均下來時間複雜度至關於O(n)。

 

心得:

  這是我學數據結構的第一節課內容,由於基礎太薄弱,看完視頻後,感受老師在講的時候什麼都明白,而後回來再看老師的筆記仍是一頭霧水,因而乎就把老師的筆記一個字一個字的打入了博客當中,這些除了圖片以外其餘徹底是手打的,只爲加強記憶力和理解力,打完了以後對裏面的內容掌握率感受仍是不高,我會繼續學習,把我所學到的知識所有寫入個人博客中,供你們學習和交流。(根據蔡元楠老師講解的數據結構精講整理此筆記)

相關文章
相關標籤/搜索