數據結構java
但凡IT江湖俠士,算法與數據結構爲必修之課。早有前輩已經明確指出:程序=算法+數據結構 。要想在以後的江湖歷練中通關,數據結構必不可少。數據結構與算法相輔相成,亦是陰陽互補之法。golang
開篇web
說道數組,幾乎每一個IT江湖人士都不陌生,甚至過半人還會很自信覺的它很簡單。 的確,在菜菜所知道的編程語言中幾乎都會有數組的影子。不過它不只僅是一種基礎的數據類型,更是一種基礎的數據結構。若是你覺的對數組足夠了解,那能不能回答一下:算法
數組的本質定義?編程
數組的內存結構?c#
數組有什麼優點?數組
數組有什麼劣勢?緩存
數組的應用場景?服務器
數組爲何大部分都從0開始編號?數據結構
數組可否用其餘容器來代替,例如c#中的List<T>?
定義
所謂數組,是相同的元素序列。數組是在程序設計中,爲了處理方便,把具備相同類型的若干元素按無序的形式組織起來的一種形式。
——百科
正如以上所述,數組在應用上屬於數據的容器。不過我仍是要補充兩點:
1. 數組在數據結構範疇屬於一種線性結構,也就是隻有前置節點和後續節點的數據結構,除數組以外,像咱們平時所用的隊列,棧,鏈表等也都屬於線性結構。
有線性結構固然就有非線性結構,好比以後咱們要介紹的二叉樹,圖 等等,這裏再也不展開~~~
2. 數組元素在內存分配上是連續的。這一點對於數組這種數據結構來講很是重要,甚至能夠說是它最大的「殺手鐗」。下邊會有更詳細的介紹。
優點和劣勢
優點
我相信全部人在使用數組的時候都知道數組能夠按照下標來訪問,例如 array[1] 。做爲一種最基礎的數據結構是什麼使數組具備這樣的隨機訪問方式呢?天性聰慧的你可能已經想到了:內存連續+相同數據類型。
如今咱們抽象一下數據在內存上分配的情景。
1. 說到數組按下標訪問,不得不說一下大多數人的一個「誤解」:數組適合查找元素。爲何說是誤解呢,是由於這種說法不夠準確,準確的說數組適合按下標來查找元素,並且按照下標查找元素的時間複雜度是O(1)。爲何呢?咱們知道要訪問數組的元素須要知道元素在內存中對應的內存地址,而數組指向的內存的地址爲首元素的地址,即:array[0]。因爲數組的每一個元素都是相同的類型,每一個類型佔用的字節數系統是知道的,因此要想訪問一個數組的元素,按照下標查找能夠抽象爲:
array[n]=array[0]+size*n
以上是元素地址的運算,其中size爲每一個元素的大小,若是爲int類型數據,那size就爲4個字節。其實確切的說,n的本質是一個離首元素的偏移量,因此array[n]就是距離首元素n個偏移量的元素,所以計算array[n]的內存地址只需以上公式。
論證一下,若是下標從1開始計算,那array[n]的內存地址計算公式就會變爲:
array[n]=array[0]+size*(n-1)
對比很容易發現,從1開始編號比從0開始編號每次獲取內存地址都多了一次 減法運算,也就多了一次cpu指令的運行。這也是數組從0下標開始訪問一個緣由。
其實還有一種可能性,那就是全部現代編程語言的鼻祖:C語言,它是從0開始計數下標的,因此如今全部衍生出來的後代語言也就延續了這個傳統。雖然不符合人類的思想,可是符合計算機的原理。固然也有一些語言能夠設置爲不從下標0開始計算,這裏再也不展開,有興趣的能夠去搜索一下。
2. 因爲數組的連續性,因此在遍歷數組的時候很是快,不只得益於數組的連續性,另外也得益於cpu的緩存,由於cpu讀取緩存只能讀取連續內存的內容,因此數組的連續性正好符合cpu緩存的指令原理,要知道cpu緩存的速度要比內存的速度快上不少。
劣勢
1. 因爲數組在內存排列上是連續的,並且要保持這種連續性,因此當增長一個元素或刪除一個元素的時候,爲了保證連續性,須要作大量元素的移動工做。
舉個栗子:要在數組頭部插入一個新元素,爲了在頭部騰出位置,全部的元素都要後移一位,假設元素個數爲n,這就致使了時間複雜度爲O(n)的一次操做,固然若是是在數組末尾插入新元素,其餘全部元素都沒必要移動,操做的時間複雜度爲O(1)。
固然這裏有一個技巧:若是你的業務要求並非數組連續有序的,當在位置k插入元素的時候,只須要把k元素轉移到數組末尾,新元素插入到k位置便可。固然仔細沉思一下這種業務場景可能性過小了,數組均可以無序,我直接插入末尾便可,沒有必要非得在k位置插入把。~~
固然還有一個特殊場景:若是是屢次連續的k位置插入操做,咱們徹底能夠合併爲一次「批量插入」操做:把k以後的元素總體移動sum(插入次數)個位置,無需一個個位置移動,把三次操做的時間複雜度合併爲一次。
與插入對應的就有刪除操做,同理,刪除操做數組爲了保持連續性,也須要元素的移動。
綜上所述,數組在添加和刪除元素的場景下劣勢比較明顯,因此在具體業務場景下應該避免頻繁添加和刪除的操做。
2. 數組的連續性就要求建立數組的時候,內存必須有相應大小的連續區塊,若是不存在,數組就有可能出現建立失敗的現象。在某些高級語言中(好比c#,golang,java)就有可能引起一次GC(垃圾回收)操做,GC操做在系統運行中是很是昂貴的,有的語言甚至會掛起全部線程的操做,對外的表現就是「暫停服務」。
3. 數組要求全部元素爲同一個類型。在存儲數據維度,它可能算是一種劣勢,可是爲了按照下標快速查找元素,業務中這也是一種優點。仁者見仁智者見智而已。
4. 數組是長度固定的數據結構,因此在原始數組的基礎上擴容是不可能的,有的語言可能實現數組的「僞擴容」,爲何說是「僞」呢,由於原理實際上是建立了一個容量更大的數組來存放原數組元素,發生了數據複製的過程,只不過對於調用者而已透明而已。
5. 數組有訪問越界的可能。咱們按照下標訪問數組的時候若是下標超出了數組長度,在現代多數高級語言中,直接就會引起異常了,可是一些低級語言好比C 有可能會訪問到數組元素之外的數據,由於要訪問的內存地址確實存在。
其餘
不少編程語言中你會發現「純數組」並無提供直接刪除元素的方法(例如:c#,golang),而是須要將數組轉化爲另外一種數據結構來實現數組元素的刪除。好比在golang種能夠轉化爲slice。這也驗證了數組的不變性。
咱們學習的每一個數據結構其實都有對應的適合場景,只不過是場景多少的問題,具體何時用,須要咱們對該數據結構的特性作深刻分析。
關於數組的特性,經過以上介紹能夠知道最大的一個亮點就是按照下標訪問,那有沒有具體業務映射這種特性呢?
1. 相信不少IT人士都遇到過會員機制,每一個會員到達必定的經驗值就會升級,怎麼判斷當前的經驗是否到達升級條件呢?咱們是否是能夠這樣作:好比當前會員等級爲3,判斷是否到達等級4的經驗值,只須要array[4]的值判斷便可,大多數人把配置放到DB,資源耗費太嚴重。也有的人放到其餘容器緩存。可是大部分場景下查詢的時間複雜度要比數組大不少。
2. 在分佈式底層應用中,咱們會有利用一致性哈希方案來解決每一個請求交給哪一個服務器去處理的場景。有興趣的同窗能夠本身去研究一下。其中有一個環節:根據哈希值查找對應的服務器,這是典型的讀多寫少的應用,並且比較偏底層。若是用其餘數據結構來解決大量的查找問題,可能會觸碰到性能的瓶頸。而數據按下標訪問時間複雜度爲O(1)的特性,使得數組在相似這些應用中很是普遍。