時間複雜度的分析及排序算法總結

2021-02-19 大前端百科
時間複雜度

當問題規模即要處理的數據增⻓時,基本操作要重複執⾏的次數必定也會增⻓,那麼我們關⼼地是這個執⾏次數以什麼樣的數量級增⻓。

算法時間複雜度是一個函數,它定性描述該算法的運行時間(也就是所花費時間的消耗)。

我們要知道討論的時間複雜度就是指一般情況下的時間複雜度。


表示法

我們⽤⼤O表示法表示⼀下常⻅的時間複雜度量級:

常數階O(1) 線性階O(n) 對數階O(logn) 線性對數階O(nlogn) 平⽅階O(n²)。

排序算法中各個算法時間複雜度為:O(n2), O(nlogn), O(n)。這些我們之前都學習過,可以先從排序算法來理解時間複雜度。這裡所說的都是指的一般情況下的時間複雜度(或者平均時間複雜度)。

當然還有指數階和階乘階這種⾮常極端的複雜度量級,我們就不討論了。


O(1)

傳說中的常數階的複雜度,這種複雜度⽆論數據規模n如何增⻓,計算時間是不變的。不管n如何增⻓,都不會影響到這個函數的計算時間,因此這個代碼的時間複雜度都是O(1)。


O(n)

線性複雜度,隨著數據規模n的增⻓,計算時間也會隨著n線性增⻓。典型的O(n)的例⼦就是線性查找。

const linearSearch = (arr, target) => {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}

線性查找的時間消化與輸⼊的數組數量n成⼀個線性⽐例,隨著n規模的增⼤,時間也會線性增⻓。


注意:下面代碼的時間複雜度也是O(n)。

O(n + n) = O(2n) = O(n)。

const search = (arr) => {
for (let i = 0; i < arr.length; i++) {
...
}

for (let i = 0; i < arr.length; i++) {
...
}

return -1;
}


O(logn)

對數複雜度,隨著問題規模n的增⻓,計算時間也會隨著n對數級增⻓。

典型的例⼦是⼆分查找法(有序序列)。

functions binarySearch(arr, target) {
let max = arr.length - 1;
let min = 0;

while (min <= max) {
let mid = Math.floor((max + min) / 2);
if (target < arr[mid]) {
max = mid - 1;
} else if (target > arr[mid]) {
min = mid + 1;
} else {
return mid;
}
}
return -1
}

在⼆分查找法的代碼中,通過while循環,成 2 倍數的縮減搜索範圍,也就是說需要經過 log2n 次即可跳出循環。

事實上在實際項⽬中, O(logn) 是⼀個⾮常好的時間複雜度,⽐如當 n=100 的數據規模時,⼆分查找只需要7次,線性查找需要100次,這對於計算機⽽⾔差距不⼤,但是當有10億的數據規模的時候,⼆分查找依然只需要30次,⽽線性查找需要驚⼈的10億次, O(logn) 時間複雜度的算法隨著數據規模的增⼤,它的優勢就越明顯。


O(nlogn)

線性對數複雜度,隨著數據規模n的增⻓,計算時間也會隨著n呈線性對數級增⻓。

這其中典型代表就是歸併排序,快速排序,堆排序,希爾排序。


以堆排序為例:

堆排序中有兩個步驟:

創建堆:

調整堆


O(n²)

平⽅級複雜度,典型情況是當存在雙重循環的時候,即把 O(n) 的代碼再嵌套循環⼀遍,它的時間複雜度就是 O(n²)了,代表應⽤是冒泡排序,插入排序,簡單選擇排序。


注意:要理解時間複雜度本質,不能單純的記憶兩個for循環的時間複雜度是O(n2),三個for循環的時間複雜度是O(n3),還要看for循環的數量級。例如希爾排序,是三個for循環,但是它的時間複雜度是O(nlogn)。


關於logn思考

logn指的是n的對數,在數學中,n的對數必須有底數,也就是像log2n,log3n...。

那時間複雜度中的logn是以幾為底數呢?

有人說logn的底數為2,這種理解非常的片面,可以說是很不準確的,說明還沒有理解時間複雜度所要表達的本質是什麼。看了一些文檔,但是並沒有看到一篇好的文章去說明,或者探討清楚這個問題。實際上無論以2,或3為底數,其實這些都是常量,並不影響我們用logn來表述算法的時間複雜度,就像兩個n數量級的for循環一樣,並不是O(2n),而是O(n)。所以,我們統一說是O(logn),也就是忽略了底數的描述。


推導:(以2,和10為例)

n = 10 log10n

log2n = log10n * log210

log2n  => log10n


遞歸算法的時間複雜度

相信很多同學對遞歸算法的時間複雜度都很模糊。

「同一道題目,同樣使用遞歸算法,有的同學會寫出了O(n)的代碼,有的同學就寫出了O(logn)的代碼」

這是為什麼呢?如果對遞歸的時間複雜度理解的不夠深入的話,就會這樣!遞歸代碼看起來很簡介,優雅,但理解起來不是很透徹,包括我有些地方也不是很透徹,那在使用的過程中就更不容易用好遞歸了。

一個例子,來帶大家逐步分析遞歸算法的時間複雜度,最後找出最優解,來看看同樣是遞歸,怎麼就寫成了O(n)的代碼。


求x的n次方

function func1(x, n) {
let result = 1; // 注意 任何數的0次方等於1
for (let i = 0; i < n; i++) {
result = result * x;
}

return result;
}

上面的代碼的時間複雜度為O(n),怎麼能把上面的代碼優化一下,降低時間複雜度呢?


用遞歸實現

function func2(x, n) {
if(n == 0){
return 1;
}

return x * func2(x, n-1);
}

遞歸算法的時間複雜度本質上是要看: 「遞歸的次數 * 每次遞歸中的操作次數」

所以上面代碼的時間複雜度是 O(n),並沒有得到優化。



另一種遞歸實現

function func3(x, n){
if(n== 0) {
return 1;
}
if(n % 2 === 1) {
let n1 = Math.floor(n/2);
return func3(x, n1) * func3(x, n1) * x;
}
return func3(x, n/2) * func3(x, n/2);
}

我們來分析一下,首先看遞歸了多少次呢,可以把遞歸抽象出一顆滿二叉樹。剛剛同學寫的這個算法,可以用一顆滿二叉樹來表示(為了方便表示,選擇n為偶數16),如圖:

這棵樹上每一個節點就代表著一次遞歸併進行了一次相乘操作,所以進行了多少次遞歸的話,就是看這棵樹上有多少個節點。

熟悉二叉樹話應該知道如何求滿二叉樹節點數量,這顆滿二叉樹的節點數量就是2^3 + 2^2 + 2^1 + 2^0 = 15,可以發現:「這其實是等比數列的求和公式,這個結論在二叉樹裡也經常出現」

這麼如果是求x的n次方,這個遞歸樹有多少個節點呢?

「時間複雜度忽略掉常數項-1之後,這個遞歸算法的時間複雜度依然是O(n)」。對,你沒看錯,依然是O(n)的時間複雜度!


看來還是沒有得到優化啊。返回看func3中存在很多重複的遞歸計算,可以從這個點進行優化。


第三種遞歸實現

function func4(x, n){
if(n== 0) {
return 1;
}
let t = func4(x, Math.floor(n/2));
if(n % 2 === 1) {
return t * t * x;
}
return t * t;
}

依然還是看他遞歸了多少次,可以看到這裡僅僅有一個遞歸調用,且每次都是n/2 ,所以這裡我們一共調用了log以2為底n的對數次。

所以,上面的算法時間複雜度為O(logn).


同樣使用遞歸,有的人可以寫出O(logn)的代碼,有的人還是可以寫出O(n)的代碼。

相信大家對遞歸算法的有一個新的認識的,同一個問題,同樣是遞歸,效率可是不一樣的!


還能不能再優化呢?就是想讓遞歸的執行時間縮短!!


第四種遞歸實現

function func5(x, n, res){
if(n == 1) {
return res;
}
return func5(x, n-1, res*x);
}

上面的遞歸實現,代碼簡潔了很多,可讀性也好了很多,但是看不出來性能哪裡好了?執行時間呢?


下面將第三種遞歸和第四種遞歸做一個執行時間的對比:

function func4(x, n){
if(n== 0) {
return 1;
}
let t = func4(x, Math.floor(n/2));
if(n % 2 === 1) {
return t * t * x;
}
return t * t;
}
function func5(x, n, res){
if(n == 1) {
return res;
}
return func5(x, n-1, res*x);

}

console.time();

console.log(func4(2, 100));

console.timeEnd();

console.time();

console.log(func5(2, 100, 2));

console.timeEnd();

我地個天啊,差距怎麼這麼大啊!!最後一種遞歸的實現怎麼竟然可以這麼的優秀!!!這個會在我的另一篇文章中招到答案。


至此,希望大家對時間複雜度有一個初步的理解和認識。

排序算法總結排序算法比較

排序算法的選擇

不同算法的時間複雜度 在不同數據輸入規模下的差異。



我們在決定使用那些算法的時候 ,不是時間複雜越低的越好,要考慮數據規模,如果數據規模很小可以用O(n^2)的算法。

就像上圖中圖中 O(5n^2) 和 O(100n) 在n為20之前 很明顯 O(5n^2)是更優的,所花費的時間也是最少的。

這也就是為什麼JS中自帶排序方法 Array.sort在數據量小的情況下,選擇插入排序的原因。

相關焦點

  • 算法的時間複雜度和空間複雜度-總結
    第一是從數學上證明算法的正確性,這一步主要用到形式化證明的方法及相關推理模式,如循環不變式、數學歸納法等。而在證明算法是正確的基礎上,第二部就是分析算法的時間複雜度。算法的時間複雜度反映了程序執行時間隨輸入規模增長而增長的量級,在很大程度上能很好反映出算法的優劣與否。因此,作為程式設計師,掌握基本的算法時間複雜度分析方法是很有必要的。
  • 從經典算法題看時間複雜度
    經常有同學在 LeetCode 的題解中問解法的複雜度是多少。作為一個懶人,我一直在「逃避」這個問題,畢竟這東西聽起來就這麼「複雜」。但本著對題解認真負責的態度(心虛),我想趁此機會做一個總結。下面我將通過一些較為經典的算法題聊一聊幾種常見的時間複雜度。什麼是時間複雜度?
  • 評估算法及算法的時間複雜度
    在實時系統中,對系統響應時間要求高,則儘量選用執行時間少的算法;當數據處理量大,而存儲空間較少時,則儘量選用節省空間的算法。本文主要討論算法的時間特性,並給出算法在時間複雜度上的度量指標。漸進時間複雜度表示的意義是:(1)在較複雜的算法中,進行精確分析是非常複雜的;(2)一般來說,我們並不關心T(n)的精確度量,而只是關心其量級。
  • 力扣(LeetCode)- 常見的排序算法總結
    如何分析一個排序算法好壞時間複雜度、比較和交換次數、原地排序(空間複雜度為(1))、排序穩定性(相等元素之間的位置先後順序不變)。有序度:是數組中具有有序關係的元素對的個數逆序度:和有序度相反。選擇排序是不穩定排序,所以應用最少。
  • JS家的排序算法 十大經典算法排序總結
    ✦ ✦ ✦十大經典算法排序總結對比一張圖概括:(Selection Sort)選擇排序須知:在時間複雜度上表現最穩定的排序算法之一,因為無論什麼數據進去都是O(n²)的時間複雜度。。。本質上來看,快速排序應該算是在冒泡排序基礎上的遞歸分治法。快速排序的名字起的是簡單粗暴,因為一聽到這個名字你就知道它存在的意義,就是快,而且效率高! 它是處理大數據最快的排序算法之一了。雖然Worst Case的時間複雜度達到了O(n²),但是人家就是優秀,在大多數情況下都比平均時間複雜度為O(n log n) 的排序算法表現要更好,可是這是為什麼呢,我也不知道。。。
  • 十大經典排序算法最強總結
    >外排序:由於數據太大,因此把數據放在磁碟中,而排序通過磁碟和內存的數據傳輸才能進行;時間複雜度: 一個算法執行所耗費的時間。在冒泡排序之類的排序中,問題規模為n,又因為需要比較n次,所以平均時間複雜度為O(n²)。在歸併排序、快速排序之類的排序中,問題規模通過分治法消減為logN次,所以時間複雜度平均O(nlogn)。
  • 排序算法:快速排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助今天介紹一下快速排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,參照遞歸表達式漸進複雜度分析快速排序的時間複雜度遞歸表達式為,其中a=2,b=2,d=1由算法導論的主定理可以推導出,快速排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • 排序算法總結(1):冒泡排序
    冒泡排序是一種交換排序。什麼是交換排序呢?交換排序:兩兩比較待排序的關鍵字,並交換不滿足次序要求的那對數,直到整個表都滿足次序要求為止。算法思想它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。
  • 十大經典排序算法之希爾排序算法
    希爾排序之前我們講過冒泡排序、選擇排序、插入排序,它們的時間複雜度都是 的時間複雜度深淵但是這個確實是按照希爾排序的思想進行寫出來的。確實,這個程序確實是四層循環,但是呢一個程序的時間複雜度不能單單看循環的層數,更應該看的是程序隨著輸入的執行次數。
  • 排序算法
    數據結構中的幾種排序算法:插入排序分為直接插入排序和希爾排序。
  • 算法的時間複雜度到底如何計算
    我們把 算法需要執行的運算次數 用 輸入大小n 的函數 表示,即 T(n) 。此時為了 估算算法需要的運行時間 和 簡化算法分析,我們引入時間複雜度的概念。定義:存在常數 c 和函數 f(N),使得當 N >= c 時 T(N) <= f(N),表示為 T(n) = O(f(n)) 。
  • 重溫7種排序算法 第一篇,交換排序算法:冒泡排序、快速排序
    最優的空間複雜度就是開始元素順序已經排好了,則空間複雜度為:0;最差的空間複雜度就是開始元素逆序排序了,則空間複雜度為:O(n);平均的空間複雜度為:O(1); 2.2時間複雜度:執行算法所需要的計算工作量每次比較後都必須移動元素3次來交換元素位置,因此
  • 用機器學習構建O(N)複雜度的排序算法,可在GPU和TPU上加速計算
    排序一直是計算機科學中最為基礎的算法之一,從簡單的冒泡排序到高效的桶排序,我們已經開發了非常多的優秀方法。但隨著機器學習的興起與大數據的應用,簡單的排序方法要求在大規模場景中有更高的穩定性與效率。中國科技大學和蘭州大學等研究者提出了一種基於機器學習的排序算法,它能實現 O(N) 的時間複雜度,且可以在 GPU 和 TPU 上高效地實現並行計算。
  • 十大經典排序算法最強總結(含JAVA代碼實現)
    所以我根據這幾天看的文章,整理了一個較為完整的排序算法總結,本文中的所有算法均有JAVA實現,經本人調試無誤後才發出,如有錯誤,請各位前輩指出。0、排序算法說明0.1 排序的定義對一序列對象根據某個關鍵字進行排序。
  • 詳解JAVA數據結構與算法:排序算法
    3) 常見的排序算法分類(見右圖):l算法的時間複雜度度量一個程序(算法)執行時間的兩種方法1) 事 後統計的方 法 這種方法可 行 , 但 是有兩個 問題 :一是要想對設計的算法的運行性能進行評測2) 事前估 算的方 法 通過分析某個算法的 時間複雜度 來判斷哪個算法更優 .時間頻度時間頻度:一個算法花費的時間與算法中語句的執行次數成正比例,哪個算法中語句執行次數多,它花費時間就多。一個算法中的語句執行次數稱為語句頻度或時間頻度。記為T(n)。
  • 為什麼要排序?排序算法的性能提升之道
    n次,n為數組的長度,因此冒泡排序排序的時間複雜度變為O(n)。改進版冒泡排序:如果看一下代碼,就會發現在上述排序算法中即使數組已經排序,時間複雜度也將相同,即O(n)。為了克服這個問題,提出了一種改進算法。創建一個標誌來確定數組是否已排序,該標誌會檢查所有相鄰對之間是否發生了交換。
  • 怎麼樣更好地理解排序算法
    :平均時間複雜度是 O(n^2),最佳情況是 O(n),最差情況是 O(n^2);空間複雜度 O(1);穩定的排序算法(相等元素的前後順序排序後不變);:平均時間複雜度是 O(n^2),最佳和最差情況也是一樣;空間複雜度 O(1);不穩定的排序算法(相等元素的前後順序排序後可能改變);
  • 關於時間複雜度,你不知道的都在這裡!
    同樣算法導論給出了例子:拿插入排序來說,插入排序的時間複雜度我們都說是O(n^2) 。輸入數據的形式對程序運算時間是有很大影響的,在數據本來有序的情況下時間複雜度是O(n),但如果數據是逆序的話,插入排序的時間複雜度就是O(n^2),也就對於所有輸入情況來說,最壞是O(n^2) 的時間複雜度,所以稱插入排序的時間複雜度為O(n^2)。
  • 排序算法:選擇排序
    在日常開發中經常會遇到一類問題,就是對一個集合的數據進行排序掌握一些排序算法,對於日常開發是非常有幫助的今天介紹一下選擇排序法01算法邏輯02時間複雜度由上圖邏輯可以得出,選擇排序的循環次數為由循環次數可以得出,選擇排序的時間複雜度為03空間複雜度由上圖邏輯可以得出
  • 程式設計師必備的幾種常見排序算法和搜索算法總結
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言最近為了鞏固一下自己的算法基礎,又把算法書裡的基本算法刷了一遍, 特地總結一下前端工程師需要了解的排序算法和搜索算法知識,雖然還有很多高深算法需要了解