✨✨ 歡迎大家來到貝蒂大講堂✨✨🎈🎈養成好習慣,先贊後看哦~🎈🎈
所屬專欄:資料結構與演算法
貝蒂的主頁:Betty‘s blog
前言
隨著應用程式變得越來越複雜和資料越來越豐富,幾百萬、幾十億甚至幾百億的資料就會出現,而對這麼大對資料進行搜尋、插入或者排序等的操作就越來越慢,人們為了解決這些問題,提高對資料的管理效率,提出了一門學科即:資料結構與演算法
1. 什麼是資料結構
資料結構(Data Structure)是計算機儲存、組織資料的方式,指相互之間存在一種或多種特定關係的資料元素的集合。
下標是常見的資料結構:
名稱 | 定義 |
---|---|
陣列(Array) | 陣列是一種聚合資料型別,它是將具有相同型別的若干變數有序地組織在一起的集合。 |
連結串列(Linked List) | 連結串列是一種資料元素按照鏈式儲存結構進行儲存的資料結構,這種儲存結構具有在物理上存在非連續的特點。 |
棧(Stack) | 棧是一種特殊的線性表,它只能在一個表的一個固定端進行資料結點的插入和刪除操作 |
佇列(Queue) | 佇列和棧類似,也是一種特殊的線性表。和棧不同的是,佇列只允許在表的一端進行插入操作,而在另一端進行刪除操作。 |
樹(Tree) | 樹是典型的非線性結構,它是包括,2 個結點的有窮集合 K |
堆(Heap) | 堆是一種特殊的樹形資料結構,一般討論的堆都是二叉堆。 |
圖(Graph) | 圖是另一種非線性資料結構。在圖結構中,資料結點一般稱為頂點,而邊是頂點的有序偶對 |
2. 什麼是演算法
演算法(Algorithm):就是定義良好的計算過程,他取一個或一組的值為輸入,併產生出一個或一組值作為輸出。簡單來說演算法就是一系列的計算步驟,用來將輸入資料轉化成輸出結果。
演算法一般分為:排序,遞迴與分治,回溯,DP,貪心,搜尋演算法
- 演算法往往數學密切相關,就如數學題一樣,每道數學題都有不同的解法,演算法也是同理。
3. 複雜度分析
3.1 演算法評估
我們在進行演算法分析時,常常需要完成兩個目標。一個是找出問題的解決方法,另一個就是找到問題的最優解。而為了找出最優解,我們就需要從兩個維度分析:
- 時間效率:演算法執行的快慢
- 空間效率:演算法所佔空間的大小
3.2 評估方法
評估時間的方法主要分為兩種,一種是實驗分析法,一種是理論分析法。
(1) 實驗分析法
實驗分析法簡單來說就是將不同種演算法輸入同一臺電腦,統計時間的快慢。但是這種方法有兩大缺陷:
- 無法排查實驗自身條件與環境的條件的影響:比如同一種演算法在不同配置的電腦上的運算速度可能完全不同,甚至結果完全相反。我們很難排查所有情況。
- 成本太高:同一種演算法可能在資料少時表現不明顯,在資料多時速率較快
(2) 理論分析法
由於實驗分析法的侷限性,就有人提出了一種理論測評的方法,就是漸近複雜度分析(asymptotic complexity analysis),簡稱複雜度分析。
這種方法體現演算法執行所需的時間(空間)資源與輸入資料大小之間的關係,能有效的反應演算法的優劣。
4. 時間複雜度與空間複雜度
4.1 時間複雜度
一個演算法所花費的時間與其中語句的執行次數成正比例,演算法中的基本操作的執行次數,為演算法的時間複雜度。
為了準確的表述一段代表所需時間,我們先假設賦值(=)與加號(+)所需時間為1ns,乘號(×)所需時間為2ns,列印所需為3ns。
讓我們計算如下程式碼所需總時間:
int main()
{
int i = 1;//1ns
int n = 0;//1ns
scanf("%d", &n);
for (i = 0; i < n; i++)
{
printf("%d ", i);//3ns
}
return 0;
}
計算時間如下:
$$
T(n)=1+1+3×n=3n+2
$$
但是實際上統計每一項所需時間是不現實的,並且由於是理論分析,當n—>∞時,其餘項皆可忽略,T(n)的數量級由最高階決定。所以我們計算時間複雜度時,可以簡化為兩個步驟:
- 忽略除最高階以外的所有項。
- 忽略所有係數。
而上述程式碼時間可以記為O(n),這種方法被稱為大O的漸進表示法。如果計算機結果全是常數,則記為O(1)。
- 並且計算複雜度時,有時候會出現不同情況的結果,這是應該以最壞的結果考慮。
4.2 空間複雜度
空間複雜度也是一個數學表示式,是對一個演算法在執行過程中臨時佔用儲存空間大小的量度 。空間複雜度的表示也遵循大O的漸進表示法
讓我們計算一下氣泡排序的空間複雜度
// 計算BubbleSort的空間複雜度?
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
- 透過觀察我們可以看出,氣泡排序並沒有開闢多餘的空間,所以空間複雜度為O(1).
5. 複雜度分類
演算法的複雜度有幾個量級,表示如下:
$$
O(1) < O( log N) < O(N) < O(Nlog N) < O(N 2 ) < O(2^𝑛 ) < 𝑂(O!)
$$
- 從左到右複雜度依次遞增,演算法的缺點也就越明顯
圖示如下:
5.1 常數O(1)階
常數階是一種非常快速的演算法,但是在實際應用中非常難實現
以下是一種時間複雜度與空間複雜度皆為O(1)的演算法:
int main()
{
int a = 0;
int b = 1;
int c = a + b;
printf("兩數之和為%d\n", c);
return 9;
}
5.2 對數階O(logN)
對數階是一種比較快的演算法,它一般每次減少一半的資料。我們常用的二分查詢演算法的時間複雜度就為O(logN)
二分查詢如下:
int binary_search(int nums[], int size, int target) //nums是陣列,size是陣列的大小,target是需要查詢的值
{
int left = 0;
int right = size - 1; // 定義了target在左閉右閉的區間內,[left, right]
while (left <= right) { //當left == right時,區間[left, right]仍然有效
int middle = left + ((right - left) / 2);//等同於 (left + right) / 2,防止溢位
if (nums[middle] > target) {
right = middle - 1; //target在左區間,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; //target在右區間,所以[middle + 1, right]
} else { //既不在左邊,也不在右邊,那就是找到答案了
return middle;
}
}
//沒有找到目標值
return -1;
}
空間複雜度為O(logN)的演算法,一般為分治演算法
比如用遞迴實現二分演算法:
int binary_search(int ar[], int low, int high, int key)
{
if(low > high)//查詢不到
return -1;
int mid = (low+high)/2;
if(key == ar[mid])//查詢到
return mid;
else if(key < ar[mid])
return Search(ar,low,mid-1,key);
else
return Search(ar,mid+1,high,key);
}
每一次執行遞迴都會對應開闢一個空間,也被稱為棧幀。
5.3 線性階O(N)
線性階演算法,時間複雜度與空間複雜度隨著數量均勻變化。
遍歷陣列或者連結串列是常見的線性階演算法,以下為時間複雜度為O(N)的演算法:
int main()
{
int n = 0;
int count = 0;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
count += i;//計算0~9的和
}
return 0;
}
以下為空間複雜度為O(N)的演算法
int main()
{
int n = 0;
int count = 0;
scanf("%d", &n);
int* p = (int*)malloc(sizeof(int) * n);
//開闢大小為n的空間
if (p == NULL)
{
perror("malloc fail");
return -1;
}
free(p);
p=NULL;
return 0;
}
5.4 線性對數階O(NlogN)
無論是時間複雜度還是空間複雜度,線性對數階一般出現在巢狀迴圈中,即一層的複雜度為O(N),另一層為O(logN)
比如說迴圈使用二分查詢列印:
int binary_search(int nums[], int size, int target) //nums是陣列,size是陣列的大小,target是需要查詢的值
{
int left = 0;
int right = size - 1; // 定義了target在左閉右閉的區間內,[left, right]
while (left <= right) { //當left == right時,區間[left, right]仍然有效
int middle = left + ((right - left) / 2);//等同於 (left + right) / 2,防止溢位
if (nums[middle] > target) {
right = middle - 1; //target在左區間,所以[left, middle - 1]
}
else if (nums[middle] < target) {
left = middle + 1; //target在右區間,所以[middle + 1, right]
}
else { //既不在左邊,也不在右邊,那就是找到答案了
printf("%d ", nums[middle]);
}
}
//沒有找到目標值
return -1;
}
void func(int nums[], int size, int target)
{
for (int i = 0; i < size; i++)
{
binary_search(nums, size, target);
}
}
空間複雜度為O(NlogN)的演算法,最常見的莫非歸併排序
void Merge(int sourceArr[],int tempArr[], int startIndex, int midIndex, int endIndex){
int i = startIndex, j=midIndex+1, k = startIndex;
while(i!=midIndex+1 && j!=endIndex+1) {
if(sourceArr[i] > sourceArr[j])
tempArr[k++] = sourceArr[j++];
else
tempArr[k++] = sourceArr[i++];
}
while(i != midIndex+1)
tempArr[k++] = sourceArr[i++];
while(j != endIndex+1)
tempArr[k++] = sourceArr[j++];
for(i=startIndex; i<=endIndex; i++)
sourceArr[i] = tempArr[i];
}
//內部使用遞迴
void MergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex) {
int midIndex;
if(startIndex < endIndex) {
midIndex = startIndex + (endIndex-startIndex) / 2;//避免溢位int
MergeSort(sourceArr, tempArr, startIndex, midIndex);
MergeSort(sourceArr, tempArr, midIndex+1, endIndex);
Merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
}
}
5.5 平方階O(N2)
平方階與線性對數階相似,常見於巢狀迴圈中,每層迴圈的複雜度為O(N)
時間複雜度為O(N2),最常見的就是氣泡排序
void BubbleSort(int* a, int n)
{
assert(a);
for (size_t end = n; end > 0; --end)
{
int exchange = 0;
for (size_t i = 1; i < end; ++i)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
break;
}
}
計算過程如下;
$$
T(N)=1+2+3+......+n-1=(n2-n)/2=O(n2)
$$
空間複雜度為O(N2),最簡單的就是動態開闢。
{
int n = 0;
int count = 0;
scanf("%d", &n);
int* p = (int*)malloc(sizeof(int) * n*n);
//開闢大小為n的空間
if (p == NULL)
{
perror("malloc fail");
return -1;
}
free(p);
p=NULL;
return 0;
}
5.6 指數階O(2N)
指數階的演算法效率低,並不常用。
常見的時間複雜度為O(2N)的演算法就是遞迴實現斐波拉契數列:
int Fib1(int n)
{
if (n == 1 || n == 2)
{
return 1;
}
else
{
return Fib1(n - 1) + Fib1(n - 2);
}
}
粗略估計
$$
T(n)=20+21+22+.....+2(n-1)=2n-1=O(2N)
$$
- 值得一提的是斐波拉契的空間複雜度為O(N),因為在遞迴至最深處後往回歸的過程中,後續空間都在銷燬的空間上建立的,這樣能大大提高空間的利用率。
空間複雜度為O(2N)的演算法一般與樹有關,比如建立滿二叉樹
TreeNode* buildTree(int n) {
if (n == 0)
return NULL;
TreeNode* root = newTreeNode(0);
root->left = buildTree(n - 1);
root->right = buildTree(n - 1);
return root;
}
5.7 階乘階O(N!)
階乘階的演算法複雜度最高,幾乎不會採用該型別的演算法。
這是一個時間複雜度為階乘階O(N!)的演算法
int func(int n)
{
if (n == 0)
return 1;
int count = 0;
for (int i = 0; i < n; i++)
{
count += func(n - 1);
}
return count;
}
示意圖:
- 空間複雜度為階乘階O(N!)的演算法並不常見,這裡就不在一一列舉。
本文由部落格一文多發平臺 OpenWrite 釋出!