精衛填海系列——數組

數據結構的分類

數據結構按照數據的排列特點可以分爲線性表和非線性表。

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

在這裏插入圖片描述

非線性表:數據之間並不是簡單的前後關係。比如:樹、圖,堆等。

在這裏插入圖片描述

數組的定義

數組是一種很常見的數據結構,我們學習的沒門語言都會涉及。那麼數組又是怎樣定義的呢?

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

數組的特性

支持隨機訪問。高效的查找,低效的插入和刪除。

我們先來看插入操作。

假如數組的長度爲n,現在,如果我們需要將一個數據插入到數組中的第k個位置。爲了把第k個位置騰出來,給新的數據,我們需要將第k~n這部分的元素都順序地往後挪一位。那插入操作的時間複雜度是多少呢?

如果在數組的末尾插入元素,那就不需要移動數據了,這個時候的時間複雜度爲O(1)。但如果在數組的開頭插入元素,那所有的數據都要一次往後移動一位,所以最壞時間複雜度是O(n)。因爲每個位置插入元素的概率是一樣的,所以平均情況時間複雜度爲(1+2+…+n)/n=O(n)。

如果數組中的數據是有序的,我們在某個位置插入一個新的元素時,就必須按照剛纔的方法搬移k之後的數據。但是如果數組中存儲的數據沒有任何規律,數組就只是一個存儲數據的集合。在這種情況下,如果要將某個數據插入到第k個位置,爲了避免規模的數據搬移,我們還有一個簡單的辦法就是,直接將第k位的數據搬移到數組元素的最後,把元素直接放入第k個位置。

在這裏插入圖片描述

利用這種處理技巧,在特定場景下,在第k個位置插入一個元素的時間複雜度就會降爲O(1)。其實這個處理思想在快排中也有用到。

我們再來看刪除操作。

跟插入數據類似,如果我們要刪除第k個位置的數據,爲了內存的連續性,也需要搬移數據,不然中間就會出現空洞,內存就不連續了。

和插入類似,如果刪除數組末尾的數據,最好情況時間複雜度爲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這幾個數據會被搬移三次,我們可以先記錄下已近刪除的數據。每次的刪除操作並不是真正地搬移數據,只是記錄數據已近被刪除。當數組沒有更多空間存儲數據時,我們再觸發執行一次真正的操作,這樣就大大減少了刪除操作導致的數據搬移。

如果你瞭解JVM,你會發現,這不就是JVM標記清除垃圾回收算法的核心思想嗎?沒錯,數據結構和算法的魅力就在於此,很多時候我們並不是要去死記硬背某個數據結構或者算法,而是要去學習它背後的思想和處理技巧,這些東西纔是最有價值的。如果你細心留意,不管是在軟件開發還是架構設計中,總能找到某些算法和數據結構的影子。

這個思想倒是不難理解,但是如果去實現呢?這是個問題,博主需要好好思考一番了。

面試中一個小錯誤

在面試的時候,面試官經常會問我們。數組和鏈表的區別,而我們往往會回答:鏈表適合插入、刪除操作,時間複雜度是O(1);數組適合查找操作,時間複雜度是O(1)。

實際上這種回答是不夠準確的。數組是適合查找操作,但是查找的時間複雜度並不爲O(1)。即便是排好序的數組,你用二分查找,時間複雜度也是O(logn)。所以,準確的表達應該是,數組支持隨機訪問,根據下標隨機訪問的時間複雜度是O(1)。

爲什麼數組的下標是從0開始而不是1呢?

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

a[k]_address=base_address+k*data_type_size;

其中data_type_size表示數組中的每個元素的大小。從數組存儲的內存模型上來看,“下標”最確切的定義應該是“偏移(offset)”。前面也講到,如果a來表示數組的首地址,a[0]就是偏移量爲0的位置,也就是首地址,a[k]就表示偏移k個type_size的位置,所以計算a[k]的內存地址只需要用上面的公式。但是,如果數組從1開始計算,那麼我們計算數組元素a[k]的內存地址就會變成:

a[k]_address=base_address+(k-1)*data_type_size;

對比兩個公式,我們不難發現,從1開始編號,每次隨機訪問數組元素都多了一次減法運算,對於CPU來說,就是多了一次減法指令。

數組作爲非常基礎的數據結構,通過下標隨機訪問數組元素又是其非常基礎的編輯操作,效率的優化就要儘可能做到極致。所以爲了減少一次減法操作,數組選擇了從0開始編號,而不是從1開始。

不過也可能是另一個原因,C語言設計者用0開始計數數組下標,之後的java、JavaScript等高級語言都效仿了C語言,或者說,爲了在一定程度上減少C語言程序猿學習Java的成本,因此繼續沿用了從0開始計數的習慣。

數組越界

不同語言對數組越界的處理是不一樣的,java會報出ArrayIndexOutOfBoundsException的異常,自動幫你檢查;c則不會,需要程序猿來檢查,所以需要注意。

容器能否完全替代數組

容器就是集合。針對數組類型,很多語言都提供了容器類,比如java中的ArrayList、C++ STL中的vector。那麼在項目開發中,什麼時候用數組,什麼時候用容器呢?

1、java ArrayList無法存儲基本類型,比如int、long,需要封裝Integer、Long類,而Autoboxing、Unboxing則有一定的性能消耗,所以如果特別關注性能,或者希望使用基本類型,就可以選擇數組。

2、如果數據大小事先已知,並且對數據的操作非常簡單,用不到ArrayList提供的大部分方法,也可以直接使用數組。

3、當要表示多維數組時,用數組往往會更加直觀。比如Object[][] array;而用容器的話則需要這樣定義:ArrayList<ArrayList>array。

總結:對於業務開發,直接使用容器就足夠了,省時省力。畢竟損耗一丟丟性能,完全不會影響到系統整體的性能。但如果你是做非常底層的開發,比如開發網絡科技,性能的優化需要做到極致,這個時候數組就會優於容器,成爲首選。

學習了王爭老師的《數據結構與算法之美》,根據課程內容整理的筆記

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