詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

小爭哥發表於2019-04-06

在大部分資料結構和演算法書籍中,陣列作為最基礎的資料型別,是最先被介紹的。一般我們都是這麼定義陣列的。

陣列這種資料結構,是儲存相同資料型別的一塊連續的儲存空間。
解讀一下這個定義的話,那就是:陣列中的資料必須是相同型別的,陣列中的資料必須是連續儲存的。
只有這樣,陣列才能實現根據下標快速地(時間複雜度是O(1))定位一個元素。

但是,如果你是一名比較喜歡鑽研的程式設計師,你會發現,在你所熟悉的程式語言中,”陣列“這種資料型別,並不一定完全符合上面的定義。比如Javascript這種語言中,陣列中的資料不一定是連續儲存的,也不一定非得是相同型別,甚至陣列可以是變長的。

var arr = new Array(4,'hello', new Date());
複製程式碼

除此之外,大部分資料結構和演算法書籍中,在講到二維或者多維陣列中資料的儲存方式的時候,一般都會這麼說:

二維陣列中的資料,是先按行再按列(或者先按列後按行),依次儲存在連續的儲存空間中。
如果二維陣列定義為a[n][m],那a[i][j]的定址公式為下面這樣(先按行後按列儲存):
address_a[i][j] = address_base + (i*m+j) * data_size;

但是,在有些程式語言中,二維陣列並不滿足上面的說法和定址公式。比如,Java中的二維陣列,第二維可以是不同長度的,而且第二維的三個陣列(arr[0]、arr[1]、arr[2])並不是連續儲存。

int arr[][] = new int[3][];
arr[0] = new int[1];
arr[1] = new int[2];
arr[2] = new int[3];
複製程式碼

是不是看的一頭霧水?難道資料結構和演算法書籍裡的講解脫離實踐?難道程式語言中的陣列沒有完全按照陣列的定義來設計?哪個對哪個錯呢?

實際上,兩個都沒錯。程式語言中的”陣列“並不完全等同於,我們在講資料結構和演算法的時候,提到的”陣列“。程式語言在實現自己的”陣列“型別的時候,並不是完全遵循資料結構”陣列“的定義,而是針對程式語言自身的特點,做了調整。

在不同的程式語言中,陣列這種資料型別的實現方式都不大相同,我就拿幾個比較典型的程式語言:C/C++、Java、Javascript,來給你展示一下,幾種比較有代表性的陣列實現方式。

1. C/C++中陣列的實現方式

C/C++中的陣列,是非常標準的資料結構中的陣列,也就是連續儲存相同型別的資料的一塊記憶體空間。在C/C++中,不管是基本型別資料,比如int、long、char,還是結構體、物件,在陣列中都是連續儲存的。我舉了一下例子,你可以看下。

int arr[3];
arr[0] = 0;
arr[1] = 1;
arr[2] = 2;
複製程式碼

陣列arr中儲存的是int基本型別的資料,對應的記憶體儲存方式,如果用畫圖的方式表示出來的話,就是下面這樣子。從圖中可以看出,資料是儲存在一片連續的記憶體空間中的。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

剛剛講的是用陣列儲存基本型別資料的例子,我們再來看:用陣列儲存struct結構體(或者class物件)的例子。

struct Dog {
  char a;
  char b;
};
struct Dog arr[3];
// 為了節省頁面,放到了一行裡了
arr[0].a = '0'; arr[0].b = '1'; 
arr[1].a = '2'; arr[1].b = '3';
arr[2].a = '4'; arr[2].b = '5';
複製程式碼

如果我們把這個結構體陣列,用畫圖的方式表示出來,就是下面這個樣子。我們發現,結構體陣列中的元素,也是儲存在一片連續的記憶體空間中的。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

剛剛講的都是一維陣列的資料儲存方式,我們再來看下,二維陣列的資料儲存方式。注意,多維陣列跟二維陣列大同小異,我們就拿二維陣列來講解。我們來看下面這段程式碼。

struct Dog {
  char a;  
  char b;
};
struct Dog arr[3][2];
複製程式碼

我們把上面的struct Dog arr[3][2]對應的資料儲存方式,用圖畫出來的話,就是下面這樣子的。從圖中,我們發現,C/C++的二維陣列,跟資料結構中二維陣列是一樣的,資料是先按行後按列,並且是連續儲存的。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

剛剛我們分析了C/C++的基本資料型別陣列、結構體或物件陣列、以及二維陣列。它們的資料儲存方式,完全符合資料結構和演算法中陣列的定義。

你還知道,在其他哪些程式語言中,陣列的定義完全符合資料結構中陣列的定義嗎?

2. Java中陣列的實現方式

看完了C/C++中的陣列,我們再來看下,Java中的陣列。Java中的陣列就有點跟資料結構中陣列的定義不一樣了。我們還是分三種情況來分析。這三種情況分別是:基本資料型別陣列、物件陣列、二維陣列(或多維陣列)。

首先,我們先來看下基本資料型別陣列,也就是說,陣列中儲存的是int、long、char等基本資料型別的資料。我們還是拿一段程式碼來舉例。

int arr[] = new int[3];
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
複製程式碼

如果我們把arr中資料在記憶體中的儲存方式,用圖畫出來的話,就是下面這個樣子的。注意,new申請的空間在堆上,arr儲存在棧上。arr儲存的是陣列空間的首地址。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

從圖中來看,在Java中,基本資料型別陣列還是符合資料結構中陣列的定義的。陣列中資料是相同型別的、並且儲存在一片連續的記憶體空間中。

看完了基本資料型別陣列,我們再來看下物件陣列,也就是說,陣列中儲存的不是int、long、char這種基本型別資料了,而是物件。我們還是拿一個例子來說明。

class Person {  
  private String name;  
  public Person(String name) {
    this.name = name;  
  }
}

Person arr[] = new Person[3];
arr[0] = new Person("0");
arr[1] = new Person("1");
arr[2] = new Person("2");
複製程式碼

在上面的程式碼中,陣列arr中儲存是Person物件。同樣,我們還是把陣列中資料在記憶體中的儲存方式,用畫圖的方式表示出來。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

從圖中,你有沒有發現,在Java中,物件陣列的儲存方式,已經跟C/C++中物件陣列的儲存方式,不大一樣了。在Java中,物件陣列中儲存的是物件在記憶體中的地址,而非物件本身。物件本身在記憶體中並不是連續儲存的,而是散落在各個地方的。

瞭解了一維陣列的儲存方式,我們再來看下,Java中的二維陣列或者多維陣列。前面也提到了,因為多維陣列跟二維陣列類似,我們還是隻拿二維陣列來講解。

Java中的二維陣列,跟資料結構中二維陣列,有很大區別。在Java中,二維陣列中的第二維,可以是不同長度的。這句話有點不好理解。我舉個例子說明一下。

int arr[][] = new int[3][];
arr[0] = new int[1];
arr[1] = new int[2];
arr[2] = new int[3];
複製程式碼

在上面的程式碼中,arr是一個二維陣列,第一維長度是3,第二維的長度各不相同:arr[0]長度是1,arr[1]長度是2,arr[2]長度是3。如果我們把這個陣列在記憶體中的儲存方式,用圖畫出來的話,就是下面這個樣子。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

剛剛這個二維陣列儲存的是基本資料型別,我們再來看下,如果二維陣列中儲存的是物件,那又會是怎麼的資料儲存方式呢?我們還是拿個例子來說明。

Person arr[][] = new Person[3][];
arr[0] = new Person[1];
arr[1] = new Person[2];
arr[2] = new Person[3];

arr[0][0] = new Person("0");
arr[1][1] = new Person("1");
複製程式碼

在上面的程式碼中,Person arr[][]是一個二維物件陣列。對於它在記憶體中儲存方式,你可以在紙上先畫下,或者在自己腦海中想下,然後,再來對比一下我畫的下面這張圖。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

我總結一下。在Java這種程式語言中,陣列這種資料型別,除了儲存基本資料型別的一維陣列之外,物件陣列、二維陣列,都跟資料結構中陣列的定義,有很大區別了。

3. JavaScript中陣列的實現方式

如果我們說,Java中的陣列,只是根據語言自己的特點,在資料結構陣列基礎之上,做的改造的話,那JavaScript這種動態指令碼語言中的陣列,完全就被改的“面目全非”了。

在開頭的時候,我們已經提到過,JavaScript中的陣列,可以儲存不同型別的資料,陣列中的資料也不一定是連續儲存的(按照下標隨機訪問的效率不高),並且還能支援變長陣列。這完全就是跟資料結構中陣列的定義反著的。如果你是一名Web前端工程師,你應該會對此很困惑吧?

實際上,JavaScript中陣列的底層實現原理,已經不是依賴資料結構中的陣列了。也就是說,JavaScript中的陣列只不過是名字叫陣列而已,跟資料結構中陣列沒啥太大關係。

接下來,我們就來看下,JavaScript中的陣列,底層是如何實現的呢?實際上,JavaScript中的陣列,會根據你儲存資料的不同,選擇不從的實現方式。

如果陣列中儲存的是相同型別的資料,那JavaScript就真的用資料結構中陣列來實現。也就是說,會分配一塊連續的記憶體空間來儲存資料。

如果陣列中儲存的是非相同型別的資料,那JavaScript就用類似雜湊表的結構來儲存資料。也就是說,資料並不是連續儲存在記憶體中的。這也是JavaScript陣列支援儲存不同型別資料的原因。

如果你往一個儲存了相同型別資料的陣列中,插入一個不同型別的資料,那JavaScript會將底層的儲存結構,從陣列變成雜湊表。

如果你熟悉JavaScript,你應該知道,JavaScript為了照顧一些底層應用的開發者,還提供了另外一種資料型別,叫做ArrayBuffer。而ArrayBuffer才符合標準的資料結構中陣列的定義。它分配一片連續的記憶體空間,僅僅用來儲存相同型別的資料。

最後,總結

資料結構和演算法先於程式語言出現。程式語言中是一些資料型別,並不能跟資料結構和演算法書籍中講到的經典資料結構,完全一一對應。比如我們今天講到的陣列,很多程式語言中,都會有陣列這種資料型別,而它們往往會根據自己語言的特點,在實現上做了調整。

歡迎留言說說,在你熟悉的語言中,陣列這種資料型別符不符合標準的資料結構中陣列的定義?或者,說一說,還有哪些資料型別,雖然名字跟資料結構中講到的一樣,但在實現上卻有很大不同呢?

關注我的微信公眾號:小爭哥,獲取更多、更新的技術、非技術分享。
作者:前Google工程師,5萬人訂閱《資料結構和演算法之美》專欄作者。
希望通過我加速你的技術、職場進步。

詳解資料結構中的“陣列”與程式語言中的“陣列”的區別和聯絡

相關文章