一文了解数组
2019 年第 75 篇文章,總第 99 篇文章
”數據結構算法入門系列的第二篇,這次介紹下數組, 數組是一個最基礎而且常見的數據結構,幾乎每種編程語言都有。
上一篇文章:
數據結構算法入門--一文了解什么是復雜度
今日推薦閱讀:
深度學習在推薦系統中的應用
如何實現隨機訪問
數組的定義:
?數組(Array)是一種線性表數據結構。它用一組連續的內存空間,來存儲一組具有相同類型的數據。
”這里指出了數組的三個特點:
線性表(Linear List)
連續的內存空間
存儲相同類型數據
首先,線性表就是數據排成一條線一樣的結構,每個線性表最多只有前后兩個方向,線性表結構如下圖所示,數組、鏈表、隊列和棧都屬于這種結構。
和線性表對立的就是非線性表,比如二叉樹、堆、圖等,它們不僅僅是簡單的前后關系,如下圖所示:
接著就是需要連續內存空間和保存相同類型的數據。這兩個特點讓數組有一個非常好的特性:隨機訪問。也就是根據下標訪問數組的時間復雜度是 O(1) ,但問題就是插入和刪除需要 O(n),因為需要進行大量的數據移動操作。
那么數組是如何實現隨機訪問的操作的呢?
首先,給定一個長度為 10 的 int 類型的數組 a[10] ,計算機會分配一塊存儲空間,這里假設就是 1000~1039 ,其中內存塊的首地址是 base_address=1000,如下圖所示
計算機給每個內存單元分配一個地址,然后通過地址訪問內存中的數據。因此,這里如果需要隨機訪問數組的某個元素,同樣也是根據地址訪問,也就是首先需要找到該元素存儲所在的地址,尋址公式如下所示:
a[i]_address = base_address + i * data_type_sizedata_type_size 表示數組每個元素的大小,這里 int 類型是 4 個字節。
注意,對于數組來說,它支持隨機訪問,根據下標隨機訪問的時間復雜度是 O(1) ,但查找時間復雜度并非是 O(1) , 因為即便排好序的數值,通過二分查找,時間復雜度是 O(logn) 。
低效的插入和刪除
數組的插入和刪除操作由于其內存數據的連續性問題,這兩個操作會非常低效,那么為什么會導致低效,有哪些改進方法呢?
插入操作
假設數組長度是 n ,現在需要將一個數據插入到數組的第 k 位置,如果需要執行這樣的操作,需要將第 k 到 n 位置上的元素都按順序往后移動一位。這個操作的時間復雜度是多少呢?
最好的情況,就是末尾插入元素,這樣不需要移動,O(1) 復雜度;最壞的情況,數組開頭就插入元素,那么就是 O(n) 的時間復雜度。平均情況時間復雜度則如下所示,每個位置插入元素概率是相同的:
當然,如果數組是有序的,就需要按照上述做法移動元素。如果數組無序呢,一個快速的方法就是僅移動目標位置的元素,即第 k 個位置的元素放到數組末尾,然后插入元素即可,這樣時間復雜度就是 O(1)。
一個簡單的例子如下圖所示,數組有 5 個元素:a,b,c,d,e,現在希望在第三個位置插入新元素 x,此時可以直接將 c 放到末尾,即 a[5] = a[2],然后 a[2]=x,即可完成操作。
這種特殊的處理技巧,可以在特定場景下(比如數組無序)將插入元素的時間復雜度降到 O(1)。
刪除操作
和插入數據類似,刪除第 k 個位置元素,同樣需要將后續的元素往前移動。
最好的情況是刪除末尾數據,O(1);
最壞就是刪除開頭的元素,O(n);
平均情況時間復雜度也是 O(n)。
同樣在某些特定場景下,并不需要時刻追求數組中數組的連續性,可以將多次刪除操作集中在一起進行操作。
如下圖所示是一個長度為 10 的數組,存儲了 8 個元素,`現在是需要依次刪除前三個元素,a,b,c。如果每次刪除操作都將所有元素往前移動,那么后續的 5 個元素總共需要移動 3 次,為了避免這個情況,我們可以先記錄被刪除的數據,每次操作僅僅記錄那個位置的元素被刪除。當數組沒有空間存儲數據時,再進行一次真正的刪除操作,這樣可以避免刪除操作導致的數據搬移。
這個做法其實就是 Java 中 JVM 標記清除垃圾回收算法的核心思想。
所以,我們對于數據結構和算法的學習,不應該只是死記硬背,而是需要了解它背后的思想和處理技巧,明白為什么需要這種數據結構和這種算法。
數組的越界問題
首先來看一段 C 語言的代碼,如下所示:
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; }這段代碼的問題就是打印結果的時候,因為循環結束條件問題,會無限打印 "hello world",給定的數組長度是 3, 但是循環結束條件是 i<=3 ,而 a[3] 其實就是訪問越界了。
但是,在 C 語言中,只要不是訪問受限的內存,所有的內存空間都是可以自由訪問的。也就是說,a[3] 也是可以訪問的,但是會定位到非數組所在的內存上,而這個地址正好是存儲變量 i 的內存地址,也就是 a[3]=0 就相當于 i=0 ,最終導致代碼的無限循環。
數組越界在 C 語言中是一種未決行為,沒有規定這種情況編譯器應該如何處理,所以通常會出現各種奇怪的邏輯錯誤。
不過,其他編程語言并不會將數組越界的工作丟給程序員來做,它們會有做越界的檢查。
數組索引從 0 開始的原因
大多數的編程語言中,數組,或者說數據結構,索引都是從 0 開始,而不是從 1 開始。
首先,從數組存儲的內存模型上看,索引,或者說“下標”最確切的定義應該是偏移(offset)。
前面介紹了數組的尋址公式:
a[i]_address = base_address + i * data_type_size如果數組是從 1 開始計數,那么尋址公式就會為:
a[i]_address = base_address + (i-1) * data_type_size對比兩個公式,可以知道每次訪問數組元素,從 1 開始計數的方式會多一次減法運算,相當于讓 CPU 多一次減法指令,但隨即訪問數組元素應該是非常基礎的操作,需要盡可能高效,因此,為了減少一次減法操作,數組采用從 0 開始計數,而非從 1 開始。
其次,主要是 C 語言設計者用 0 開始計數,后續的編程語言,如 Java 等都效仿了 C 語言,這也是方便 C 語言程序員學習其他語言的學習成本。當然,像 Python 還支持負數下標。
參考:
極客時間的數據結構與算法之美課程
歡迎關注我的微信公眾號--算法猿的成長,或者掃描下方的二維碼,大家一起交流,學習和進步!
如果覺得不錯,在看、轉發就是對小編的一個支持!
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
- 上一篇: 电脑选购:看完才明白,一体机和台式机电脑
- 下一篇: 黑帽SEO必须掌握的四种暗链代码