面試官: 100萬個成員的數組取第一個和最後一個有性能差距嗎?

本文首發於微信公衆號「程序員面試官」html

數組幾乎能夠是全部軟件工程師最經常使用到的數據結構,正是由於如此,不少開發者對其不夠重視.前端

而面試中常常有這樣一類問題: 「100萬個成員的數組取第一個和最後一個有性能差距嗎?爲何?」java

除此以外,咱們在平時的業務開發中會常常出現數組一把梭的狀況,大多數狀況下咱們都會用數組的形式進行操做,而有讀源碼習慣的開發者可能會發現,在一些底層庫中,咱們可能平時用數組的地方,底層庫卻選擇了另外的數據結構,這又是爲何?程序員

但願你們帶着以上的問題咱們進行討論.web

什麼是數組

數組是計算機科學中最基本的數據結構了,絕大多數編程語言都內置了這種數據結構,也是開發者最多見的數據結構.面試

數組(英語: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的元素.

而二分查找的效率則更高,因爲咱們知道此數組的元素是有序遞增排列的:

  1. 咱們能夠取一個索引爲3的元素爲中間值p
  2. 將p與目標值6進行對比,發現p的值4<6,那麼此時因爲是遞增數組,目標值必定在索引3以後的元素中
  3. 此時,再在索引3以後到尾部的元素中取出新的中間值p與目標值比對,再重複下去,直到找到目標值

咱們能夠發現這樣的操做每一次對比都能排除當前元素數量一半的元素,總體下來的時間複雜度只有O(log n),這表示此方法的效率極高.

這種高效的方法在數據量越大的狀況下,越能體現出來,好比目前有一個10億成員的數組是有序遞增,若是按照線性查找,最差的狀況下須要10億此查找操做才能找到結果,而二分查找僅僅須要7次.

插入性能優化

好比有如下數組,咱們要將一個新成員orange插入索引1的位置,一般狀況下須要後三位成員後移,orange佔據索引1的位置.

可是若是咱們的需求並不必定須要索引的有序性呢?也就是說,咱們能夠把數組當成一個集合概念,咱們能夠在索引1的位置插入orange並在數組的尾部開闢一個新內存將本來在1位置的banana存入新內存中,這樣雖然索引的亂了,可是整個操做僅僅須要O(1)的時間複雜度.

arr = ['apple', 'banana', 'grape', 'watermelon'] 複製代碼

刪除性能優化

刪除操做須要將產出位置後的元素集體向前移動,這很是消耗性能,尤爲是在頻繁的刪除、插入操做中更是如此。

咱們能夠先記錄下相關的操做,可是並不當即進行刪除,當到必定的節點時咱們再一次性依據記錄對數組進行操做,這樣數組成員的反覆頻繁移動變成了一次性操做,能夠很大程度上提升性能.

 

數組刪除

 

這個思想應用很是普遍:

  1. 前端框架的虛擬DOM就是將對DOM的大量操做先儲存在差別隊列中,而後再一次性更新,避免了DOM的迴流和重繪.
  2. V8和JVM中的標記清除算法也是基於此思想,標記清除算法分爲兩個階段,標記階段對訪問到的對象都打上一個標識,在清除階段發現某個對象沒有標記則進行回收.

小結

回到題目中的問題,咱們如今已經能夠很清楚地知道「100萬個成員的數組取第一個和最後一個是否有性能差距」,答案顯然是沒有,由於數組是一塊線性連續的內存,咱們能夠經過尋址公式一步取出對應的成員,這跟成員的位置沒有關係.

最後咱們常常在面試或者LeetCode中碰到這樣一類問題,即數組中的子元素問題.

好比: 給定一個整數數組,計算長度爲 'k' 的連續子數組的最大總和。

 

題目

 

什麼方法能夠儘量地下降時間複雜度?說出你的思路便可.


 

相關文章
相關標籤/搜索