算法的時間復雜度與空間復雜度(清晰簡潔一次看懂)


時間復雜度

概念定義

根據定義,時間復雜度指輸入數據大小為 N 時,算法運行所需花費的時間。需要注意:(重點在輸入數據的大小上,如果跟輸入數據無關則不考慮)

統計的是算法的「計算操作數量」,而不是「運行的絕對時間」。計算操作數量和運行絕對時間呈正相關關系,並不相等。算法運行時間受到「編程語言 、計算機處理器速度、運行環境」等多種因素影響。例如,

同樣的算法使用 Python 或 C++ 實現、使用 CPU 或 GPU 、使用本地 IDE 或力扣平台提交,運行時間都不同。

體現的是計算操作隨數據大小 N 變化時的變化情況。假設算法運行總共需要「 1次操作」、「100 次操作」,此兩情況的時間復雜度都為常數級 O(1) ;需要「 N 次操作」、「 100N次操作」的時間復雜度都為O(N)。

 

常見的時間復雜度的排序:

O(1)<O(logN)<O(N)<O(NlogN)<O(N2)<O(2N)<O(N!)

 

常數 O(1)運行次數與 N 大小呈常數關系,即不隨輸入數據大小 N的變化而變化。

int algorithm(int N) {
    int a = 1; int b = 2; int x = a * b + N; return 1; }

運行次數與 N 大小呈常數關系,即不隨輸入數據大小 N 的變化而變化。

 

int algorithm(int N) {
    int count = 0; int a = 10000; for (int i = 0; i < a; i++) { count++; } return count; }

對於以上代碼,無論 a 取多大,都與輸入數據大小無關,因此時間復雜度仍為 O(1)

 

線性 O(N) :循環運行次數與 N 大小呈線性關系,時間復雜度為 O(N) 。

對於以下代碼,雖然是兩層循環,但第二層與 N大小無關,因此整體仍與 N呈線性關系。

int algorithm(int N) {
    int count = 0; int a = 10000; for (int i = 0; i < N; i++) { for (int j = 0; j < a; j++) { count++; } } return count; }

 

 

平方 O(N^2) :兩層循環相互獨立,都與 N呈線性關系,因此總體與 N 呈平方關系,時間復雜度為 O(N^2)。

以冒泡排序為例,其包含了兩層相互獨立的循環。

1、第一層循環的復雜度為O(N)

2、第二層循環的復雜度為O(N)

因為冒泡排序的總體時間復雜度為O(N2)

int[] bubbleSort(int[] nums) {
    int N = nums.length; for (int i = 0; i < N - 1; i++) { for (int j = 0; j < N - 1 - i; j++) { if (nums[j] > nums[j + 1]) { int tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; } } } return nums; }

 

指數 O(2^N)  :
生物學科中的 “細胞分裂” 即是指數級增長。初始狀態為 1 個細胞,分裂一輪后為 2 個,分裂兩輪后為 4 個,……,分裂 N 輪后有 2^N個細胞。

算法中,指數階常出現於遞歸,算法原理圖與代碼如下所示。

int algorithm(int N) {
    if (N <= 0) return 1; int count_1 = algorithm(N - 1); int count_2 = algorithm(N - 1); return count_1 + count_2; }

 

 

 

階乘 O(N!) :

階乘階對應數學上常見的 “全排列” 。即給定 N 個互不重復的元素,求其所有可能的排列方案,則方案數量為:

N×(N−1)×(N−2)×⋯×2×1=N!

如下圖與代碼所示,階乘常使用遞歸實現,算法原理:第一層分裂出 N 個,第二層分裂出 N - 1 個,…… ,直至到第 N 層時終止並回溯。

int algorithm(int N) {
    if (N <= 0) return 1; int count = 0; for (int i = 0; i < N; i++) { count += algorithm(N - 1); } return count; }

 

 

 

對數 O(logN) :

對數階與指數階相反,指數階為 “每輪分裂出兩倍的情況” ,而對數階是 “每輪排除一半的情況” 。對數階常出現於「二分法」、「分治」等算法中,體現着 “一分為二” 或 “一分為多” 的算法思想。

設循環次數為 m ,則輸入數據大小 N與 2 ^ m呈線性關系,兩邊同時取 log_2對數,則得到循環次數 m與 log2​N 呈線性關系,即時間復雜度為 O(logN) 。

int algorithm(int N) {
    int count = 0; float i = N; while (i > 1) { i = i / 2; count++; } return count; }

 

 

線性對數 O(NlogN) :

兩層循環相互獨立,第一層和第二層時間復雜度分別為O(logN) 和O(N) ,則總體時間復雜度為 O(NlogN) ;

int algorithm(int N) {
    int count = 0; float i = N; while (i > 1) { i = i / 2; for (int j = 0; j < N; j++) count++; } return count; }

 

 

 

 

空間復雜度

概念定義
空間復雜度涉及的空間類型有:

輸入空間: 存儲輸入數據所需的空間大小;

暫存空間: 算法運行過程中,存儲所有中間變量和對象等數據所需的空間大小;

輸出空間: 算法運行返回時,存儲輸出數據所需的空間大小;

通常情況下,空間復雜度指在輸入數據大小為 NN 時,算法運行所使用的「暫存空間」+「輸出空間」的總體大小。

 

根據來源不同,算法使用的內存空間分為三類:

指令空間:編譯后,程序指令使用的內存空間。

數據空間:算法中的各項變量使用的空間,包括:聲明的常量、變量、動態數組、動態對象等使用的內存空間。

棧幀空間:程序調用函數是基於棧實現的,函數在調用期間,占用常量大小的棧幀空間,直至返回后釋放。如以下代碼所示,在循環中調用函數,每輪調用 test() 返回后,棧幀空間已被釋放,因此空間復雜度仍為 O(1) 。

 

 

符號表示
通常情況下,空間復雜度統計算法在 “最差情況” 下使用的空間大小,以體現算法運行所需預留的空間量,使用符號 O 表示。

最差情況有兩層含義,分別為「最差輸入數據」、算法運行中的「最差運行點」。例如以下代碼:

最差輸入數據: 當 N≤10 時,數組 nums 的長度恆定為 10 ,空間復雜度為 O(10) = O(1)=O(1) ;當 N>10 時,數組 nums 長度為 N ,空間復雜度為 O(N) ;因此,空間復雜度應為最差輸入數據情況下的 O(N) 。

最差運行點: 在執行 nums = [0] * 10 時,算法僅使用 O(1)大小的空間;而當執行 nums = [0] * N 時,算法使用 O(N) 的空間;因此,空間復雜度應為最差運行點的 O(N) 。

 

常數O(1)

普通常量、變量、對象、元素數量與輸入數據大小 NN 無關的集合,皆使用常數大小的空間。

void algorithm(int N) {
    int num = 0; int[] nums = new int[10000]; Node node = new Node(0); Map<Integer, String> dic = new HashMap<>() {{ put(0, "0"); }}; }

 

 

如以下代碼所示,雖然函數 test() 調用了 N次,但每輪調用后 test() 已返回,無累計棧幀空間使用,因此空間復雜度仍為O(1) 。

void algorithm(int N) {
    for (int i = 0; i < N; i++) { test(); } }

 

 

線性 O(N) :

元素數量與 N呈線性關系的任意類型集合(常見於一維數組、鏈表、哈希表等),皆使用線性大小的空間。

void algorithm(int N) {
    int[] nums_1 = new int[N]; int[] nums_2 = new int[N / 2]; List<Node> nodes = new ArrayList<>(); for (int i = 0; i < N; i++) { nodes.add(new Node(i)); } Map<Integer, String> dic = new HashMap<>(); for (int i = 0; i < N; i++) { dic.put(i, String.valueOf(i)); } }

 

 

如下圖與代碼所示,此遞歸調用期間,會同時存在 N 個未返回的 algorithm() 函數,因此使用O(N) 大小的棧幀空間。

int algorithm(int N) {
    if (N <= 1) return 1; return algorithm(N - 1) + 1; }

 

 

平方 O(N^2) :

元素數量與 N 呈平方關系的任意類型集合(常見於矩陣),皆使用平方大小的空間。

void algorithm(int N) {
    int num_matrix[][] = new int[N][N]; List<List<Node>> node_matrix = new ArrayList<>(); for (int i = 0; i < N; i++) { List<Node> nodes = new ArrayList<>(); for (int j = 0; j < N; j++) { nodes.add(new Node(j)); } node_matrix.add(nodes); } }

 

 

如下圖與代碼所示,遞歸調用時同時存在 N個未返回的 algorithm() 函數,使用 O(N) 棧幀空間;每層遞歸函數中聲明了數組,平均長度為N/2 使用 O(N)空間;因此總體空間復雜度為 O(N^2)。

int algorithm(int N) {
    if (N <= 0) return 0; int[] nums = new int[N]; return algorithm(N - 1); }

 

 

指數 O(2^N)

指數階常見於二叉樹、多叉樹。例如,高度為 N 的「滿二叉樹」的節點數量為 2^N,占用 O(2^N)大小的空間;同理,高度為 N 的「滿 m 叉樹」的節點數量為 m^N,占用 O(m^N) = O(2^N)=O(2N) 大小的空間。

 

 

對數 O(logN) :
對數階常出現於分治算法的棧幀空間累計、數據類型轉換等,例如:快速排序 ,平均空間復雜度為 Θ(logN) ,最差空間復雜度為 O(N)。拓展知識:通過應用 Tail Call Optimization ,可以將快速排序的最差空間

復雜度限定至 O(N) 。

數字轉化為字符串 ,設某正整數為 N ,則字符串的空間復雜度為O(logN) 。推導如下:正整數 N的位數為log10N ,即轉化的字符串長度為 log10​N ,因此空間復雜度為 O(logN) 。

 

時空權衡
對於算法的性能,需要從時間和空間的使用情況來綜合評價。優良的算法應具備兩個特性,即時間和空間復雜度皆較低。而實際上,對於某個算法問題,同時優化時間復雜度和空間復雜度是非常困難的。降低時間復雜度,往往是以提升空間復雜度為代價的,反之亦然。

由於當代計算機的內存充足,通常情況下,算法設計中一般會采取「空間換時間」的做法,即犧牲部分計算機存儲空間,來提升算法的運行速度。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM