《数据结构与算法之美》是极客时间上的一个算法学习系列,在学习之后特在此做记录和总结。
掌握数据结构和算法,不管对于阅读框架源码,还是理解其背后的设计思想,都是非常有用的。一旦掌握数据结构和算法,之前可能需要费很大劲儿来优化的代码,需要花很多心思来设计的架构,用了数据结构和算法之后,很容易就可以解决了。
数据结构和算法是相辅相成的,数据结构是为算法服务的,算法要作用在特定的数据结构之上。
(1)从广义上讲,数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。
(2)从狭义上讲,是指某些著名的数据结构和算法,比如队列、栈、堆、二分查找、动态规划等。
该系列总结了 20 个最常用的、最基础数据结构与算法:
(1)10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;
(2)10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。
一、时间复杂度分析
大 O 复杂度表示法,实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
分析一段代码的时间复杂度有三个比较实用的方法:
(1)只关注循环执行次数最多的一段代码。例如代码被执行了 n 次,所以总的时间复杂度就是 O(n)。
(2)加法法则:总复杂度等于量级最大的那段代码的复杂度。例如两段代码的复杂度分别为O(n) 和 O(n^2),那么整段代码的时间复杂度就为 O(n^2)。
(3)乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积。假设 T1(n) = O(n),T2(n) = O(n^2),则 T1(n) * T2(n) = O(n^3)
二、多项式量级
这些复杂度量级几乎涵盖了你今后可以接触的所有代码的复杂度量级。
非多项式量级只有两个:O(2^n) 和 O(n!)。接下来主要来看几种常见的多项式时间复杂度。
1)O(1)
只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。
只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。
2)O(logn)、O(nlogn)
假设代码的执行次数是2^x=n,那么x=x=log2^n,因此这段代码的时间复杂度就是 O(log2^n)。
假设有一段代码的时间复杂度是O(log3^n),而log3^n 就等于 log3^2 * log2^n,所以 O(log3^n) = O(C * log2^n),其中 C=log3^2 是一个常量。
在采用大 O 标记复杂度的时候,可以忽略系数。所以,O(log2^n) 就等于 O(log3^n)。
因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,时间复杂度就是 O(nlogn)。归并排序、快速排序的时间复杂度都是 O(nlogn)。
3)O(m+n)、O(m*n)
m 和 n 是表示两个数据规模。
因为无法事先评估 m 和 n 谁的量级大,所以在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。
三、空间复杂度分析
时间复杂度的全称是渐进时间复杂度,表示算法的执行时间与数据规模之间的增长关系。
类比一下,空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
常见的空间复杂度就是 O(1)、O(n)、O(n2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。
四、最好、最坏、平均、均摊时间复杂度
1)最好和最坏
最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度。例如在最理想的情况下,要查找的变量 x 正好是数组的第一个元素。
最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度。如果数组中没有要查找的变量 x,我们需要把整个数组都遍历一遍才行。
2)平均情况时间复杂度
要查找的变量 x,要么在数组里,要么就不在数组里。为了方便理解,假设在数组中与不在数组中的概率都为 1/2。
另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。
所以,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率就是 1/(2n)。
把每种情况发生的概率也考虑进去,那平均时间复杂度的计算过程就变成了这样:
这个值就是概率论中的加权平均值,也叫作期望值,所以平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。
时间复杂度的大 O 标记法中,可以省略掉系数、低阶、常量,这段代码的加权平均时间复杂度仍然是 O(n)。