恋上数据结构与算法:复杂度(一)

第一阶段的大纲

恋上数据结构与算法:复杂度(一)_第1张图片

文章目录

(一)开发环境搭建
(二)斐波那契数
(三)算法的评估
(四)时间复杂度的估算
(五)大O表示法
(六)斐波那契数复杂度分析
(七)leetcode

(一)开发环境搭建

恋上数据结构与算法:复杂度(一)_第2张图片
恋上数据结构与算法:复杂度(一)_第3张图片

(二)斐波那契数

题目:求第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));
    }

效果如下:
恋上数据结构与算法:复杂度(一)_第4张图片
看似没有问题,但是当n比较大的时候,就有问题了,如下:
恋上数据结构与算法:复杂度(一)_第5张图片
可以看到已经卡死了,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));
    }

效果如下:
恋上数据结构与算法:复杂度(一)_第6张图片
我们使用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));
            }
        });
    }

对比如下:
恋上数据结构与算法:复杂度(一)_第7张图片

(三)算法的评估

代码的长短并不能反映代码的性能,如下:
恋上数据结构与算法:复杂度(一)_第8张图片
分析: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

  • 常见的复杂度
    恋上数据结构与算法:复杂度(一)_第9张图片

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

  • 可以借助函数生成工具对比复杂度的大小:https://zh.numberempire.com/graphingcalculator.php
    恋上数据结构与算法:复杂度(一)_第10张图片
    恋上数据结构与算法:复杂度(一)_第11张图片

  • 对之前的代码用大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);
		}
  • 多个数据规模的情况
    恋上数据结构与算法:复杂度(一)_第12张图片
  • 对之前的代码用大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);
    }

恋上数据结构与算法:复杂度(一)_第13张图片

(七)leetcode

  • 一个用于练习算法的好网站
    https://leetcode.com/
    https://leetcode-cn.com/
  • 斐波那契数
    https://leetcode-cn.com/problems/fibonacci-number/

你可能感兴趣的:(恋上数据结构与算法:第一季)