简单来说,数据结构就是数据的存储方式,比如数组就是把数据存在一段连续的内存上,而链表则是通过指针的关联将数据存在任意可用的内存上;栈是先进后出,队列是先进先出。
而算法则是对这些数据的操作方法,比如数据的插入、查找、删除、排序等。
二者相辅相成,互为一体,数据结构为算法服务,而算法要在指定数据结构上进行操作。
学习数据结构和算法的目的是为了在实际应用的时候更加优化地利用内存,提高程序运行效率,而复杂度分析则是给我们提供一个衡量代码质量好坏的标准。
如果我们在不运行程序的情况下就可以定性知道代码的内存占用和时间消耗,这将会给我们提供一个当前程序的总体评估和未来的改进方向。
直接运行程序就可以知道算法的执行时间和占用内存,但这个过程往往会受到运行环境和数据规模的影响,因此,我们需要一个不用进行具体测试就可以粗略估计算法执行效率的方法,这就是复杂度分析。
int cal(int n) { int sum = 0; int i = 1; for (; i <= n; ++i) { sum = sum + i; } return sum; }
以查找为例,看如下代码
// 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; }
最好情况时间复杂度就是在程序最理想的状态下,数组第一个元素就是我们要查找的元素,只需要查找一次;而最坏情况时间复杂度就是在程序最糟糕的状态下,数组最后一个元素才是我们要查找的元素,需要查找完整个数组;
事实上,我们要查找的元素可能存在数组中的任何一个位置,甚至可能不存在于数组中,因此,考虑所有情况出现的概率,求出各种情况下时间复杂度的平均值,也就是平均情况时间复杂度。
// array 表示一个长度为 n 的数组 // 代码中的 array.length 就等于 n int[] array = new int[n]; int count = 0; void insert(int val) { if (count == array.length) { int sum = 0; for (int i = 0; i < array.length; ++i) { sum = sum + array[i]; } array[0] = sum; count = 1; } array[count] = val; ++count; }
这段代码的功能是向数组中插入一个元素,当数组未满时,直接插入,时间复杂度为 \(O(1)\);当数组满时,先计算数组所有元素的和,再插入元素,时间复杂度为 \(O(n)\)。
并且,两种复杂度不同的操作具有一定的规律,一系列 \(O(1)\) 的插入导致数组占满,然后紧跟着一个 \(O(n)\) 的插入,再继续循环往复。
这时候,我么就可以把 \(O(n)\) 复杂度的这个操作平均分摊到前面的 \(O(1)\) 复杂度操作上去,整体的时间复杂度也就变成了 \(O(1)\),这就是均摊情况时间复杂度。
如果大部分情况时间复杂度都很低,只有少数情况时间复杂度较高,并且这些操作具有前后的时序关系,那么我们就可以应用均摊情况时间复杂度来进行分析。通常来说,均摊情况时间复杂度就等于最好情况时间复杂度。
严格高阶函数集合 \(\omega(f(n))= \{g(n)\space |\space \forall \space c >0, \exists \space n_0, \forall \space n>n_0, 0 \leqslant cf(n) < g(n) \}\) 称为比 \(f(n)\) 高阶的函数集合。
迭代法求解递归方程
\[\begin{aligned} T(n)&=2T(n/2)+cn \\ &=2^2T(n/2^2)+cn+cn\\ &=2^3T(n/2^3)+cn+cn+cn\\ &=\cdots\\ &=2^kT(n/2^k)+kcn\\ &=2^kT(1)+kcn \gets 2^k=n\\ &=nT(1)+cnlogn\\ &=\Theta(nlogn)\\ \end{aligned} \]
设 \(a\geqslant 1, b>1\) 是常数,\(f(n)\) 是函数,\(T(n)\) 是定义在非负整数集上的函数且 \(T(n)=aT(n/b)+f(n)\),那么 \(T(n)\) 可以如下求解:
- \(f(n)\) 是 \(n^{log_ba}\) 的低阶函数,且低出一个多项式,\(f(n)=O(n^{log_b a-\epsilon}), \epsilon>0\),那么 \(T(n)=\Theta(n^{log_b a})\)
- \(f(n)\) 是 \(n^{log_ba}\) 的同阶函数,\(f(n)=\Theta(n^{log_b a})\),那么 \(T(n)=\Theta(n^{log_b a}logn)\)
- \(f(n)\) 是 \(n^{log_ba}\) 的高阶函数,且高出一个多项式,\(f(n)=\Omega(n^{log_b a+\epsilon}), \epsilon>0\),而且存在常数 \(c<1\) 使得 \(af(n/b)<cf(n)\) 对充分大的 \(n\) 成立,那么 \(T(n)=\Theta(f(n))\)
参考资料-极客时间专栏《数据结构与算法之美》
获取更多精彩,请关注「seniusen」!