目录

  • 1. 前言
  • 2. 正文
    • 2.1 算法和数据结构
      • 2.1.1 什么是算法?
      • 2.1.2 衡量算法好坏最重要的标准有哪两个?
      • 2.1.3 什么是数据结构?
      • 2.1.4 算法和数据结构有什么关系呢?
      • 2.1.5 数据结构有哪些组成方式?
    • 2.2 时间复杂度
      • 2.2.1 如何评估算法时间上的优劣?
      • 2.2.2 什么是基本操作或者说常数操作?
      • 2.2.3 如何计算程序的基本操作次数?
      • 2.2.4 什么是时间复杂度?
      • 2.2.5 推导出时间复杂度的原则有哪些?
    • 2.3 空间复杂度
      • 2.3.1 什么是空间复杂度?
      • 2.3.2 如何计算空间复杂度?
      • 2.3.5 如何在时间复杂度和空间复杂度之间取舍?
  • 3. 最后
  • 参考

1. 前言

说实话,对于数据结构与算法,是存在畏惧心理的。但是,一点一点地学,总会能克服掉这种心理吧。

2. 正文

2.1 算法和数据结构

2.1.1 什么是算法?

算法,对应的英文单词是 algorithm,最早来自数学领域。

在数学领域,算法是用于解决某一类问题的公式和思想。如等差数列的公式:

在计算机科学领域,算法的本质是一系列程序指令,用于解决特定的运算和逻辑问题。如给出一系列整数,找出最大的整数,或者给出一系列整数,按从小到大排序。

2.1.2 衡量算法好坏最重要的标准有哪两个?

时间复杂度和空间复杂度。

考量一个算法好坏主要从算法所占用的时间和空间两个维度去考量。

  • 时间维度:是指执行当前算法所消耗的时间,通常用时间复杂度来描述;
  • 空间维度:是指执行当前算法需要占用多少内存空间,通常用空间复杂度来描述。

2.1.3 什么是数据结构?

数据结构,对应的英文单词是 data structure,是数据的组织、管理和存储格式,使用数据结构的目的是为了更加高效地访问和修改数据。

2.1.4 算法和数据结构有什么关系呢?

数据结构是算法的基石。如果把算法比作美丽灵动的舞者,那么数据结构就是舞者脚下广阔而坚实的舞台。
数据结构和算法是互不分开的。离开了算法,数据结构就显得毫无意义;而没有了数据结构,算法就没有实现的条件了。在解决问题时,不同的算法会选用不同的数据结构。

2.1.5 数据结构有哪些组成方式?

  • 线性结构:最简单的基本数据结构,包括数组、链表,以及由它们衍生出来的栈、队列、哈希表;
  • :相对复杂的基本数据结构,包括二叉树,以及由它衍生出来的二叉堆等;
  • :更为复杂的基本数据结构;
  • 其他数据结构:由基本数据结构变形而来,用于解决某些特定问题,如跳表、哈希链表、位图等。

2.2 时间复杂度

2.2.1 如何评估算法时间上的优劣?

通过统计代码的绝对执行时间:代码的绝对执行时间只有在实际运行后才能得到,但是绝对执行时间会受到运行环境(机器性能的高低)和输入规模(数据规模的大小)的影响,所以通过比较代码的绝对执行时间来确定算法的时间复杂度有很大的局限性。

通过预估代码的基本操作次数:这需要对一个算法流程非常熟悉,然后写出这个算法流程中,发生了多少次基本执行操作,进而总结出基本操作次数的表达式。基本操作次数的表达式会转化为时间复杂度指标来表示。

如果时间复杂度指标无法区分算法好坏,就需要通过实际运行代码的方式来比较算法好坏了。

2.2.2 什么是基本操作或者说常数操作?

如果一个操作花费的时间是固定的并且和样本的数据量没有关系,就把这样的操作称为基本操作,或常数操作。

常见的基本操作有:

  • 从数组里面取出一个元素;
  • 加减乘除运算;
  • 位运算。

常见的非基本操作有:

  • 从链表上面取出一个元素(跟链表长度有关);
  • 在数组上查找最小值(跟数组长度有关)。

2.2.3 如何计算程序的基本操作次数?

这里分场景来举例说明。

场景1:

从链表中取出第 i 个元素,每次查找下一个节点耗时为 1 时间单位:

public static void case1() {LinkedList<Integer> list = new LinkedList<>();for (int i = 0; i < 2000; i++) {list.add(i);}// 取出第 1 个元素Integer a = list.get(0);// 取出第 999 个元素Integer c = list.get(999);
}

则基本操作次数表达式 T(n) = n,因为第 n 个元素需要从链表头查找 n 次才可以获取到。

场景2:
给定一个整数 n,求初始值为 1 的整数乘以多少次 2 才可以达到 n

public static void case2() {int i = 1;int n = 16;while (i < n) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}i = i * 2;}System.out.println("i = " + i);
}

这里我们把循环内的休眠时间和 i = i * 2 作为一个大的常数操作,而忽略掉 i < n 这个常数操作时间。
可以知道,
如果 n = 2,则 T = 1;
如果 n = 4,则 T = 2;
如果 n = 16,则 T = 4;
总结一下,T(n) = log2^n

场景3:
从数组中取出第 i 个元素,每次挪动指针耗时为 1 时间单位:

public static void case3() {int[] arr = new int[2000];for (int i = 0; i < 2000; i++) {arr[i] = i;}// 实际上第一次挪动比较耗时int a = arr[0];// 以后的获取耗时相当int b = arr[1];int c = arr[1999];}

T(n) = 1 时间单位。

场景4:
打印 nn 列的星号:

public static void case4() {int n = 6;for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {System.out.print("*");}System.out.println();}
}

打印第 1 行,需要 1 次 int i = 1; 赋值操作;1 次 i <= n 比较;n 次 j <= n 比较;n 次 j++ 操作;n 次 System.out.print("*");,1 次System.out.println(); 换行操作 ,总共需要 3n + 3 次常数操作;
打印第 2 行,需要 1 次 i++ 操作;1 次 i <= n 比较; n 次 j <= n 比较;n 次 j++ 操作;n 次 System.out.print("*");,1 次System.out.println(); 换行操作 ,总共需要 3n + 3 次常数操作;

打印第 n 行,需要 1 次 i++ 操作;1 次 i <= n 比较; n 次 j <= n 比较;n 次 j++ 操作;n 次 System.out.print("*");,1 次System.out.println(); 换行操作 ,总共需要 3n + 3 次常数操作。
最后还需要 1 次 i <= n 比较;结束循环。

所以,总共需要的常数操作次数 T(n) = (3n + 3) * n + 1 = 3n^2 + 3n + 1

2.2.4 什么是时间复杂度?

若存在函数 f(n),使得当 n 趋于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称 f(n) 是 T(n) 的同数量级函数。记作 T(n) = O(f(n)),称为O(f(n)),O为算法的渐进时间复杂度,简称为时间复杂度。

因为渐进时间复杂度用大写O来表示,所以也被称为大O表示法。大O用来表示上界,表示了算法的最坏情况运行时间。

2.2.5 推导出时间复杂度的原则有哪些?

  • 如果运行时间是常数量级,则用常数 1 表示;
  • 只保留时间函数中的最高阶项;
  • 如果最高阶项存在,则省去最高阶项前面的系数。

场景1:T(n) = n。
不是常数量级,最高阶项是 n,则转化的时间复杂度为:T(n)=O(n);

场景2:T(n) = log2^n。
不是常数量级,最高阶项是 log2^n,省略常数 2,则转化的时间复杂度为:T(n)=O(logn);

场景3:T(n) = 1。
是常数量级,则转化的时间复杂度为:T(n)=O(1);

场景4:T(n) = 3n^2 + 3n + 1。
不是常数量级,最高阶项是 3n^2,去除系数 3,则转化的时间复杂度为:T(n)=O(n ^ 2);

2.3 空间复杂度

2.3.1 什么是空间复杂度?

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,同样使用大O(读作欧,不是零)表示法。

程序占用空间大小的计算公式记作 S(n) = O(f(n)),其中 n 为问题的规模,f(n) 为算法所占存储空间的函数。

2.3.2 如何计算空间复杂度?

这里需要分情况来说明。

场景1:常量空间

当算法分配的存储空间大小是固定的,和输入规模没有直接的关系时,空间复杂度记作 O(1)。如:

public static void case1(int n) {// 给变量 i 分配的存储空间大小是固定的,跟输入规模 n 没有直接的关系。int i = 0;for (; i < n; i++) {// 遍历元素。}
}

场景2:线性空间

当算法分配的存储空间是一个线性的集合(如数组或链表),并且集合大小和输入规模 n 成正比时,空间复杂度记作 O(n)。如:

public static void case2(int n) {int[] arr = new int[n];
}

场景3:二维空间

当算法分配的存储空间是一个二维数组集合,并且二维数组的行和列都与输入规模 n 成正比时,空间复杂度记作 O(n^2)。如:

public static void case3(int n) {int[][] arr = new int[n][n];
}

场景4:递归空间

计算机在执行程序时,会专门分配一块内存,用来存储方法调用栈(栈是一种先进后出的数据结构,或者说后进先出的数据结构)。

方法调用栈包括进栈和出栈两种操作。

  • 进栈:当进入一个方法时,执行进栈操作,把调用的方法和参数信息(栈帧)压入栈中。
  • 出栈:当一个方法返回时,执行出栈操作,把调用的方法和参数信息(栈帧)从栈中弹出。
/*** 斐波那契数列指的是这样一个数列:0,1,1,2,3,5,8,13,21,34,55,89...*/
public static int fibonacci(int n) {if (n <= 1) {return n;}return fibonacci(n - 2) + fibonacci(n - 1);
}

当 n = 1 时,会有 fibonacci 方法和 n = 1 入栈:在方法内部,满足 n <= 1,所以直接返回,有 1 个栈帧;
当 n = 2 时,会有 fibonacci 方法和 n = 2 入栈:在方法内部,fibonacci 方法和 n = 0 入栈,fibonacci 方法和 n = 1 入栈,总共会有 3 个栈帧;
当 n = 3 时,会有 fibonacci 方法和 n = 3 入栈:在方法内部,fibonacci 方法和 n = 1 入栈,fibonacci 方法和 n = 2 入栈,总共会有 5 个栈帧;
当 n = 4 时,会有 fibonacci 方法和 n = 4 入栈:在方法内部,fibonacci 方法和 n = 2 入栈,fibonacci 方法和 n = 3 入栈,总共会有 9 个栈帧;
当 n = 5 时,会有 fibonacci 方法和 n = 5 入栈:在方法内部,fibonacci 方法和 n = 3 入栈,fibonacci 方法和 n = 4 入栈,总共会有 15 个栈帧。
当 n = 6 时,会有 fibonacci 方法和 n = 6 入栈:在方法内部,fibonacci 方法和 n = 4 入栈,fibonacci 方法和 n = 5 入栈,总共会有 25 个栈帧。

S(n) ≈ (n - 1) ^ 2 = O(n^2);

再举一个递归的例子:求和

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

当 n = 1 时,有 sum 方法和 n = 1 入栈,方法内部:满足 n <= 1,直接返回。总共有 1 个栈帧;
当 n = 2 时,有 sum 方法和 n = 2 入栈,方法内部:有 sum 方法和 n = 1 入栈。总共有 2 个栈帧;
当 n = 3 时,有 sum 方法和 n = 3 入栈,方法内部:有 sum 方法和 n = 2 入栈。总共有 3 个栈帧;

所以,S(n) = n = O(n);

2.3.5 如何在时间复杂度和空间复杂度之间取舍?

在绝大多数时候,时间复杂度更重要一些,也就是说,哪怕要多分配一些内存空间,也要提升程序的执行速度。

比如,查找一个数组中的重复数字的例子。

采用牺牲时间换空间的实现如下:

/*** i = 0 时,外部循环:1 次 int i = 0; 操作,1 次 i < arr.length; 操作,内部循环:1 次 int j = 0; 操作,1 次 j < i; 操作,共 4 次操作* i = 1 时,外部循环:1 次 i++ 操作,1 次 i < arr.length; 操作,内部循环:1 次 int j = 0; 操作,2 次 j < i; 操作,1 次比较操作,1 次 j++ 操作,共 7 次操作* i = 2 时,外部循环:1 次 i++ 操作,1 次 i < arr.length; 操作,内部循环:1 次 int j = 0; 操作,3 次 j < i; 操作,2 次比较操作,2 次 j++ 操作,共 10 次操作* i = n 时,外部循环:1 次 i++ 操作,1 次 i < arr.length; 操作,内部循环:1 次 int j = 0; 操作,n + 1 次 j < i; 操作,n 次比较操作,n 次 j++ 操作,共 3n + 4 次操作* 总共:T(n) = (3n + 4)*n / 2 = 1.5n^2 + 2n = O(n^2)*/
public static void findTheSameTwoNumber1() {int[] arr = new int[]{3, 1, 2, 5, 4, 9, 7, 2};for (int i = 0; i < arr.length; i++) {for (int j = 0; j < i; j++) {if (arr[j] == arr[i]) {System.out.println("the same number is: " + arr[j]);break;}}}
}
/**

时间复杂度是 O(n^2),空间复杂度是 O(1)。

采用牺牲空间换时间的实现如下:

/*** i = 0 时,1 次 int i = 0; 操作,1 次 i < arr.length; 操作,1 次 hashSet.add(arr[i]) 操作,共 3 次操作* i = 1 时,1 次 i++ 操作,1 次 i < arr.length; 操作,1 次 hashSet.add(arr[i]) 操作,共 3 次操作* i = n 时,1 次 i++ 操作,1 次 i < arr.length; 操作,1 次 hashSet.add(arr[i]) 操作,共 3 次操作* 所以 T(n) = 3n = O(n);*/
public static void findTheSameTwoNumber2() {int[] arr = new int[]{3, 1, 2, 5, 4, 9, 7, 2};// 借助中间数据,一个哈希集合,需要开辟一定的内存空间来存储有用的数据信息。HashSet<Integer> hashSet = new HashSet<>();for (int i = 0; i < arr.length; i++) {if (!hashSet.add(arr[i])) {System.out.println("the same number is: " + arr[i]);break;}}
}

时间复杂度是 O(n),空间复杂度是 O(1)。

3. 最后

本文主要学习时间复杂度和空间复杂度的概念以及例子。

参考

  • 关于时间复杂度,你不知道的都在这里!;
  • 什么是大O表示法;
  • 三种表示方法:O, Ω, Θ

《漫画算法-小灰的算法之旅》第1章-算法概述读书笔记相关推荐

  1. 我眼中的算法导论 | 第一章——算法在计算中的作用、第二章——算法基础

    一个小白的算法学习之路.读<算法导论>第一天.本文仅作为学习的心得记录. 算法(Algorithm) 对于一个程序员来说,无论资历深浅,对算法一词的含义一定会或多或少有自己的体会,在< ...

  2. 《游戏之旅-我的编程感悟》读书笔记

    前段时间看了云风的<游戏之旅-我的编程感悟>,印象深刻,旦一直没做总结.作者云风以自身经历向我们传达了他对计算机.游戏和编程的感悟. 从接触计算机,到算法.编程语言.windows编程到汇 ...

  3. 漫画算法-小灰的算法之旅-排序算法(四)

    本文内容基于<漫画算法 小灰的算法之旅>,魏梦舒著. 1. 分类 1.1 时间复杂度为O(n^2)的排序算法 1.2 时间复杂度为O(nlogn)的排序算法 1.3 时间复杂度为线性的排序 ...

  4. 漫画算法小灰学习算法笔记

    写在前面的话: 学习算法,需要做的是领悟算法思想.理解算法对内存空间和性能的 影响,以及开动脑筋去寻求解决问题的最佳方案. 正文如下: 第1章 算法概述 1.1.2 什么是算法 算出1+2+3+4+5 ...

  5. 【读书笔记】《漫画算法》:克服对算法的恐惧,从漫画开始

    写在开头 在上小学和初高中的时候,要我写读后感这种东西,我是非常厌恶的.无非就是老师布置的一个作业,还是那种无趣且磨人的工作. 结果十多年过去了,到了工作的年纪,看书反倒是自觉地写起读后感来了,而且居 ...

  6. 《漫画算法》读书笔记

    <漫画算法>读书笔记 在图书馆借阅算法书时,看到了一本非常吸引我的算法书--<漫画算法>.算法还能以漫画的方式展示出来吗?我带着我的疑惑翻开了这本书,里面的语言非常接地气,通俗 ...

  7. mahout探索之旅---频繁模式挖掘算法与理解

    频繁模式挖掘 (先声明一下,文章内容可能你在网上也能找到,但是我参考了几篇文章的优势,使得算法更容易理解) Apriori算法 Apriori算法是一个经典的数据挖掘算法,Apriori的单词的意思是 ...

  8. 《算法图解》读书笔记—像小说一样有趣的算法入门书

    前言 学习算法课程的时候,老师推荐了两本算法和数据结构入门书,一本是<算法图解>.一本是<大话数据结构>,<算法图解>这本书最近读完了,读完的最大感受就是对算法不再 ...

  9. <<算法很美>>——(三)十大排序算法(上)

    目录 前言 冒泡排序 图解冒泡 代码实现 冒泡优化 选择排序 图解选排​ 代码实现 插入排序 图解插入 ​代码实现 希尔排序 图解希尔 ​代码实现: 归并排序 图解归并 ​代码实现 快速排序 图解快排 ...

最新文章

  1. 经典的导航二级式导航菜单增强版
  2. oracle中的数据集合操作
  3. jquery.form 和MVC4做无刷新上传DEMO
  4. 关于ios 里面碰到内存错误的两种设置
  5. 松弛法(relaxation)
  6. xp共享文件win7访问时不能保存密码
  7. 国家社科基金项目清单、申报书填写指南和课题申报书模板
  8. 双向链表、双向循环链表
  9. R语言 数据抽样(数据失衡处理、sample随机抽样、数据等比抽样、交叉验证抽样)
  10. ueditor+秀米
  11. netbeans java中文_netbeans中文乱码解决方案
  12. P1359 租用游艇(dijkstra不优化)
  13. python学习-day18、文件处理、
  14. Python_文本分析入门_SnowNLP(1)
  15. 计算机中SRAM的作用,SRAM特点及工作原理
  16. PCIE TLP 写中断
  17. 智能化“决战”开启新周期:大众“向上”、蔚来“向下”
  18. 【DP】西北大学集训队选拔赛(重现赛) B 饱和式救援
  19. Apache Spark 3.x集群部署
  20. java过滤器python是啥_过滤器如何在python中使用softlayer API

热门文章

  1. android解压zip文件进度条,Android实现文件解压带进度条功能
  2. 这款文言文编程语言是什么神物?
  3. cmakelist相关
  4. 第三章 调试措施编制中的方法及遇到的问题
  5. sorce insight 4.0 编辑程序不更新下载如何设置
  6. 打造平安校园,师慧“大学校园一张图综合服务系统”为高校搭建安全管理体系
  7. 开源项目推荐:OpenGL之开源库OpenSceneGraph
  8. 什么是LDAP/AD,以及同SSO的区别
  9. GCN-图卷积神经网络算法简单实现(含python代码)
  10. [ERROR] Maven execution terminated abnormally (exit code 1)