本系列全部文章的代碼都是用JavaScript實現,之因此用JavaScript實現是由於它能夠直接在瀏覽器宿主中運行代碼,即在瀏覽器中按f12打開控制檯,選擇console按鈕,在下面空白的文本框把本例的代碼黏貼上去回車便可運行。方便各位同窗學習和調試。python
數組這個概念相信各位同窗在平常寫代碼的時候確定會常常用到,咱們一般用數組做爲容器來存儲數據。基本上每一種編程語言都有這種數據結構,它是一個基礎的數據結構,下面將仔細的講解數組的原理及應用。算法
什麼是數組呢?按照專業的名詞解釋,數組是一種線性表數據結構,它用連續的內存空間來存儲一組具備相同類型的數據。從定義裏咱們能夠看到幾個關鍵詞,分別是線性表(Linear List)和連續的內存空間和相同類型的數據。編程
線性表的意思其實就是數據排成像一條線同樣的結構。每一個線性表上的數據最多隻有前和後兩個方向。其實除了數組,鏈表、隊列、棧等都是線性表結構。而與線性表對立的則是非線性表 ,好比二叉樹、圖、堆等。之因此叫非線性,是由於非線性表中的數據並非簡單的先後關係。數組
當咱們聲明一個數組的時候,計算機就會爲數組分配一個連續的內存空間。假如咱們聲明的數組長度是10,在數組中存儲的元素都說int類型的數據,若是內存的首地址爲1000,則計算機爲數組分配了1000~1039的連續內存空間。數組和鏈表不一樣的一點就是數組存儲的都是連續的內存空間,而鏈表存儲的都說不連續的內存空間,因此若是一個計算機的內存只有1G的狀況下,咱們聲明瞭一個佔用1G內存的數組頗有可能會致使內存溢出,由於有可能內存裏有不連續的空間,而聲明1G內存的鏈表則不會出現這種狀況。瀏覽器
結合上面所說的兩點,數組因爲是線性的而且是連續的內存空間,隨機訪問的時候時間複雜度很是的快,爲O(1)。數組的隨機訪問並不須要遍歷自己,只須要知道下標就能夠得出值。可是有利也有弊,與快速的查詢相反的就是在插入和刪除的時候所要耗費更多的複雜度。在這裏須要提一點的是,數組是隨機查找的時候時間複雜度爲O(1),不能籠統的認爲數組在執行查找操做的時候時間複雜度爲O(1),若是你用二分查找來對數組進行查找操做,耗費的時間複雜度爲O(logn)。bash
上面提到數組因爲連續的內存空間致使了在執行插入和刪除操做的時候佔用大量的性能。首先咱們來講一下插入操做在數組的執行過程。數據結構
假設咱們聲明瞭一個數組長度爲n,若是咱們要插入的數組在數組第m個位置的時候,爲了可以讓數據成功的插入下標m當中,咱們要把m到n這一部分的數據日後移一位,而後把數據放入下標m當中。那若是數據是要插入到數組最後面的話,那時間複雜度也只是O(1),若是是在開頭插入的話時間複雜度則爲O(n),由於每一個位置的機率都是同樣的,因此咱們能夠獲得平均時間複雜度爲:。編程語言
若是一個數組是有序的,咱們爲了保持數組的有序性,的確只能用上述的方法來解。可是若是數組是無序的,爲了不大規模的數據移動,咱們能夠把當前下標m的數據放到最後面,把咱們的值放入到下標m當中。利用這個方法咱們能夠將時間複雜度降到O(1),性能將極大的提高。函數
同理在刪除中,若是咱們要刪除下標爲m的元素,爲了內存的連續性,也須要把m到n後面的數據往前移,否則就不連續。刪除的最好時間複雜度是O(1),即刪除的是結尾的數據的時候。最壞時間複雜度則爲O(n),即在開頭的數據被刪除。它的平均時間複雜度的公式也和上面插入的公式同樣,結果爲O(n)。佈局
那麼若是咱們對數組進行頻繁的刪除操做,程序的性能將會極大的下降,有時候辦法能夠解決呢?這個時候咱們能夠藉助JVM標記清除垃圾回收算法來實現。當執行刪除操做的時候咱們並非真的把數組裏的元素給刪除掉,而是給該元素標記一個刪除狀態,等到後面數組沒有更多的空間存儲數組的時候再一次性的執行刪除操做,極大地減小數據的遷移。下面用JavaScript代碼來簡單的實現一下:
var arr = new Array(10)
var count = 0
function insertArr(obj) {
if (typeof arr[9] === 'object') {
var tempArr = []
for (var a = 0; a < arr.length; a++) {
if (!arr[a].removeSign) {
arr[a].index = tempArr.length
arr[a].removeSign = false
tempArr.push(arr[a])
}
}
arr = tempArr
count = tempArr.length
if (arr.length === 10) {
console.error('數組越界')
return
}
}
arr[count] = {
value: obj.value,
removeSign: false,
index: count
}
count++
}
function removeArr(index){
if (arr.length === 0) {
console.error('數組長度爲0,不能刪除元素')
return
}
else if (index > arr.length) {
console.error('數組越界')
return
}
// 若是當前的已標記爲true則查看下一個元素是否爲true,若是不是則標記爲true,是的話則繼續遞歸
if (arr[index].removeSign) {
return removeArr(++index)
}
arr[index].removeSign = true
}複製代碼
這個代碼的含義是聲明一個長度爲10的數組,存入的都是對象,對象裏的value屬性表明它的值,removeSign屬性表示的是刪除標誌,爲false的時候表示的是未刪除,index屬性表示的是下標。下面的一個測試用例表示在數組裏存入10個數,而後刪除其中三個,最後添加一個元素後獲得長度爲8的數組。整個程序在存入是數據大於數組長度的時候纔會發生數組的刪除操做。
for (let a = 0; a < 8; a++) {
insertArr({
value: a,
removeSign: false
})
}
removeArr(2)
insertArr({
value: 10,
removeSign: false
})
insertArr({
value: 11,
removeSign: false
})
removeArr(1)
removeArr(2)
removeArr(3)
insertArr({
value: 13,
removeSign: false
})複製代碼
數組越界問題在不一樣的編程語言中會出現不同的結果。就拿上面的JavaScript代碼爲例,因爲JavaScript的數組是動態的,因此即便你聲明一個長度爲10的數組,你也能夠給數組的第十一位賦值,以後數組的長度就會變成11。而像Java這種靜態語言,自己就有對數組長度是否越界進行檢查,當你給數組第十一位賦值的時候就會報數組越界的問題,而像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;
}
複製代碼
上面的這個代碼在C語言環境中是無限循環輸出hello world,爲何會出現這種狀況呢?那是由於在 C 語言中,只要不是訪問受限的內存,全部的內存空間都是能夠自由訪問的,函數體內的局部變量存在棧上,且是連續壓棧。在Linux進程的內存佈局中,棧區在高地址空間,從高向低增加。變量i和arr在相鄰地址,且i比arr的地址大,因此arr越界正好訪問到i。固然,前提是i和arr元素同類型,不然那段代碼還是未決行爲。而且不少計算機病毒也正是利用到了代碼中的數組越界能夠訪問非法地址的漏洞,來攻擊系統,因此寫代碼的時候必定要警戒數組越界。
從數組存儲的內存模型上來看,「下標」最確切的定義應該是「偏移(offset)」。上面說到,咱們定義一個數組時,計算機會給每一個內存單元分配一個地址,計算機經過地址來訪問內存中的數據。當計算機須要隨機訪問數組中的某個元素時,它會首先經過下面的尋址公式,計算出該元素存儲的內存地址公式以下:
a[i]_address = base_address + i * data_type_size
其中 data_type_size 表示數組中每一個元素的大小。若是用 a 來表示數組的首地址,a[0] 就是偏移爲 0 的位置,也就是首地址,a[k] 就表示偏移 k 個 type_size 的位置,因此計算 a[k] 的內存地址只須要用這個公式:
a[k]_address = base_address + k * type_size
可是,若是數組從 1 開始計數,那咱們計算數組元素 a[k] 的內存地址就會變爲:
a[k]_address = base_address + (k-1)*type_size
對比兩個公式,咱們不難發現,從 1 開始編號,每次隨機訪問數組元素都多了一次減法運算,對於 CPU 來講,就是多了一次減法指令。數組做爲很是基礎的數據結構,經過下標隨機訪問數組元素又是其很是基礎的編程操做,效率的優化就要儘量作到極致。因此爲了減小一次減法操做,數組選擇了從 0 開始編號,而不是從 1 開始。不過其餘編程語言不必定數組下標就是從0開始,好比MATLAB,而像python則能夠負下標。
上面講到的是一維數組的內存尋址公式,若是到一個m*n的二維數組,當它的下標i<m,j<n時,它的公式以下:
a[i][j]address = base_address + n * i * type_size + j * type_size = base_address + ( i * n + j) * type_size
同理a*b*c的三維數組,當它的下標i<a,j<b,k<c時,公式以下:
a[i][j][k]address = base_address + bc * i * type_sizze + c * j * type_size + k * type_size = base_address + (bc * i + c * j + k) * type_size
上一篇文章:數據結構與算法的重溫之旅(二)——複雜度進階分析
下一篇文章:數據結構與算法的重溫之旅(四)——鏈表