1. 程式人生 > >資料結構與演算法之美(三):陣列

資料結構與演算法之美(三):陣列

陣列看起來簡單基礎,但是很多人沒有理解這個資料結構的精髓。帶著為什麼陣列要從0開始編號,而不是從1開始的問題,進入主題。

一、 如何實現隨機訪問

1) 陣列是一種線性資料結構,用連續的儲存空間儲存相同型別資料: I) 線性表:陣列、連結串列、佇列、棧 ;非線性表:樹、圖 II) 連續的記憶體空間、相同的資料,所以陣列可以隨機訪問,但對有序陣列進行刪除、插入,為了保持陣列的有序性, 就要做大量的資料搬移工作。

a) 陣列如何實現下標隨機訪問。 陣列記憶體分佈圖 引入陣列在記憶體中的分配圖,得出定址公式:a[i]_address = base_address + i * data_type_size

b) 糾正陣列和連結串列的錯誤認識。陣列的查詢操作時間複雜度並不是O(1)。即便是排好的陣列,用二分查詢,時間複雜度也是O(logn)。 正確表述:陣列支援隨機訪問,根據下標隨機訪問的時間複雜度為O(1)

二、低效的插入和刪除

1) 有序插入:從最好O(1) 最壞O(n) 平均O(n); 2) 無序插入:陣列若無序,插入新的元素時,可以將第K個位置元素移動到陣列末尾,把心的元素,插入到第k個位置,此處複雜度為O(1); 3) 有序刪除:從最好O(1) 最壞O(n) 平均O(n); 4) 多次刪除集中在一起,提高刪除效率: 記錄下已經被刪除的資料,每次的刪除操作並不是搬移資料,只是記錄資料已經被刪除,當陣列沒有更多的儲存空間時,再觸發一次真正的刪除操作。即JVM標記清除垃圾回收演算法。

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

用C語言迴圈越界訪問的例子說明訪問越界的bug:

int main(int argc, char* argv[]){
    int i = 0;
    int arr[3] = {0};
    for(; i<=3; i++){
        arr[i] = 0;
        printf("hello world\n");
    }
    return 0;
}

如果用來編譯這段程式的編譯器按照記憶體地址遞減的方式給變數分配記憶體,那麼記憶體中的i將會被置為0,則為死迴圈永遠出不去。

四、容器能否完全替代陣列

針對陣列型別,很多程式語言都提供了容器類,比如C++ STL 的vector,java 的ArrayList,容器最大的優勢是將陣列的操作封裝起來了,比如陣列的刪除和插入操作,還有就是容器支援動態擴容。當然陣列也有其適合使用的場景。 陣列適合的場景: 1) Java ArrayList 的使用涉及裝箱拆箱,有一定的效能損耗,如果特別在乎效能,可以考慮陣列; 2) 若資料大小事先已知,並且涉及的資料操作非常簡單,可以使用陣列; 3) 表示多維陣列時,陣列往往更加直觀; 4) 業務開發容器即可。底層開發,如網路框架,效能優化等,選擇陣列。

五、解答陣列下標為什麼從0開始

從0開始編號,陣列元素的定址公式是: a[k]_address = base_address + k * type_size

從1開始編號,陣列元素的定址公式是: a[k]_address = base_address + (k-1)* type_size

從定址公式上來看,從一開始編號,每次定址計算都增加了一個k-1的運算,對於CPU來說就是多了一次減法指令,陣列作為基礎的資料結構,效率肯定要做到極致,所以陣列選擇了從0開始編號。當然上面說的也不一定是壓倒性證明,也可能有一定的歷史原因。