第一階段的大綱
文章目錄
(一)開發環境搭建
(二)斐波那契數
(三)算法的評估
(四)時間複雜度的估算
(五)大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的迭代
如果單從執行效率上進行評估,可能會想到這麼一種方案:比較不同算法對同一組輸入的執行處理時間,這種方案也叫做:事後統計法
但是這種方法有明顯的缺點:
- 執行時間嚴重依賴硬件以及運行時各種不確定的環境因素
- 必須編寫相應的測算代碼
- 測試數據的選擇比較難保證公正性
一般從以下維度來評估算法的優劣:
- 正確性
- 可讀性
- 健壯性(對不合理輸入的反應能力和處理能力)
- 時間複雜度(time complexity):估算程序指令的執行次數(執行時間)
- 空間複雜度(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);
}