資料結構與演算法 - 陣列
資料結構與演算法 - 陣列
資料結構
一、定義
陣列(Array)是一種線性表資料結構。它用一組連續的記憶體空間,來儲存一組具有相同型別的資料。
線性表(Linear List)就是資料排成像一條線一樣的結構。每個線性表上的資料最多隻有兩個方向。除了陣列,連結串列、佇列、棧也是線性表結構。
與線性表對立的是非線性表,比如二叉樹、堆、圖等。之所以叫非線性,是因為,在非線性表中,資料之間並不是簡單的前後關係。
二、操作
操作 | 時間複雜度 |
---|---|
查詢 | \(O(1)\) |
插入 | 開頭\(O(1)\),末尾\(O(n)\),平均\(O(n)\) |
刪除 | 開頭\(O(1)\),末尾\(O(n)\) |
三、細節
- 為什麼資料編號從0開始?
從陣列的儲存模型上來看,“下標”最確切的定義應該是“偏移量(offset)”。
如果用a代表陣列的首地址,a[0]就是偏移量為0的位置,也就是首地址,a[k]就表示偏移量k個 type_size 的位置,所以計算 a[k] 的記憶體地址,以1開始,則多一次減法運算:
- 陣列型別與大小如何選擇?
- 陣列型別根據實際的情況,預先估算資料量大小、儲存大小等需求;
- 如果資料大小事先已知,並且對資料的操作非常簡單,可以定義靜態陣列;
- 如果資料大小需要動態新增,並且需要反覆操作,可以定義動態陣列
vector
;
靜態與動態陣列
一、 定義
1.靜態陣列:不可以更改陣列長度的
2.動態陣列:動態陣列本質上就是陣列,是由靜態陣列封裝的一些擴容能力。
靜態陣列在記憶體中位於棧區,是在定義時就已經在棧上分配了固定大小,在執行時這個大小不能改變,在函式執行完以後,系統自動銷燬;
動態陣列是使用者自己創建出來的,位於記憶體的堆區,它的大小是在執行時給定,並且可以改變其大小。同時,使用完必須由程式設計師自己釋放,否則嚴重會引起記憶體洩露。
二、 動態擴容機制
vector(c++):
-
擴容原理?當向vector中插入元素時,如果元素有效個數size與空間容量capacity相等時,vector內部會觸發擴容機制。拷貝元素和釋放舊空間,可以通過&vector[0]的方式來檢視資料首地址改變情況。
-
擴容大小?每次擴容新空間不能太大,也不能太小,太大容易造成空間浪費,太小則會導致頻繁擴容而影響程式效率。不同的的編譯器實現方式不同,vs中以1.5倍擴容,GCC以2倍擴容。
-
如何避免擴容導致效率低?如果在插入之前,可以預估vector儲存元素的個數,提前將底層容量開闢好即可。如果插入之前進行reserve,只要空間給足,則插入時不會擴容,如果沒有reserve,則會邊插入邊擴容,效率極其低下。
-
為什麼選擇以倍數方式擴容?以等長個數k進行擴容,向vector插入n個元素,需要插入元素操作和搬移元素操作的總和:\(n + \sum_{i=1}^{\frac{n}{k}}ik= n + \frac{(1+n/k)*n}{2}\),平攤下來每次操作時間\((n + \frac{(1+n/k)*n}{2})/n=3/2+n/2*k = O(N)\)。以倍數方式m進行擴容,向vector插入n個元素,需要插入元素操作和搬移元素操作的總和:\(n + \sum_{i=1}^{\log_{m}{n}}m^i= n + \frac{m(n-1)}{m-1}=n+\frac{mn}{m-1}\),平攤每次操作的時間\((n+\frac{mn}{m-1})/n = O(\frac{m}{m-1}))=O(1)\),m為常量。
-
為什麼選擇1.5倍或者2倍方式擴容,而不是3倍、4倍?(斐波那契數,1.618)理想的分配方案是在第N次擴容時如果能複用之前N-1次釋放的空間,因此按照小於2倍方式擴容,多次擴容之後就可以複用之前釋放空間。如果倍數超過2倍(包含2倍)方式擴容會存在:(1)空間浪費可能會比較高,比如:擴容後申請了64個空間,但只存了33個元素,有接近一半的空間沒有使用。(2)無法使用到前面已釋放的記憶體。
List(python):list是以兩倍進行擴容,並且會建立新的陣列,然後拷貝,系統再回收久陣列。
二維陣列
一、定義
二維陣列就是在一維陣列上,多加一個維度;
二、宣告與初始化
- c++ (靜態陣列)
//資料型別 陣列名[行數][列數];
int a1[3][3];
//資料型別 陣列名[行數][列數] = {{資料1,資料2,資料3},{資料4,資料5}};
int a2[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
//資料型別 陣列名[行數][列數] ={資料1,資料2,資料3,資料4}
int a2[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
//資料型別 陣列名[ ] [列數] = {資料1,資料2,資料3,資料4};
int a3[][3] = { 1,2,3,4,5,6 };
//查詢二維陣列所佔記憶體空間
cout << "二維陣列佔用記憶體空間為:" << sizeof(arr) << endl;
cout << "二維陣列第一行佔用記憶體為:" << sizeof(arr[0]) << endl;
cout << "二維陣列第一個元素所佔記憶體空間" << sizeof(arr[0][0]) << endl;
// 行列數
cout << "二維陣列行數為:" << sizeof(arr) / sizeof(arr[0]) << endl;
cout << "二維陣列列為:" << sizeof(arr[0]) / sizeof(arr[0][0]) << endl;
//可以檢視二維陣列的首地址
cout << "二維陣列首地址為:" << (int)arr << endl;
cout << "二維陣列第一行首地址為:" << (int)arr[0] << endl;
cout << "二維陣列第二行首地址為:" << (int)arr[1] << endl;
cout << "二維陣列第一個元素首地址:" << (int)&arr[0][0] << endl;
- c++ (動態陣列)
vector<vector<int>> a(m, vecotr<int>(n, 0));
- python (list)
li = [[0 for i in range(m)] for j in range(n)]
山脈陣列
一、 定義
山脈陣列:\(arr.length >= 3\),在 \(0 < i < arr.length - 1\)條件下,存在\(i\)使得:
- \(arr[0] < arr[1] < ... arr[i-1] < arr[i]\)
- \(arr[i] > arr[i+1] > ... > arr[arr.length - 1]\)
二、 題型
序號 | 題目 | 難度 |
---|---|---|
—— | ———————————————————————————————————————————————— | ————— |
0845 | 845. 陣列中的最長山脈 | 中等 |
0852 | 852. 山脈陣列的峰頂索引 | 簡單 |
0941 | 941. 有效的山脈陣列 | 簡單 |
1095 | 1095. 山脈陣列中查詢目標值 | 困難 |
旋轉陣列
一、 定義
旋轉陣列:nums在預先未知的某個下標 \(k(0 <= k < nums.length)\)上進行了 旋轉,使陣列變為\([nums[k], ..., nums[n-1], nums[0], ..., nums[k-1]]\)(下標 從 0 開始 計數)。例如, \([0,1,2,4,5,6,7]\) 在下標 3
處經旋轉後可能變為 \([4,5,6,7,0,1,2]\) 。
注意:
1.旋轉陣列,無論選擇多少次都是二分有序。
2.元素可以重複出現, 因此旋轉點一定是最小值, 但最小值不一定是旋轉點,如\([2,0,2,2,2]\)。因此需要判斷左右端點,當左右端點與中點相等,左遞增1右遞減1,然後再根據單調性來判斷。最小值可以根據右端點來比較。
二、 題型
序號 | 題目 | 難度 |
---|---|---|
—— | ———————————————————————————————————————————————— | ————— |
189.輪轉陣列 | ||
33. 搜尋旋轉排序陣列 | 中等 | |
81. 搜尋旋轉排序陣列 II | 中等 | |
153. 尋找旋轉排序陣列中的最小值 | 中等 | |
154. 尋找旋轉排序陣列中的最小值 II | 困難 | |
劍指 Offer 11. 旋轉陣列的最小數字 | 中等 | |
面試題 10.03. 搜尋旋轉陣列 | 中等 | |
環形陣列
一、 定義
陣列是 環形 的,所以可以假設從最後一個元素向前移動一步會到達第一個元素,而第一個元素向後移動一步會到達最後一個元素。
二、 題型
序號 | 題目 | 難度 |
---|---|---|
—— | ———————————————————————————————————————————————— | ————— |
457. 環形陣列是否存在迴圈 | ||
劍指 Offer II 090. 環形房屋偷盜 |
子陣列
一、 定義
子陣列 是陣列的連續子序列。
思路:滑動視窗(可變滑窗)和動態規劃。
二、 題型