1. 程式人生 > >陣列的下標,為什麼從0開始?

陣列的下標,為什麼從0開始?

本文是學習演算法的筆記,《資料結構與演算法之美》,極客時間的課程

為什麼陣列的下標是從0開始,而不是從1開始呢?從1開始不是更符合人們的習慣麼。

這個問題,稍後回答,先聊聊陣列的基本特性。

陣列(Array)一種線性表資料結構,用一組連續的記憶體空間,儲存一組相同型別的資料

線性表(Linear List),每個線性表上最多有前後兩個方向。 陣列、佇列、連結串列、棧都是線性表結構

非線性表,如二叉樹、堆、圖等。在非線性表中,資料不是簡單的前後關係。

連續的記憶體空間和相同的資料結構,使其可以“隨機訪問”,弊端也很明顯,某些操作十分低效。在陣列中刪除或插入資料時,要做大量的資料搬移工作。

陣列如何根據下標隨機訪問陣列元素

以一個長度為10的int型陣列為例 int[] a = new int[10]
計算機給a[10]分配了一塊連續的記憶體空間1000~1039,記憶體塊的首地址 base_address = 1000
在這裡插入圖片描述
根據下標來訪問陣列元素,計算該元素的記憶體地址:用這麼一個公式
a[i]_address = base_address + i*data_type_size

本例中陣列中的元素為int型,data_type_size 大小就是4位元組。這個公式很好理解。

陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為O(1)

陣列插入操作:假設一個長度為n的陣列,如果將一個數據插入到陣列中第k個位置,需要把原陣列第k~n這部分的元素往後移一個位置。

操作的時間複雜度為O(n)。分析:假設陣列長度為n,在陣列頭部插入資料,需要把n個數據都往後移動一位,即最壞情況時間複雜度為O(n),在陣列尾部插入一個數據,不需要移動,直接插入即可,即最好情況時間複雜度為o(1)。平均情況時間複雜度如何呢?每個位置插入的概率是一樣的,都為1/n ,加權平均數為 (n + (n-1) + ……+1)*1/n 去掉係數,複雜度o(n)

如果數組裡的資料是有序的,那麼在第k個位置插入資料時,就需要把k~n的元素全部往後移一位,時間複雜度為o(n)。如果陣列僅作用於儲存資料,不關注順序,那麼在第k個位置插入一個數據時,可以先把這個資料放到陣列末尾,再把要插入的資料放到k位置上。此時時間複雜度為o(1)。

再看陣列的刪除操作。刪除陣列中的某個資料,保證記憶體空間是連續的,就需要將後面的資料移動。和插入類似,如果是在陣列末尾刪除資料,得最好情況時間複雜度,是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,我們並不真正刪除資料,只是記錄被刪除的資料。當陣列的空間不夠用的時候,才會觸發真正的刪除。這樣減少了大量的因刪除資料所造成的資料移動。
在這裡插入圖片描述
ArrayList 底層實現,有用到陣列,當陣列空間不足裡,會自動擴容到原來的1.5倍。這時就會有大量的資料移動操作。如果,我們要從資料庫裡,取出1000個數據,放到ArrayList中。那我們在建立它的時候,就指定了資料大小,就可以減少大量擴容和資料移動的效能消耗。

現在回答開篇的那個問題,陣列為什麼下標是從0開始?

從陣列中儲存的資料模型來看,下標最精確的意思是”偏移量“,a[0]的偏移量是0,即為首地址。a[i]的偏移量是i,定址公式就是a[i]_address = base_address + i*data_type_size

如果下標從1開始,那對應的定址公式a[i]_address = base_address + (i-1)*data_type_size
對CPU來說,每次隨機訪問,就多了一次運算,多發一條指令。

上面的解析,算不上壓倒性的證明。當初C語言的設計者用0開始計數陣列下標,之後java、javaScript等高階語言都仿效了C語言,這也減少了C語言程式設計師學習java的成本。