專注於Java領域優質技術,歡迎關注
數組幾乎可以是所有軟件工程師最常用到的數據結構,正是因為如此,很多開發者對其不夠重視.
而面試中經常有這樣一類問題: 「100萬個成員的數組取第一個和最後一個有性能差距嗎?為什麼?」
除此之外,我們在平時的業務開發中會經常出現數組一把梭的情況,大多數情況下我們都會用數組的形式進行操作,而有讀源碼習慣的開發者可能會發現,在一些底層庫中,我們可能平時用數組的地方,底層庫卻選擇了另外的數據結構,這又是為什麼?
希望大家帶著以上的問題我們進行討論.
什麼是數組
數組是計算機科學中最基本的數據結構了,絕大多數編程語言都內置了這種數據結構,也是開發者最常見的數據結構.
數組(英語:Array),是由相同類型的元素(element)的集合所組成的數據結構,分配一塊連續的內存來存儲.
當然,在一些動態語言中例如Python的列表或者JavaScript的數組都可能是非連續性的內存,也可以存儲不同類型的元素.
比如我們有如下一個數組:
arr = [1, 2, 3, 4, 5]
其在內存中的表現應該是這樣的:
我們可以看到,這個數組在內存中是以連續線性的形式儲存的,這個連續線性的儲存形式既有其優勢又有其劣勢,只有我們搞清楚優劣才能在以後的開發中更好地使用數組.
數組的特性
一個數據結構通常都有「插入、查找、刪除、讀取」這四種基本的操作,我們會逐一分析這些操作帶來的性能差異.
首先我們要辨析一個概念--性能.
這裡的性能並不是絕對意義上速度的快慢,因為不同的設備其硬件基礎就會產生巨大的速度差異,這裡的性能是我們在算法分析中的「複雜度」概念.
插入性能
我們已經知道數組是一段連續儲存的內存,當我們要將新元素插入到數組k的位置時呢?這個時候需要將k索引處之後的所有元素往後挪一位,並將k索引的位置插入新元素.
我們看到這個時候需要進行操作的工作量就大多了,通常情況下,插入操作的時間複雜度是O(n).
刪除性能
刪除操作其實與插入很類似,同樣我要刪除數組之內的k索引位置的元素,我們就需要將其刪除後,為了保持內存的連續性,需要將k之後的元素通通向前移動一位,這個情況的時間複雜度也是O(n).
查找性能
比如我們要查找一個數組中是否存在一個為2的元素,那麼計算機需要如何操作呢?
如果是人的話,在少量數據的情況下我們自然可以一眼找到是否有2的元素,而計算機不是,計算機需要從索引0開始往下匹配,直到匹配到2的元素為止.
這個查找的過程其實就是我們常見的線性查找,這個時候需要匹配的平均步數與數組的長度n有關,這個時間複雜度同樣是O(n).
讀取性能
我們已經強調過數組的特點是擁有相同的數據類型和一塊連續的線性內存,那麼正是基於以上的特點,數組的讀取性能非常的卓越,時間複雜度為O(1),相比於鏈表、二叉樹等數據結構,它的優勢非常明顯.
那麼數組是如何做到這麼低的時間複雜度呢?
假設我們的數組內存起始地址為start,而元素類型的長度為size,數組索引為i,那麼我們很容易得到這個數組內存地址的尋址公式:
arr[i]_address = start + size * i
比如我們要讀取arr[3]的值,那麼只需要把3代入尋址公式,計算機就可以一步查詢到對應的元素,因此數組讀取的時間複雜度只有O(1).
性能優化
我們已經知道除了「讀取」這一個操作以外,其他操作的時間複雜度都在O(n),那麼有沒有有效的方法進行性能優化呢?
查找性能優化
當數組的元素是無序狀態下,我們只能用相對不太快的線性查找進行查找,當元素是有序狀態(遞增或者遞減),我們可以用另一種更高效的方法--二分查找.
假設我們有一個有int類型組成的數組,以遞增的方式儲存:
arr = [1, 2, 3, 4, 5, 6, 7]
如果我們要查找值為6元素,按照線性查找的方式需要根據數組索引從0依次比對,直到碰到索引5的元素.
而二分查找的效率則更高,由於我們知道此數組的元素是有序遞增排列的:
- 我們可以取一個索引為3的元素為中間值p
- 將p與目標值6進行對比,發現p的值4<6,那麼此時由於是遞增數組,目標值一定在索引3之後的元素中
- 此時,再在索引3之後到尾部的元素中取出新的中間值p與目標值比對,再重複下去,直到找到目標值
我們可以發現這樣的操作每一次對比都能排除當前元素數量一半的元素,整體下來的時間複雜度只有O(log n),這表示此方法的效率極高.
這種高效的方法在數據量越大的情況下,越能體現出來,比如目前有一個10億成員的數組是有序遞增,如果按照線性查找,最差的情況下需要10億此查找操作才能找到結果,而二分查找僅僅需要7次.
插入性能優化
比如有以下數組,我們要將一個新成員orange插入索引1的位置,通常情況下需要後三位成員後移,orange佔據索引1的位置.
但是如果我們的需求並不一定需要索引的有序性呢?也就是說,我們可以把數組當成一個集合概念,我們可以在索引1的位置插入orange並在數組的尾部開闢一個新內存將原本在1位置的banana存入新內存中,這樣雖然索引的亂了,但是整個操作僅僅需要O(1)的時間複雜度.
arr = ['apple', 'banana', 'grape', 'watermelon']
刪除性能優化
刪除操作需要將產出位置後的元素集體向前移動,這非常消耗性能,尤其是在頻繁的刪除、插入操作中更是如此。
我們可以先記錄下相關的操作,但是並不立即進行刪除,當到一定的節點時我們再一次性依據記錄對數組進行操作,這樣數組成員的反覆頻繁移動變成了一次性操作,可以很大程度上提高性能.
這個思想應用非常廣泛:
- 前端框架的虛擬DOM就是將對DOM的大量操作先儲存在差異隊列中,然後再一次性更新,避免了DOM的迴流和重繪.
- V8和JVM中的標記清除算法也是基於此思想,標記清除算法分為兩個階段,標記階段對訪問到的對象都打上一個標識,在清除階段發現某個對象沒有標記則進行回收.
小結
回到題目中的問題,我們現在已經可以很清楚地知道「100萬個成員的數組取第一個和最後一個是否有性能差距」,答案顯然是沒有,因為數組是一塊線性連續的內存,我們可以通過尋址公式一步取出對應的成員,這跟成員的位置沒有關係.
最後我們經常在面試或者LeetCode中碰到這樣一類問題,即數組中的子元素問題.
比如: 給定一個整數數組,計算長度為 'k' 的連續子數組的最大總和。
什麼方法可以儘可能地降低時間複雜度?說出你的思路即可.
來源:掘金 鏈接:https://juejin.im/post/5d75a5266fb9a06b1a56b137
閱讀更多 Java技術架構 的文章