戀上數據結構與算法:複雜度(一)

第一階段的大綱

在這裏插入圖片描述

文章目錄

(一)開發環境搭建
(二)斐波那契數
(三)算法的評估
(四)時間複雜度的估算
(五)大O表示法
(六)斐波那契數複雜度分析
(七)leetcode

(一)開發環境搭建

在這裏插入圖片描述
在這裏插入圖片描述

(二)斐波那契數

題目:求第n個斐波那契數(fibonacci number)

分析:斐波那契數就是下一個數等於前面兩個數相加,如:0 1 1 2 3 5 8 13 …

我們先用遞歸(自己調用自己)實現,如下:

    public static int fib1(int n) {
        if (n <= 1) return n;
        return fib1(n - 1) + fib1(n - 2);
    }

    public static void main(String[] args) {
        System.out.println(fib1(0));
        System.out.println(fib1(1));
        System.out.println(fib1(2));
        System.out.println(fib1(3));
        System.out.println(fib1(4));
    }

效果如下:
在這裏插入圖片描述
看似沒有問題,但是當n比較大的時候,就有問題了,如下:
在這裏插入圖片描述
可以看到已經卡死了,CPU的風扇狂轉,我們換一種算法試試

分析

n:     0 1 2 3 4 5
fib(n):0 1 1 2 3 5 8 13 ...

當n=2時,需要相加運算一次
當n=3時,需要相加運算兩次

所以我們需要相加n-1次

這種算法其實就是迭代,如下:

    public static int fib2(int n) {
        if (n <= 1) return n;

        int first = 0;
        int second = 1;
        for (int i = 0; i < n - 1; i++) {
            int sum = first + second;
            first = second;//第二個數 等於 下一次循環的第一個數
            second = sum;//前面兩個數相加的結果 等於 下一次循環的第二個數
        }
        return second;
    }

    public static void main(String[] args) {
        System.out.println(fib2(64));
    }

效果如下:
在這裏插入圖片描述
我們使用TimeTool工具類來對比兩種算法的運行時間,如下:

package com.zzq;

import java.text.SimpleDateFormat;
import java.util.Date;

public class TimeTool {
	private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS");
	
	public interface Task {
		void execute();
	}
	
	public static void check(String title, Task task) {
		if (task == null) return;
		title = (title == null) ? "" : ("【" + title + "】");
		System.out.println(title);
		System.out.println("開始:" + fmt.format(new Date()));
		long begin = System.currentTimeMillis();
		task.execute();
		long end = System.currentTimeMillis();
		System.out.println("結束:" + fmt.format(new Date()));
		double delta = (end - begin) / 1000.0;
		System.out.println("耗時:" + delta + "秒");
		System.out.println("-------------------------------------");
	}
}
    public static void main(String[] args) {
        int n = 40;
        TimeTool.check("遞歸", new TimeTool.Task() {
            @Override
            public void execute() {
                System.out.println(fib1(n));
            }
        });
        TimeTool.check("迭代", new TimeTool.Task() {
            @Override
            public void execute() {
                System.out.println(fib2(n));
            }
        });
    }

對比如下:
在這裏插入圖片描述

(三)算法的評估

代碼的長短並不能反映代碼的性能,如下:
在這裏插入圖片描述
分析:fib1用的是遞歸,代碼量比較少,但是性能遠不如fib2的迭代

如果單從執行效率上進行評估,可能會想到這麼一種方案:比較不同算法對同一組輸入的執行處理時間,這種方案也叫做:事後統計法

但是這種方法有明顯的缺點

  1. 執行時間嚴重依賴硬件以及運行時各種不確定的環境因素
  2. 必須編寫相應的測算代碼
  3. 測試數據的選擇比較難保證公正性

一般從以下維度來評估算法的優劣

  1. 正確性
  2. 可讀性
  3. 健壯性(對不合理輸入的反應能力和處理能力)
  4. 時間複雜度(time complexity):估算程序指令的執行次數(執行時間)
  5. 空間複雜度(space complexity):估算所需佔用的存儲空間

(四)時間複雜度的估算

	public static void test1(int n) {
	
		// 1(因爲下面三個語句只會執行其中一個)
		if (n > 10) { 
			System.out.println("n > 10");
		} else if (n > 5) { // 2
			System.out.println("n > 5");
		} else {
			System.out.println("n <= 5"); 
		}
		
		// 1 + 4 + 4 + 4
		// 13
		for (int i = 0; i < 4; i++) {
			System.out.println("test");
		}
		// 14
	}

	public static void test2(int n) {
		// 1 + 3n
		for (int i = 0; i < n; i++) {
			System.out.println("test");
		}
	}

	public static void test3(int n) {
		// 1 + 2n + n * (1 + 3n)
		// 1 + 2n + n + 3n^2
		// 3n^2 + 3n + 1
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test4(int n) {
		// 1 + 2n + n * (1 + 45)
		// 1 + 2n + 46n
		// 48n + 1
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < 15; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test5(int n) {
		//執行過程中n的變化: 8 (4 2 1) 0 , 括號中的有效
		//就是看除多少次小於或等於0
		// 8 = 2^3
		// 16 = 2^4
		
		// 3 = log2(8)
		// 4 = log2(16)
		
		// 執行次數 = log2(n)
		while ((n = n / 2) > 0) {
			System.out.println("test");
		}
	}

	public static void test6(int n) {
		// log5(n)
		while ((n = n / 5) > 0) {
			System.out.println("test");
		}
	}

	public static void test7(int n) {
		// 1 + 2*log2(n) + log2(n) * (1 + 3n)
		// 1 + 3*log2(n) + 2 * nlog2(n)
		for (int i = 1; i < n; i += i) { // i+=i 相當於 i=i*2 , 就是看乘多少次大於或等於n , n=1*2*2*2.... , 也是log2n的關係
			// 1 + 3n
			for (int j = 0; j < n; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test10(int n) {
		// 1+3n
		int a = 10;
		int b = 20;
		int c = a + b;
		int[] array = new int[n];
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i] + c);
		}

(五)大O表示法

  • 忽略常數、係數、低階

      9 >> O(1)
      2n + 3 >> O(n)
      n2 + 2n + 6 >> O(n2)
      4n3 + 3n2 + 22n + 100 >> O(n3)
    
  • 對數階的細節

      log2n = log29 ∗ log9n
    

    所以 log2n 、log9n 統稱爲 logn

  • 常見的複雜度
    在這裏插入圖片描述

  • 複雜度排序:O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!) < O(nn)

  • 可以藉助函數生成工具對比複雜度的大小:https://zh.numberempire.com/graphingcalculator.php
    在這裏插入圖片描述
    在這裏插入圖片描述

  • 對之前的代碼用大O表示法估算時間複雜度

	public static void test1(int n) {
	
		// 1(因爲下面三個語句只會執行其中一個)
		if (n > 10) { 
			System.out.println("n > 10");
		} else if (n > 5) { // 2
			System.out.println("n > 5");
		} else {
			System.out.println("n <= 5"); 
		}
		
		// 1 + 4 + 4 + 4
		// 13
		for (int i = 0; i < 4; i++) {
			System.out.println("test");
		}
		// 14
		// O(1)
	}

	public static void test2(int n) {
		// 1 + 3n
		// O(n)
		for (int i = 0; i < n; i++) {
			System.out.println("test");
		}
	}

	public static void test3(int n) {
		// 1 + 2n + n * (1 + 3n)
		// 1 + 2n + n + 3n^2
		// 3n^2 + 3n + 1
		// O(n^2)
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test4(int n) {
		// 1 + 2n + n * (1 + 45)
		// 1 + 2n + 46n
		// 48n + 1
		// O(n)
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < 15; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test5(int n) {
		//執行過程中n的變化: 8 (4 2 1) 0 , 括號中的有效
		//就是看除多少次小於或等於0
		// 8 = 2^3
		// 16 = 2^4
		
		// 3 = log2(8)
		// 4 = log2(16)
		
		// 執行次數 = log2(n)
		// O(logn)
		while ((n = n / 2) > 0) {
			System.out.println("test");
		}
	}

	public static void test6(int n) {
		// log5(n)
		// O(logn)
		while ((n = n / 5) > 0) {
			System.out.println("test");
		}
	}

	public static void test7(int n) {
		// 1 + 2*log2(n) + log2(n) * (1 + 3n)
		// 1 + 3*log2(n) + 2 * nlog2(n)
		// O(nlogn)
		for (int i = 1; i < n; i += i) { // i+=i 相當於 i=i*2 , 就是看乘多少次大於或等於n , n=1*2*2*2.... , 也是log2n的關係
			// 1 + 3n
			for (int j = 0; j < n; j++) {
				System.out.println("test");
			}
		}
	}

	public static void test10(int n) {
		// 1+3n
		// O(n)
		int a = 10;
		int b = 20;
		int c = a + b;
		int[] array = new int[n];
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i] + c);
		}
  • 多個數據規模的情況
    在這裏插入圖片描述
  • 對之前的代碼用大O表示法估算空間複雜度,其它的空間複雜度都是O(1),下面這個除外:
	public static void test10(int n) {
		// O(n)
		int a = 10;
		int b = 20;
		int c = a + b;
		int[] array = new int[n];
		for (int i = 0; i < array.length; i++) {
			System.out.println(array[i] + c);
		}
	}

分析:n爲多少就向堆空間申請多少個int類型,所以空間複雜度是O(n)

(六)斐波那契數複雜度分析

迭代法是O(n)

    public static int fib2(int n) {
        if (n <= 1) return n;

        int first = 0;
        int second = 1;
        for (int i = 0; i < n - 1; i++) {
            int sum = first + second;
            first = second;//第二個數 等於 下一次循環的第一個數
            second = sum;//前面兩個數相加的結果 等於 下一次循環的第二個數
        }
        return second;
    }

下面分析遞歸法,如下:

    public static int fib1(int n) {
        if (n <= 1) return n;
        return fib1(n - 1) + fib1(n - 2);
    }

在這裏插入圖片描述

(七)leetcode

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章