数据结构与算法分析c++第四版_数据结构与算法 - 时空复杂度分析
這周主要總結了時間復雜度的學習,跟小伙伴們分享下,歡迎指正。
一、為何需要分析算法復雜度
挺多同學本科都學習過數據結構和算法這門課,但是有沒有想過這門課到底是解決什么問題?科學家設計這些數據結構和算法是要干嘛?
其實,最終的目的只有一個:讓我們寫的代碼在計算機上運行的速度更快,使用的內存更省!,可是如何才能知道我們寫的代碼使用多少運行時間和內存呢?這就需要分析算法時間復雜度和空間復雜度,只有學會分析這 2 者,才能知道我們設計的算法到底有沒有性能的提升,不然你費了很大功夫想了一個算法,卻發現運行速度慢如烏龜,得不償失。
如果能夠在運行算法之前就能知道算法大概的執行時間那就好了,而復雜度分析恰好可以解決這個問題!復雜度分析又分為 2 種:
1.1 運行后分析
這種就是寫完算法直接放到機器上面跑,統計下到底用了多少時間和內存,但是這種方法有 2 個缺點:
1.2 運行前分析
那既然運行后分析有不可避免的缺點,有沒有辦法在紙上提前計算一下算法大概的執行時間和內存用量呢?當然有,就是今天的主角大 O 復雜度表示法!
二、大 O 復雜度表示法
我用一個例子來一步步解釋大 O 復雜度表示法到底是什么意思:
int cal(int n) {int sum = 0;int i = 1;int j = 1;for (; i <= n; ++i) {j = 1;for (; j <= n; ++j) {sum = sum + i * j;}}}對 CPU 而言執行程序分為以下 3 步:
因為我們只是在運行前粗略地估計算法的運行時間,因此可以假設每行代碼在 CPU 上運行的時間都相同,為 cputime,那么我們就可以直接計算出上面函數中所有代碼執行的總時間(次數 x 單位時間),n 表示輸入數據的規模:
- 第 2、3、4 行:每行需要 1 個 cpu_time,共
- 第 5、6 行:因為都是循環 n 次,所以需要
- 第 7、8 行:因為內外一共有 2 層 n 次的循環,所以需要
那么總的運行時間 T(n) 為:
對于每個確定的算法,所有代碼的執行次數一定,那么上面的 T(n) 與所有代碼的執行次數
成正比關系,比例系數就是 CPU 執行每行代碼的時間 cputime,因為可以把上式寫成大 O 復雜度表示法:其中:
- T(n):表示算法執行的總時間
- f(n):表示算法總的執行次數,就是
- O():表示 T(n) 與 f(n) 的正比關系,也就是大 O 的由來
所以上面函數的總的執行時間又可以寫成:
但是要注意:大 O 復雜度表示法并不是計算代碼準確的運行時間,而是表示一種代碼執行時間隨著數據規模 n 增長的變化趨勢,記住不是準確的時間,只是一種趨勢而已,因為實際工作的算法可能需要接受很大量級的數據,通過分析算法運行時間與輸入數據規模的變化趨勢就能大概知道一個算法能不能在實際環境中很好的工作。
但是呢,上面的大 O 表示法還是不夠簡潔,比如當算法代碼很多的時候,那我們是不是要在后面(或者前面)加上很多項:
這也不方便,因此大佬們又想了方法:** 只需要保留最大量級的運行次數即可!** 這是因為當輸入數據規模很大,比如 100000, 1000000 等,常數項 3、一階項 2n 等低階的運行次數對最高次項
的影響并不大,所有代碼的運行次數基本與最高階 的運行次數保持一致。因此我們只需要保留最高階的運行次數就行,并且系數也可以去掉,因為我們表示的是變化趨勢,常量系數并不影響變化趨勢:
這樣最終的大 O 復雜度表示法就成型了!這里之所以寫的那么詳細是因為這個復雜度分析真的非常重要,因為我們只把算法寫出來還不夠,我們還要能夠分析算法的優劣,并且以后的工作中如果需要選擇數據結構來完成指定的功能,你也必須要提前考慮自己選擇的數據結構的運行時間,這是非常重要的,你做的不是玩具,而是要能給用戶提供體驗良好的產品。
三、時間復雜度分析
學完大 O 表示法后,分析時間空間復雜度就很容易了,就是把前面的推導過程在不同的算法上計算一下,不過常見的復雜度分析分為這 3 種。
3.1 只分析循環次數最多的一段代碼
前面說過,大 O 表示法只需要關注最高階運行次數,而在實際的代碼中最高階運行次數的代碼往往是嵌套循環的最內層代碼,也就是循環執行次數最多的一段代碼,這種情況我們就只需要分析它就可以:
int cal(int n) {int sum = 0;int i = 1;for (; i <= n; ++i) {sum = sum + i;}return sum;}可以發現執行次數最多的代碼是 sum = sum + i,利用前面的大 O 分析法,很容易得出 T(n) = O(n)
3.2 加法法則
加入我們的程序有 2 個單層的 n 次 for 循環,還有一個 2 層的 n x n 的 for 循環,注意這里每個循環的數據規模是一樣的,都是 n,必須保證這一點:
int cal(int n) {int sum_1 = 0;int p = 1;for (; p < 100; ++p) {sum_1 = sum_1 + p;}int sum_2 = 0;int q = 1;for (; q < n; ++q) {sum_2 = sum_2 + q;}int sum_3 = 0;int i = 1;int j = 1;for (; i <= n; ++i) {j = 1; for (; j <= n; ++j) {sum_3 = sum_3 + i * j;}}return sum_1 + sum_2 + sum_3;}這種情況下我們只需要分別求出每個 for 的時間復雜度,然后加起來取最大的量級就行:
總的 T(n):
因為我們取最大量級,所以如果忽略數據規模 n 的特殊情況,我們可以取上面 3 個復雜度的最大值為最終結果:
上面算法最終的時間復雜度即為
,加法法則一般的公式如下,就是上面過程的一般情況,只不過用數學化的表示而已,很容易理解:3.3 乘法法則
類比加法法則,乘法法則就很好理解,加法是相加,乘法就是多個復雜度相乘,體現在代碼中就是有多個嵌套的循環調用,比如:
int cal(int n) {int ret = 0; int i = 1;for (; i < n; ++i) {ret = ret + f(i);} } int f(int n) {int sum = 0;int i = 1;for (; i < n; ++i) {sum = sum + i;} return sum;}在 cal 函數的循環中每次都調用 f 函數來計算,所以總的時間復雜度是這兩個函數的乘積:
注意不同與加法,乘法法則不是取最大的量級,而是直接冪次累加,要注意這點!
常用的時間復雜度
@(Google 王爭)總結的幾乎所有的復雜度如下圖:
相信你應該聽過 28 法則,20% 的技術能解決 80% 的問題,同樣對復雜度分析,工作中常用就如下幾個:
O(1)
代碼中不含有循環、遞歸外的代碼執行時間都為 O (1),記住 O (1) 并不是表示所有代碼只執行一次,而是表示這些代碼的執行次數不會隨著輸入數據規模 n 的變化而變化,即變化趨勢是一個常量:
int i = 8; int j = 6; int sum = i + j;不管有多少行上面這樣的代碼,他們的時間復雜度都是 O(1)。
O(logn), O(nlogn)
這兩種稱為對數階復雜度,在排序算法中比較常見,比如歸并排序,快速排序的復雜度都是 O(nlogn),但是這種復雜度分析相對難一點,看個例子:
i=1; while (i <= n) {i = i * 2; }這段代碼的復雜度就是分析 i = i * 2 的執行次數,只要求出執行次數就搞定了,學過等比數列的很容易看出,
,所以當:求出:x = log2(n),則時間復雜度就是 O (log2n),依葫蘆畫瓢:
i = 1; while (i <= n) {i = i * 3; }同理 x = log3(n),復雜度為 O (log3n),但是如果有很多不同的代碼是不是都要改變底數呢?并不用,因為對數有個換底公式,可以把一個對數換成指定的底數:
比如:
其中
是常數:因為大 O 表示法可以省略系數,所以:
可以看出
求出的復雜度也可以表示為 ,所以我們 統一忽略對數階的底數,都表示為 ,那么對于 就很好理解了,就是利用乘法法則把 復雜度的代碼執行 n 次即可。O(m + n), O(m * n)
這種復雜度有 2 個輸入數據規模 m 和 n,這表示我們的算法接收 2 個數據的輸入,但是因為數據規模不一定相同,所以不能簡單的利用加法法則:
int cal(int m, int n) {int sum_1 = 0;int i = 1;for (; i < m; ++i) {sum_1 = sum_1 + i;}int sum_2 = 0;int j = 1;for (; j < n; ++j) {sum_2 = sum_2 + j;}return sum_1 + sum_2; }這種情況下,不能取最大的量級,因為數據規模不一樣,我們只需要做復雜度的相加即可:T(n) = O(m + n),一般性的公式如下:
T1(m) + T2(n) = O[f(m) + g(n)]但乘法規則仍然有效,如果兩個嵌套循環的數據規模不同也成立:
for (int i = 0; i < m; i++)for (int j = 0; j < n; j++)...T1(m) * T2(n) = O[f(m) * g(n)]四、空間復雜度分析
與時間復雜度類似,(漸進)空間復雜度表示的是算法的存儲空間隨著數據規模變化的趨勢,空間復雜度分析比較容易:
void print(int n) {int i = 0;int[] a = new int[n];for (i; i <n; ++i) {a[i] = i * i;}for (i = n-1; i >= 0; --i) {print out a[i]} }比如:
- 第二行:申請的 int 內存與數據規模 n 無關,空間復雜度為 O (1)
- 第三行:申請的 int 數組內存與數據規模有關,規模越大申請的內存就越多,空間復雜度為 O (n)
所以根據大 O 表示法,最終的空間復雜度就是 O(n),常用的空間復雜有:
空間復雜度分析比較簡單,只需要看看有沒有與數據規模相關的內存申請操作即可,我們的重點還是放在時間復雜度分析,不管是面試還是考試,時間復雜度都是必定會問到的。
五、不同情況下的復雜度分析
除了以上各種情況的復雜度分析外,還需知道不同情況下的復雜度,主要分為 4 種情況:
- 最好、最壞
- 平均
- 均攤
下面簡單總結下,都挺好理解的。
5.1 最好、最壞情況時間復雜度
比如這個例子,在數組中查找元素 x 并返回索引:
// n 表示數組 array 的長度 int find(int[] array, int n, int x) {int i = 0;int pos = -1;for (; i < n; ++i) {if (array[i] == x) {pos = i;break;}}return pos; }假如要查找的元素 X 是數組的第一個元素,那么我們只需要循環一次就可以結束算法,此時的時間復雜度為 O (1),也即最好情況時間復雜度,但是假如我們要查找的元素 x 不在數組中,那么我們就需要循環 n 次結束算法,此時的時間復雜度為 O (n),對應的是最壞情況時間復雜度。
5.2 平均情況時間復雜度
通常情況下,最好或者最壞時間復雜度情況發生的概率不大,所以為了表示一般情況下的復雜度,我們使用平均情況時間復雜度,這種情況需要簡單的計算,不過很容易,還以上面的例子:
// n 表示數組 array 的長度 int find(int[] array, int n, int x) {int i = 0;int pos = -1;for (; i < n; ++i) {if (array[i] == x) {pos = i;break;}}return pos; }因為是平均情況所以可以做個假設,假設要查找的變量 x:
- 在數組中的概率為 1/2
- 不在數組中的概率也為 1/2
- 出現在 0 - n-1 這 n 個數組位置的概率相同,即為 1/n
所以根據概率乘法:要查找的數據 x 出現在數組中的任意位置的概率為 1/2n,先出現在數組中的概率乘以任意位置的概率,然后我們就可以將每個元素被找到時要查找的次數和對應概率相乘,最后再相加就得到算法的平均時間復雜度:
簡單解釋下:
- 1 x 1/2n 表示數組第一個元素只需要循環查找一次
- n x 1/2n 表示數組最后一個元素需要循環查找 n 次
- n x 1/2 表示不在數組中的元素也需要查找 n 次,因為你必須把數組元素都遍歷完后才能知道當前要查找的 x 不在數組中,那肯定就在數組外面嘍,前面說過在數組外面的概率也是 1/2
那么最后的結果就是全部想加,因為可以省略系數,所以上面查找元素 x 算法的時間復雜度也是
。雖然存在這 3 種情況,但是實際工作中分析算法的時間復雜度時并不需要嚴格分析這 3 種不同情況。一般情況下,使用前面講的復雜度分析方法得出復雜度即可,如果要詳細推導的話,可以計算下平均時間復雜度。5.3 均攤時間復雜度
這是一種特殊情況下的復雜度,并不是很常見,但還是了解下,它的主要思想是:把運行時間多的情況下的復雜度拆分,并均攤到運行時間少的情況下,看個例子:
// array表示一個長度為n的數組 // 代碼中的array.length就等于n int[] array = new int[n]; int count = 0;void insert(int val) {if (count == array.length) {int sum = 0;for (int i = 0; i < array.length; ++i) {sum = sum + array[i];}array[0] = sum;count = 1;}array[count] = val;++count; }這個算法實現如下功能,往數組中插入一個元素 val:
利用前面學習的復雜度分析可以很容易知道這兩種情況的時間復雜度分別為:O(n) 和 O(1),但是考慮實際運行情況,我們總是先把數組一個個位置存滿 O (1),然后滿了之后再執行求和的操作 O (n),并且這兩種情況的發生是有規律的。
因此我們可以把比較耗時 O (n) 復雜度的求和操作的均攤到不太耗時的 O (1) 插入操作上,這樣總體的時間復雜度就會變成 O (1),這就是均攤的思想,總結下均攤時間復雜度的應用場景:
如果你工作中的算法滿足這 3 個條件,那么可以嘗試用均攤時間復雜度來分析,這種情況比較少見,但還是要知道有這樣一種類型。
六、復雜度分析總結
今天學習的時空復雜度分析是數據結構和算法學習中非常重要的一環,只有學會分析時空復雜度,我們才能知道自己寫的算法到底能夠提升多快,如果不會分析復雜度,那只會盲目地選擇數據結構,盲目地設計算法,時間復雜度就是我們設計算法的指標。
我們學習數據結構和算法的目的就是為了寫出運行速度更快,存儲用量更低的代碼,如果都不會分析算法的執行時間和內存用量,那最后何談學過這門課或者學會這門課呢?請務必重視復雜度分析,后面設計算法會經常用到。
OK,大家下個專題見:)
本文原創首發于微信公號「登龍」,分享機器學習、算法編程、Python、機器人技術等原創文章,掃碼關注回復「1024」你懂的!總結
以上是生活随笔為你收集整理的数据结构与算法分析c++第四版_数据结构与算法 - 时空复杂度分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 学习Java编程equals()和has
- 下一篇: s3c2440移植MQTT