上一篇,我們講完演算法複雜度,接下來我們來見一見我們非常熟悉的朋友--陣列。
我們平時使用的陣列是資料型別,但是陣列不僅僅是資料型別更是一種基礎的資料結構。
陣列的定義
我們來看看陣列定義:分類連續的記憶體空間來儲存相同型別集合的線性表資料結構。
線性表+連續記憶體+相同型別 著三個特性合併出了陣列的必殺技:隨機訪問。
那麼陣列是怎麼實現下標隨機訪問的呢?
隨機訪問
根據頭節點和固定型別具體長度,就可以實現隨機訪問。
search[i]_address = base_address + i*data_type_size
雖然對於訪問來講,下標隨機訪問的複雜度為O(1)
插入和刪除
但是對於插入和刪除來說日子就不是那麼好過了。
因為陣列要保證記憶體的連續性這個特性,所以插入和刪除都是比較低效的,我們具體來看看插入和刪除。
插入:我們在任意節點前插入,那麼後面的元素必須後移一位來保證連續性。
假設我們有一個陣列為 int[n] =[1,2,3,4,5,......,n]
我們在左側頂頭插入呢?後面的全部都移動,所以算個複雜度為O(n),我們上面一篇提到過的最差情況時間複雜度
與此對應的是末尾插入,完美,啥都不用幹,這就不用說了就是 最好情況時間複雜度。
那麼如果在中間任意位置出現呢?多種情況,所以就是平均情況時間複雜度
歸納一下:
陣列左側頂頭插入元素,最壞情況時間複雜度 O(n)
陣列末尾追加插入元素:最好情況時間複雜度 O(1)
陣列中間情況插入元素,平均情況時間複雜度 O(n)
插入平均時間複雜度
我們這裡來驗證一下,陣列下標隨機訪問的平均時間複雜度為什麼是O(n):
對於一個 int[n] = [1,2,3,4,5,......,n]
我從頭依次進行插入,那麼對應的移動陣列次數是
n
(n-1)
(n-2)
...
1
0
對於平均時間複雜度來講,可以插入的空前後兩端2個+中間n-1,即 n+1 種情況
這裡的每個插入點發生的情況概率是一樣的都是 1 / (n+1)
所以,平均時間複雜度為移動次數*發生的概率
n * [1/(n+1)]
(n-1) * [1/(n+1)]
...
1 * [1/(n+1)]
0 * [1/(n+1)]
簡化一下:(0+1+2+3...n) / (n+1) => [(n-1)/2] * n + n / (n+1)
演算法複雜度去除係數、常量、低階,這裡的平均情況時間複雜度是O(n)
日常開發中,如果我們遇到有序陣列,那我們必須在插入的同時,後移後面的所有位置。
但是如果陣列對排序不敏感,那麼我們的插入可以在後面追加,這樣避免了移動陣列,複雜度就是O(1)
刪除:如果我們刪除陣列中的元素,為了保持陣列的連續性,依然需要搬運陣列元素。
所以刪除操作和插入操作的最好、最壞、平均複雜度是對應的。
其實我們也可以加一個刪除標記,在空閒的時候進行刪除重排序。
訪問越界問題
對於以上這段C程式來說,上面這段程式碼,我們在書寫的時候,沒有仔細檢查,使得<寫成了<=,這樣就會產生了越界問題。
前提條件:
1、因為不同的CPU架構不同的編譯器會有不同的記憶體分配策略:從高地址向低地址增長,或者從低地址向高地址增長。也就是大小端問題。
2、首先壓棧的是 i ,之後是陣列arr。所以 i 的記憶體地址比arr中的元素高,並且 i 和arr中的元素相鄰,並且型別相同,也就是子節是對齊的。
所以當 arr [ 3 ] 這個地址越界訪問到了 相鄰的同型別的 i 這個變數的記憶體地址。arr[3] =0,其實也就是 i = 0,好吧,又從頭開始了,無限迴圈。
那麼為什麼我們平時使用的語言即便是越界也會程式異常終止。其實這是編譯器做的工作,編譯器不同,記憶體申請方式也不同。
我們平時的編譯器已經幫我們做好了程式碼檢查等工作。所以避免了越界的情況。
C# []、Array、ArrayList、List<T>
首先 [] ,就是我們宣告簡單的陣列,因為陣列是連續的線性資料結構,所以很多操作微軟又給做了封裝。
Array ,就是微軟對陣列進行的封裝類。下面又進一步衍生出了動態陣列。
ArrayList,就是動態陣列,裡面封裝了陣列很多的操作。尤其是動態擴容。泛型之後,又出了泛型列表。
List<T>是動態陣列的泛型版本,避免了頻繁的裝箱拆箱,效率較高,也是我們平時使用最頻繁的一種。
其實微軟已經開源了.NET Framework 原始碼,詳細可檢視此地址。
還有一種方式,可以使用遠端除錯原始碼,遠端下載原始碼到本地,進行原始碼除錯:
工具 -》 除錯 -》勾選 啟用原始碼單步除錯
當然速度上肯定會比原生程式碼慢。
以上就是今天的內容。