時間複雜度分析快速入門:題型分類法

2021-03-02 五分鐘學算法

點擊上方「五分鐘學算法」,選擇「星標」公眾號

重磅乾貨,第一時間送達

來源:面向大象編程

最近,有不少讀者留言或私信問我時間複雜度的分析方法。時間複雜度說難也不難,說簡單也不簡單,但它一定是我們學習算法的過程中過不去的一道坎。這篇文章就想給大家介紹一種快速分析時間複雜度的方法 —— 題型分類法

網絡上關於時間複雜度分析的文章,大部分都充斥著教材裡的陳詞濫調:先講一大堆數學概念,什麼增長速度、漸進複雜度,看得讓人摸不著頭腦。我今天的文章不想跟你講這些虛的,只想跟你講講最實用的技巧,讓你迅速搞定面試中的時間複雜度分析

我們在面試中需要分析時間複雜度,無非是以下兩種情況:

無論是哪種情況,你一定都清楚這道題的題型。那麼只要記住不同題型的時間複雜度分析方法,二叉樹套用二叉樹的分析法、動態規劃套用動態規劃的分析法,就能做到快速分析時間複雜度了。

下面是我總結出的不同題型的時間複雜度分析方法:

題型時間複雜度
分析方法數組
鍊表
動態規劃數循環法二叉樹
回溯法
DFS/BFS整體法分治法
二分查找log n 分析法

本文將介紹「數循環法」與「整體法」兩種方法、六類題型的全部分析技巧。針對這六類題型,文末有按不同題型分類的往期文章回顧,供大家參考!

一、數循環法

數循環法,就是看代碼中有幾個循環、每個循環會執行幾次,來確定算法的時間複雜度。

我們必須要掌握的一個基礎知識是:在程序的三大基本結構(順序、分支、循環)中,只有循環能真正影響時間複雜度。一般情況下,我們可以直接忽略代碼中的順序結構、分支結構,直接看循環結構來判斷時間複雜度。

程序的三大基本結構數循環法典型題型:動態規劃類題目

「數循環法」最典型的題型就是動態規劃了。我們以一道經典的「最長公共子序列(LCS)」問題來看看數循環法是怎麼發揮作用的。

(以下題解代碼來自文章:LeetCode 例題精講 | 15 最長公共子序列:二維動態規劃的解法,不了解 LCS 問題或不清楚解法的同學,可以參考這篇文章。)

最長公共子序列(LCS)的題解代碼如下:

public int longestCommonSubsequence(String s, String t) {
    if (s.isEmpty() || t.isEmpty()) {
        return 0;
    }
    int m = s.length();
    int n = t.length();
    int[][] dp = new int[m+1][n+1];
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            if (i == 0 || j == 0) {
                dp[i][j] = 0;
            } else {
                if (s.charAt(i-1) == t.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
    }
    return dp[m][n];
}

根據數循環法的基本思路,我們可以直接忽略掉代碼中的各個細節,只看循環結構

public int longestCommonSubsequence(String s, String t) {
    ...
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= n; j++) {
            ...
        }
    }
    ...
}

數循環法要數兩個東西:循環的層數每個循環執行的次數。在這個例子中,代碼有兩個循環,都是簡單的 for-i 循環:

那麼,算法的時間複雜度就是內外層循環次數之積,也就是

循環的不同層數:數組類題目

很多數組類題目也可以用數循環法來分析時間複雜度。根據題目和解法的不同,循環可能有 1~3 層。數循環法對於不同層數的循環都是有效的。

例如,經典的「最大子數組和」題目(LeetCode 53. Maximum Subarray Sum),從暴力的三重循環到動態規劃的單層循環解法都有。

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int res = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        for (int j = i; j < n; j++) {
            int sum = 0;
            for (int k = i; k <= j; k++) {
                sum += nums[k];                    
            }
            res = Math.max(res, sum);
        }
    }
    return res;
}

時間複雜度

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int res = Integer.MIN_VALUE;
    for (int i = 0; i < n; i++) {
        int sum = 0;
        for (int j = i; j < n; j++) {
            sum += nums[j];
            res = Math.max(res, sum);
        }
    }
    return res;
}

時間複雜度

public int maxSubArray(int[] nums) {
    int n = nums.length;
    int[] dp = new int[n+1];
    dp[0] = 0;

    int res = Integer.MIN_VALUE;
    for (int k = 1; k <= n; k++) {
        dp[k] = Math.max(dp[k-1], 0) + nums[k-1];
        res = Math.max(res, dp[k]);
    }
    return res;
}

時間複雜度

不理解問題的動態規劃解法原理的同學,可以參考這篇文章:LeetCode 例題精講 | 16 最大子數組和:子數組類問題的動態規劃技巧

while 循環的處理方式:鍊表類題目

在上面的例子中,代碼中的循環都是簡單的 for-i 循環。有些情況下,代碼中的循環是 while 循環,或是一些複雜的 for 循環。對於這種情況,我們就需要仔細分析循環的執行次數

鍊表題型會經常用到 while 循環。我們用這道經典的「快慢指針」題目進行分析:LeetCode 876 - Middle of the Linked List。

(以下題解代碼來自文章:LeetCode 例題精講 | 05 雙指針×鍊表問題:快慢指針,對題目或解法不了解的同學可以參考。)

public ListNode middleNode(ListNode head) {
    ListNode fast = head;
    ListNode slow = head;
    while (fast != null && fast.next != null) {
        // fast 一次前進兩個元素,slow 一次前進一個元素
        fast = fast.next.next;
        slow = slow.next;
    }
    // 鍊表元素為奇數個時,slow 指向鍊表的中點
    // 鍊表元素為偶數個時,slow 指向鍊表兩個中點的右邊一個
    return slow;
}

可以看到,while 循環在 fast 指針到達鍊表尾部時結束。而 fast 指針初始位於鍊表的頭部,一次前進兩個元素。設鍊表的長度為

二、整體法

整體法一般涉及到遞歸和數據結構,不能直接用循環次數判定時間複雜度,而是要看代碼中處理了幾個元素,來確定算法的時間複雜度。

使用整體法進行分析的題型有:二叉樹、回溯法、DFS、BFS 等。下面我們分別舉例介紹。

二叉樹:看結點的個數

《LeetCode 例題精講》系列中寫過好幾次二叉樹的解題技巧。這些二叉樹問題的解法都有一個共性:

實際上,大部分二叉樹問題都可以使用子問題方法與「三步走」套路來求解。我專門寫過一篇文章總結二叉樹問題的套路:二叉樹問題太複雜?「三步走」方法解決它!

二叉樹問題既然都是用遞歸求解,就沒有循環來讓我們使用「數循環法」。這時候,我們就需要使用整體法,通過遍歷結點的個數來判斷時間複雜度。

二叉樹問題的時間複雜度離不開一個變量 只遍歷每個結點一次。這種情況下,算法的時間複雜度就是

不過對於稍難一些的題目,也可能出現「重複遍歷」的情況。例如 LeetCode 437. Path Sum III 這道題:

給定一個二叉樹,它的每個結點都存放著一個整數值。找出路徑和等於給定數值的路徑總數。

路徑不需要從根結點開始,也不需要在葉結點結束,但是路徑方向必須是向下的(只能從父結點到子結點)。

由於題目中的路徑可能位於二叉樹的中部,不少人會寫出這樣的題解代碼:

public int pathSum(TreeNode root, int sum) {
    if (root == null) {
        return 0;
    }
    return rootPathSum(root, sum)
        + pathSum(root.left, sum)
        + pathSum(root.right, sum);
}

int rootPathSum(TreeNode root, int sum) {
    if (root == null) {
        return 0;
    }
    int target = sum - root.val;
    return (target == 0 ? 1 : 0) 
        + rootPathSum(root.left, target)
        + rootPathSum(root.right, target);
}

這是一個典型的「雙遞歸」代碼,存在 pathSum 和 rootPathSum 兩個遞歸函數,它們會相互調用。這樣,一個結點會不止遍歷一次,算法的時間複雜度就不能簡單地用

也就是說,如果想在二叉樹問題中寫出

回溯法:看結果的個數

回溯法類題目有一個非常簡單的時間複雜度分析指南:有多少個結果,時間複雜度就是多少。例如:

LeetCode 78. Subsets,求數組所有可能的子集。對於大小為

為什麼是這樣分析的呢?這是因為回溯法的原理就是不斷嘗試可能的結果,遇到走不通的地方再撤回(回溯)嘗試下一個方案。那麼,回溯法嘗試的次數和結果的次數是相對應的。我們可以根據結果的數量級來判斷回溯法的時間複雜度。

需要注意的時,回溯算法的時間複雜度一般都很大,如

DFS/BFS:看元素的個數

很多同學還沒有掌握 DFS/BFS 類題目分析的竅門。其實這類題目的分析方法很簡單:不論是 DFS 還是 BFS,都是用來遍歷所有元素的,那麼算法的時間複雜度就是元素的個數!例如:

如果是遍歷二叉樹,設二叉樹的結點個數為

DFS 代碼一般是使用遞歸,只能使用整體法。而 BFS 代碼中一般會有 1~2 層的循環。有的同學可能會疑惑於究竟使用數循環法還是整體法。例如 BFS 層序遍歷:

// 二叉樹的層序遍歷
void bfs(TreeNode root) {
    Queue<TreeNode> queue = new ArrayDeque<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        int n = queue.size();
        for (int i = 0; i < n; i++) { 
            // 變量 i 無實際意義,只是為了循環 n 次
            TreeNode node = queue.poll();
            if (node.left != null) {
                queue.add(node.left);
            }
            if (node.right != null) {
                queue.add(node.right);
            }
        }
    }
}

(關於 BFS 層序遍歷的詳細講解,參見文章:LeetCode 例題精講 | 13 BFS 的使用場景:層序遍歷、最短路徑問題)

在這段代碼中,使用了兩層循環,乍一看是平方級別的時間複雜度。然而,內層循環的次數

如果用整體法來分析的話。考慮隊列 queue,二叉樹的每個結點都會進隊一次、出隊一次。那麼循環的次數恰好也是二叉樹的結點數。設二叉樹的結點個數是

因此,我們在分析 BFS 算法的時間複雜度時,要使用整體法,而不要關注具體的循環。

總結

本文介紹了兩種時間複雜度分析方法:「數循環法」與「整體法」,用於分析六類題型:動態規劃、數組、鍊表、二叉樹、回溯法、DFS/BFS。基本上已經涵蓋了常見的算法題目種類。

準備面試的時間總是有限的。在準備算法題時,我們的主要精力還是要放在題目的解法上。時間複雜度分析的學習不應追求面面俱到,而是先做到能上手分析,後續再不斷補充學習。希望本文列舉的時間複雜度分析方法能夠對你有所幫助。

關於分治法、二分搜索的時間複雜度,由於分析方法比較複雜,在後面會有專門的文章詳細講解,敬請期待。

以下是與六類題型相關的往期文章分類回顧:

動態規劃:

數組:

鍊表:

二叉樹:

回溯法:

DFS/BFS:

---

由 五分鐘學算法 原班人馬打造的公眾號:圖解面試算法,現已正式上線!接下來我們將會在該公眾號上,為大家分享優質的算法解題思路,堅持每天一篇原創文章的輸出,視頻動畫製作不易,感興趣的小夥伴可以關注點讚一下哈!

相關焦點

  • 時間複雜度的分析及排序算法總結
    ,這種複雜度⽆論數據規模n如何增⻓,計算時間是不變的。不管n如何增⻓,都不會影響到這個函數的計算時間,因此這個代碼的時間複雜度都是O(1)。這其中典型代表就是歸併排序,快速排序,堆排序,希爾排序。一個例子,來帶大家逐步分析遞歸算法的時間複雜度,最後找出最優解,來看看同樣是遞歸,怎麼就寫成了O(n)的代碼。
  • 時間複雜度的表示、分析、計算方法……一文帶你看懂時間複雜度!
    名詞解釋:在計算機科學中,時間複雜性,又稱時間複雜度,算法的時間複雜度是一個函數,它定性描述該算法的運行時間。這是一個代表算法輸入值的字符串的長度的函數。時間複雜度常用大O符號表述,不包括這個函數的低階項和首項係數。
  • 算法的時間複雜度和空間複雜度-總結
    而在證明算法是正確的基礎上,第二部就是分析算法的時間複雜度。算法的時間複雜度反映了程序執行時間隨輸入規模增長而增長的量級,在很大程度上能很好反映出算法的優劣與否。因此,作為程式設計師,掌握基本的算法時間複雜度分析方法是很有必要的。算法執行時間需通過依據該算法編制的程序在計算機上運行時所消耗的時間來度量。而度量一個程序的執行時間通常有兩種方法。
  • 從經典算法題看時間複雜度
    算法的時間複雜度(Time complexity)是一個函數,用於定性描述算法的運行時間。提出時間複雜度的目的是:分析與比較完成同一個任務而設計的不同算法。分析算法的結果意味著算法需要的資源,雖然有時我們關心像內存,通信帶寬或者計算機硬體這類資源,但是通常我們想要度量的是計算時間。
  • 關於時間複雜度,你不知道的都在這裡!
    同樣的同理再看一下快速排序,都知道快速排序是O(nlogn),但是當數據已經有序情況下,快速排序的時間複雜度是O(n^2) 的,「所以嚴格從大O的定義來講,快速排序的時間複雜度應該是O(n^2)」。「但是我們依然說快速排序是O(nlogn)的時間複雜度,這個就是業內的一個默認規定,這裡說的O代表的就是一般情況,而不是嚴格的上界」。
  • 評估算法及算法的時間複雜度
    在實時系統中,對系統響應時間要求高,則儘量選用執行時間少的算法;當數據處理量大,而存儲空間較少時,則儘量選用節省空間的算法。本文主要討論算法的時間特性,並給出算法在時間複雜度上的度量指標。若有某個輔助函數f(n),當n趨向於無窮大時,如果T(n)/ f(n)的極限為不等於零的常數,則認為T(n)與f(n)是同量級的函數,記作:T(n) =O(f(n)),O(f(n))稱為算法的漸進時間複雜度,簡稱時間複雜度。
  • Web前端面試題:如何分析時間複雜度-開課吧
    問題:如何分析時間複雜度?O(1) 傳說中的常數階的複雜度,這種複雜度無論數據規模n如何增長,計算時間是不變的。const increment = n => n++舉個簡單的例:不管n如何增長,都不會影響到這個函數的計算時間,因此這個代碼的時間複雜度都是O(1)。
  • 算法的時間複雜度到底如何計算
    此時為了 估算算法需要的運行時間 和 簡化算法分析,我們引入時間複雜度的概念。定義:存在常數 c 和函數 f(N),使得當 N >= c 時 T(N) <= f(N),表示為 T(n) = O(f(n)) 。
  • 【皮皮灰】考研數據結構中關於時間複雜度的計算
    考研中考察重點:選擇題中關於時間複雜度的計算,算法題中對算法有時間複雜度的要求。本文共4350字,通過閱讀本文你可以get:1.時間複雜度的概念以及皮皮灰的一些看法。2.非遞歸函數時間複雜度的計算。3.遞歸函數時間複雜度的計算。
  • 算法的時間複雜度
    算法的時間與空間複雜度事後分析法缺點:不同的數據規模,不同的機器下算法運行的時間不同,無法做到計算運行時間事前分析法大O時間複雜度漸進時間複雜度 隨著n的增長,程序運行時間跟隨n變化的趨勢幾個原則去掉常數項2(n^2) =n^2一段代碼取時間複雜度最高的test(n) { //時間複雜度n^3 for(int i = 0; i < n ; i++){ for(int i = 0; i
  • 可能是最可愛的一文讀懂系列:皮卡丘の複雜度分析指南
    如何做「複雜度分析「?3. 從排序算法說起4. 遞歸算法分析5. 如何選擇最好的算法?其中,1、2部分會對複雜度分析做簡單介紹(熟悉複雜度分析基本概念的同學可以跳直部分3)。部分3會以排序算法為例,給出複雜度分析的經典案例。 部分4會重點分析遞歸算法,並介紹遞歸算法複雜度分析的兩種方法:「遞歸樹法」和更通用簡潔的「主定理法」。
  • 新手如何快速入門數據分析?
    CDA數據分析研究院原創作品, 轉載需授權隨著網際網路迅猛發展,各大公司沉澱了很多的數據,如何找出藏在這些數據背後的規律,利用這些數據來給公司創造價值,作為一個新手面對這些問題的時候,你是不是考慮怎麼快速學習數據分析呢?
  • 一種混沌組合序列密碼電路設計與複雜度分析方法
    隨著現代科學技術的發展,將神經網絡、混沌等算法融入密碼學的研究已不斷深入,伴隨著數位化技術和大規模集成電路的快速發展,一些算法不僅停止在理論研究與模擬仿真實驗上,利用硬體電路進行設計並實現已逐漸成為事實。如何衡量加密晶片的複雜度是應該研究的問題,當加密晶片被敵方得到並破譯時,以高複雜度保持加密信息的安全性是加密晶片成功設計的關鍵。
  • 數據分析,從認知事物的基本方法,分類法開始!第2輯
    解構事物的三要素——要素、屬性和行為維度分類法屬性分類法流程分類法層級分類法分類中的權重設定問題5.2 解構事物的三要素要素、屬性和行為的解構方法是認識事物的基本方法,在日常工作中並不見得要嚴格按照這種方法來解構事物,特別是非專業的數據分析人員,直接根據自己對事物的理解來解構需要分析的主體即可。如果是專業的數據分析人員,在對企業經營管理中的各種數據構建數學模型時,這個方法就變得非常重要了,它能夠大幅度拓寬我們解構分析主體的思路。
  • 谷歌分析(快速入門)課程開課啦!
    觸脈谷歌分析(快速入門)課程即日起開始招生!
  • 項目分析的有效度與複雜度關係,以Cover舉例
    剛開始隨著複雜性上升,有效度也會上升,然後維度越來越多,單點研究越來越深,達到瓶頸,然後複雜度繼續提升,有效度反而會下降。DeFi領域的小幣種研究維度很多,實際投資的時候,為了快速把握幾個或者幾十個項目,沒有那麼多時間各個維度全部看一遍。說是各個維度,也有很多維度無法把握,例如,我也不具備研究代碼漏洞的技術能力——如果你有個技術團隊,加上幾個研究員分工協作或許會好一些。當然,即使有了團隊,模型複雜度上來了,也請回歸第一條複雜度和有效度的理論。
  • 原創 | 時間複雜度不理解?看這個一定懂!
    😂小白: 嗯嗯,是的,我也上網搜了很多的關於複雜度的分析的文章,有些文章覺得越看越迷糊,看著看著就看不下去了,然後我就去看看評論,想著會有人跟我一樣覺得看不懂,結果評論都是「寫的很好」😂,頓時覺得自己是不是太笨了🤣。
  • 時空複雜度(時間複雜度/空間複雜度)O(1)、O(n)、O(n^2)、O(log n)、O(n log n)是什麼意思?
    它描述了時空複雜度.大O符號是我在大學裡學過的東西之一,我了解過這個算法的概念。我知道的不算多,可以回答一些基本的問題,僅此而已。從大學畢業以後,我對這個算法的了解基本沒有改變,因為自從我開始工作以來,我沒有使用過它,也沒有聽到任何同事提到過它。所,我想我應該花點時間回顧一下它,並在這篇文章中總結大O符號的基礎知識,以及一些代碼示例來幫助解釋它。什麼是大O符號?
  • 通過一道面試題目,講一講遞歸算法的時間複雜度!
    如果對遞歸的時間複雜度理解的不夠深入的話,就會這樣!那麼我通過一道簡單的面試題,模擬面試的場景,來帶大家逐步分析遞歸算法的時間複雜度,最後找出最優解,來看看同樣是遞歸,怎麼就寫成了O(n)的代碼。每次n-1,遞歸了n次時間複雜度是O(n),每次進行了一個乘法操作,乘法操作的時間複雜度一個常數項O(1),所以這份代碼的時間複雜度是 n * 1 = O(n)。這個時間複雜度就沒有達到面試官的預期。
  • 常用文獻分類法之《中國圖書館分類法》
    華譯SCI服務:醫學SCI論文、基金標書、母語翻譯潤色、實驗數據分析服務,如需了解詳情,請加我們售前客服QQ/微信:678677。 《中國圖書館分類法》(原名《中國圖書館圖書分類法》,簡稱《中圖法》)是科學分類和知識分類為基礎,並結合文獻內容特點及其某些外表特徵進行邏輯劃分和系統排架列的類目表。它是類分文獻、組織文獻、分類排架、編制分類檢索系統的工具。