爲什麼很多編程語言中數組都是從 0 開始編號?

1、什麼是數組?

數組(Array)是一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據。

概念解析:

      線性表:線性表就是數據排成像一條線一樣的結構。每個線性表上的數據最多隻有前和後兩個方向。其實除了數組,鏈表、隊列、棧等也是線性表結構。

      連續的內存空間和相同類型的數據:所以數組根據下標具有隨機訪問特性,這兩個限制也讓數組的很多操作變得非常低效,比如要想在數組中刪除、插入一個數據,爲了保證連續性,就需要做大量的數據搬移工作。

 

2、數組是如何實現根據下標隨機訪問數組元素的嗎?

舉例:長度爲 10 的 int 類型的數組 int[] a = new int[10]來舉例。在我畫的這個圖中,計算機給數組分配了一塊連續內存空間 1000~1039,其中,內存塊的首地址爲 base_address = 1000。

 計算機會給每個內存單元分配一個地址,計算機通過地址來訪問內存中的數據。當計算機需要隨機訪問數組中的某個元素時,首先通過下面的尋址公式,計算出該元素存儲的內存地址:

1 a[i]_address = base_address + i * data_type_size
2 
3 data_type_size 表示數組中每個元素的大小。我們舉的這個例子裏,數組中存儲的是 int 類型數據,所以 data_type_size 就爲 4 個字節。

面試常見問題:數組和鏈表的區別?

“鏈表適合插入、刪除,時間複雜度 O(1);數組的查找操作時間複雜度並不是O(1)。即便是排好的數組,用二分查找,時間複雜度也是O(logn)。所以應該這麼說:數組支持隨機訪問,根據下標隨機訪問的時間複雜度爲 O(1)。

 

3、數組低效的“插入”和“刪除”

低效原因:

數組爲了保持內存數據的連續性,會導致插入、刪除這兩個操作比較低效,因爲需要搬移數據。
插入:
假設數組長度爲n,要將一個數據插入到第 k 個位置,爲了把第 k 個位置騰出來,我們需要將k~n這部分元素順序的向後移動一位。
插入的時間複雜度:
最好情況:在數組的末尾插入元素,不需要移動數據了,時間複雜度爲O(1)。
最壞情況:在開頭插入元素,那就需要把所有的數據向後移動一位,時間複雜度爲O(n)。
平均時間複雜度:(1+2+…+n)/n = O(n)。

 刪除:和插入類似,如果刪除數組末尾的數據,則最好情況時間複雜度爲O(1),如果要刪除開頭數據,則最壞情況時間複雜度爲O(n),平均時間複雜度爲O(n)。

 改進方法:

插入:

如果數組中的元素沒有任何規律,數組只是被當作一個數據集合,在這種情況下,如果要將某個數據插入到第k個位置,爲了避免大規模的數據遷移,一個簡單的辦法就是直接將現在第k個元素放到最後,
把新元素放進來。 例如:arr[10] = {a,b,c,d,e}要將x插入第三個位置arr[10]={a,b,x,d,e,c} 這樣插入的時間複雜度爲O(1),這種方法在快速排序中也會用到。 刪除:
在某些特殊情況下,我們並不一定非得追求數組中數據的連續性,如果我們將多次刪除操作放在一起執行,效率會高很多。 舉個例子:a[10]={a,b,c,d,e,f,g,h} 如果我們要依次刪除abc三個元素,需要搬移三次後面的數據,爲了避免這個重複的搬移工作,可以先記錄下來已經刪除的數據,
每次的刪除操作並不是真正的搬移數據,只是記錄數據已經被刪除,當數組中沒有更多的空間存儲數據時,我們再觸發執行一次真正的刪除操作,這樣就大大減少了搬移工作,
這也是標記清除垃圾回收算法的核心思想。

 

4、 數組越界問題

首先看一段C語言代碼:

1 int main(int argc, char* argv[]){
2     int i = 0;
3     int arr[3] = {0};
4     for(; i<=3; i++){
5         arr[i] = 0;
6         printf("hello world\n");
7     }
8     return 0;
9 }

這段代碼的結果並不是打印三行hello world,而是無限打印hello world。爲啥呢?

因爲在C語言中,除了受限制的內存,其他所有內存空間都是可以自由訪問的。a[3]也會被定位到某塊不屬於數組的內存地址上,而這個地址正好是存儲變量 i 的內存地址,那麼 a[3]=0 就相當於 i=0,所以就會導致代碼無限循環。

那爲什麼會無限打印呢?

根據我所學和百度的知識解釋下:函數體內的局部變量存在棧區,在Linux內存佈局中,棧區在高地址空間,從高到低增長,先int i = 0;再int arr[3]={0};變量i和arr地址相鄰,並且i地址比arr地址大,首先壓棧的i,a[2],a[1],a[0],循環中arr訪問越界正好到i,而此時i變量的地址是數組當前進程的,所以進行修改的時候,操作系統並不會終止進程。當然這只是32位操作系統下,64位操作系統下 默認會進行8字節對齊 變量i的地址就不緊跟着數組後面了。另外這個還和編譯環境有關,對於不同的編譯器,在內存分配時,會按照內存地址遞增或遞減的方式進行分配。如果是內存地址遞減的方式,就會造成無限循環。

5、容器和數組

相比於數組,Java中的ArrayList封裝了數組的很多操作,並支持動態擴容,每次存儲空間不夠的時候,它都會將空間自動擴容爲 1.5 倍大小。但是一旦超過存儲容量,擴容時比較耗時,因爲涉及到內存申請和數據搬移。

所以,如果事先能確定需要存儲的數據大小,最好在創建 ArrayList 的時候事先指定數據大小。比如我們要從數據庫中取出 10000 條數據放入 ArrayList。我們看下面這幾行代碼,你會發現,相比之下,事先指定數據大小可以省掉很多次內存申請和數據搬移操作。

ArrayList<User> users = new ArrayList(10000);
for (int i = 0; i < 10000; ++i) {
  users.add(xxx);
}

數組適合的場景:

      1) Java ArrayList 的使用涉及裝箱拆箱,有一定的性能損耗,如果特別關注性能,可以考慮數組;
  2) 若數據大小事先已知,並且涉及的數據操作非常簡單,可以使用數組;
  3) 表示多維數組時,數組往往更加直觀;
  4) 業務開發使用容器即可。若涉及底層開發,如網絡框架,性能優化等,則最好選擇數組。

 

6、解答開篇問題

   1、 效率原因:

從內存模型來看,“下標”也稱爲“偏移”。我們知道在C語言中數組名代表首地址(第一個元素的地址),a[0]就是偏移爲 0 的位置。a[k]就表示偏移 k 個元素類型大小的位置。得出計算公式:

a[k]_address = base_address + k * type_size
但是鑰匙從
1 開始計數,那這個公式就會變爲:
a[k]_address
= base_address + (k-1) * type_size 對比兩個公式,如果從 1 開始編號,每次隨機訪問數組元素就多了一次減法運算,對於CPU來說就是多了一次減法指令。 數組作爲非常基礎的數據結構,通過下標訪問數組元素又是數組上的基礎操作,效率優化應做的很好,所以爲了減少一次減法操作,數組選擇了從 0 開始編號。

 2、歷史原因:

C語言的設計者用 0 開始計數下標,之後的Java、C++等高級語言都效仿C語言,沿用了從0開始計數的習慣。

還有一些語言並不是從0開始計數的,如:Matlab。 甚至Python還支持負數下標。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章