重点:10 个数据结构:数组、链表、栈、队列、散列表、二叉树、堆、跳表、图、Trie 树;
10 个算法:递归、排序、二分查找、搜索、哈希算法、贪心算法、分治算法、回溯算法、动态规划、字符串匹配算法。
基本概念
- 从广义上讲,数据结构就是指一组数据的存储结构。算法就是操作数据的一组方法。
- 从狭义上讲,指某些著名的数据结构和算法,比如队列、栈、堆、二分查找、动态规划等。
- 数据结构与算法:数据结构是为算法服务的,算法要作用在特定的数据结构之上。
时间复杂度
eg:
int cal(int n) {
int sum = 0;
int i = 1;
int j = 1;
for (; i <= n; ++i) {
j = 1;
for (; j <= n; ++j) {
sum = sum + i * j;
}
}
}
- 假设每个语句的执行时间是 unit_time
- 2、3、4 行代码,每行都需要 1 个 unit_time 的执行时间
- 5、6 行代码循环执行了 n 遍,需要 2n * unit_time 的执行时间
- 7、8 行代码循环执行了 n 2 n^2 n2 遍,所以需要 2 n 2 n^2 n2 * unit_time 的执行时间
- 整段代码总的执行时间 T(n) = (2 n 2 n^2 n2+2n+3)*unit_time
- 总结:所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比
公式
- T(n)表示代码执行的时间
- n 表示数据规模的大小
- f(n) 表示每行代码执行的次数总和
- O表示代码的执行时间 T(n) 与 f(n) 表达式成正比
例子中的 T(n) = O(2 n 2 n^2 n2+2n+3)。这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以,也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。
当 n 很大时,公式中的低阶、常量、系数三部分并不左右增长趋势,所以都可以忽略。我们只需要记录一个最大量级就可以了,所以例子的时间复杂度可以记为:T(n) = O( n 2 n^2 n2)。
计算原则
- 只关注循环执行次数最多的一段代码
- 加法法则:总复杂度等于量级最大的那段代码的复杂度(并不会相加,而是会取MAX)
- 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
常见复杂度量级
2
n
2^n
2n:
n!:
分为:多项式量级和非多项式量级。
其中,非多项式量级只有两个:O(
2
n
2^n
2n) 和 O(n!)。
非多项式量级的算法问题叫作 NP(Non-Deterministic Polynomial,非确定多项式)问题,n 越来越大时,非多项式量级算法的执行时间会急剧增加,求解问题的执行时间会无限增长。所以,非多项式时间复杂度的算法其实是非常低效的算法,所以在开发中避免使用,在此也不展开讲。
1. Ο(1)
只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1),1实际就是 n 0 n^0 n0。
2. O(logn)、O(nlogn)
i=1;
while (i <= n) {
i = i * 2;
}
则
2
x
2^x
2x=n 中的x就是执行次数。 所以,这段代码的时间复杂度就是 O(
l
o
g
2
n
log{_2}n
log2n)
而:
l
o
g
3
n
log{_3}n
log3n 就等于
l
o
g
3
2
log{_3}2
log32 *
l
o
g
2
n
log{_2}n
log2n,
l
o
g
3
2
log{_3}2
log32 是一个常量,可以忽略系数,所以,O(
l
o
g
2
n
log{_2}n
log2n) 就等于 O(
l
o
g
3
n
log{_3}n
log3n),
l
o
g
n
2
logn^2
logn2 =
2
l
o
g
n
2logn
2logn =
l
o
g
n
logn
logn。因此,在对数阶时间复杂度的表示方法里,我们忽略对数的“底”,统一表示为 O(logn)。
注意:
l
o
g
n
logn
logn *
l
o
g
n
logn
logn =
l
o
g
2
n
log{^2}n
log2n ;
log(a) (M·N)=log(a) M+log(a) N ;
log(a) (M÷N)=log(a) M-log(a) N ;
3. O(m+n)、O(m*n)
int cal(int m, int n) {
int sum_1 = 0;
int i = 1;
for (; i < m; ++i) {
sum_1 = sum_1 + i;
}
int sum_2 = 0;
int j = 1;
for (; j < n; ++j) {
sum_2 = sum_2 + j;
}
return sum_1 + sum_2;
}
m 和 n 是表示两个数据规模。因为无法事先评估 m 和 n 谁的量级大,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。针对这种情况,原来的加法法则就不正确了。
当然,代码是嵌套的话:时间复杂度就是O(m*n)
空间复杂度
空间复杂度全称就是渐进空间复杂度(asymptotic space complexity),表示算法的存储空间与数据规模之间的增长关系。
复杂度都是对算法而言的:要看算法额外的内存消耗,不是看算法应用的对象是多少存储空间
eg:数组[n]扩容到[2n],则扩容算法需要额外开辟出一个2n空间才能去拷贝。所以扩容算法空间复杂度为2n====>O(n)。
如:print算法的空间复杂度就是O(n)(实现该算法需要额外开出一个数组[n])
//实现 n -到-> 0 平方的输出
void print(int n) {
int i = 0;
int[] a = new int[n];
for (i; i <n; ++i) {
a[i] = i * i;
}
for (i = n-1; i >= 0; --i) {
print out a[i]
}
}
平均时间复杂度
最好、最坏、平均时间复杂度。
// n表示数组array的长度
int find(int[] array, int n, int x) {
int i = 0;
int pos = -1;
for (; i < n; ++i) {
if (array[i] == x) {
pos = i;
break;
}
}
return pos;
}
不考虑概率情况下:
- 有 n+1 种情况:在数组的 0~n-1 位置中和不在数组中
- 省略掉系数、低阶、常量,所以,咱们把刚刚这个公式简化之后,得到的平均时间复杂度就是 O(n)。
平均时间复杂度的全称应该叫加权平均时间复杂度或者期望时间复杂度。
所以,考虑概率的情况下:
- 有 n+1 种情况:在数组的 0~n-1 位置中和不在数组中
- 假设在数组中与不在数组中的概率都为 1/2。另外,要查找的数据出现在 0~n-1 这 n 个位置的概率也是一样的,为 1/n。所以,根据概率乘法法则,要查找的数据出现在 0~n-1 中任意位置的概率就是 1/(2n)。
- 代码的加权平均值为 (3n+1)/4。用大 O 表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂度仍然是 O(n)。
均摊时间复杂度
均摊时间复杂度就是一种特殊的平均时间复杂度,一般的平均复杂度是无规律的,而均摊时间复杂度都是有规律的。如,一个n次循环:前n-1个循环操作时间复杂度为O(1),然后第n个时间复杂度为O(n)。
摊还分析法:将较高时间复杂度那次操作的耗时,平摊到其他那些时间复杂度比较低的操作上。 如上举例:可以将第n次的最高时间复杂度O(n)平分到前n-1个时间复杂度较低的操作上。 O(n)/(n-1) + O(1) = O(1) + O(1) = O(1) [前面一个O(1)是因为分子分母都是n的同次幂,后面一个O(1)是前n-1次自己的O(1)]。所以时间复杂度是O(1)。
拓展:上述的最高时间复杂度就是最坏时间复杂度;
剩余的复杂度较低的操作就是最好时间复杂度;
进行均摊分析法后,均摊时间复杂度往往就是最好时间复杂度。
例题:
// 全局变量,大小为10的数组array,长度len,下标i。
int array[] = new int[10];
int len = 10;
int i = 0;
// 往数组中添加一个元素
void add(int element) {
if (i >= len) { // 数组空间不够了
// 重新申请一个2倍大小的数组空间
int new_array[] = new int[len*2];
// 把原来array数组中的数据依次copy到new_array
for (int j = 0; j < len; ++j) {
new_array[j] = array[j];
}
// new_array复制给array,array现在大小就是2倍len了
array = new_array;
len = 2 * len;
}
// 将element放到下标为i的位置,下标i加一
array[i] = element;
++i;
}
答案:最好是O(1),最坏是O(n), 均摊是O(1)