既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
依此类推,可以得到公式:
(n - 1) + (n - 2) + (n - 3) + ... + 0
= (0 + n - 1) * n / 2
= (n ^ 2 - n) / 2
因此,该算法的时间复杂度为O(n^2)。
当然,并非所有双重循环的时间复杂度都是O(n^2),如果内层循环次数为常数c,则时间复杂度为c*O(n)=O(n)。示例代码如下:
void printInformation(int n) {
for (int i = 1; i <= n ; i++) {
for (int j = 1; j <= 30; j++) {
cout << "Hello World!" << endl;
}
}
}
2.4 O(logn)
对于O(logn)对数阶时间复杂度,需要执行(logn)次操作(如变量交换、加法等算法运算等)才能完成算法的完整操作。
下面给出一个二分查找(binary search)的示例代码:
int binarySearch(int arr[], int n, int target) {
int left = 0, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
if (arr[mid] > target) {
right = mid - 1;
}
else {
left = mid + 1;
}
}
return -1;
}
在上面的示例代码中,通过while循环中的除2操作,将每次搜索范围缩小至前一次的一半,因此最坏的情况需要经过logn次操作跳出循环,因此该算法的时间复杂度为O(logn)。上述代码的运行过程示意图如下:
这里推导一下该算法的执行过程:
-
在第1次查找完成后,剩余的搜索范围为n*(1/2);
-
在第2次查找完成后,剩余的搜索范围为n*(1/2)*(1/2);
依此类推,可以得到公式:
n*((1/2)^k)=1
k=logn
说明:
-
n为数组长度;
-
k为最坏情况下查找到目标元素所需的次数(或者跳出循环的次数);
-
等式“n*((1/2)^k)=1”中右侧的“1”表示剩余的待搜索范围。当剩余的搜索范围为“1”时,该次搜索操作完成后,无论是否找到了目标元素,此算法都将结束执行。
因此,该算法的时间复杂度为O(logn)。
2.5 O(nlogn)
将时间复杂度为O(logn)的代码循环n次,则该算法的时间复杂度为n*O(logn),即O(nlogn)。
示例代码如下:
void hello() {
for (i = 1; i < n; i++) {
int j = 1;
while (j < n) {
j = j * 2;
}
}
}
3 其他几种时间复杂度
本章介绍以下几种复杂度:
-
递归算法(recursive algorithm)的时间复杂度;
-
最好情况(best case)时间复杂度;
-
最坏情况(worst case)时间复杂度;
-
平均情况(average case)时间复杂度;
-
均摊(amortized)时间复杂度。
3.1 递归算法时间复杂度
3.1.1 一次递归调用
如果递归函数中,只进行了一次递归调用,且递归深度为depth,同时递归函数中操作的时间复杂度为T,则该递归算法的总体时间复杂度为O(T * depth)。
下面给出一个使用递归函数的二分查找(binary search)的示例代码:
int binarySearch(int arr[], int left, int right, int target) {
if (left > right) {
return -1;
}
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
}
else if (arr[mid] > target) {
return binarySearch(arr, left, mid - 1, target);
} else {
return binarySearch(arr, mid + 1, right, target);
}
}
在上述代码中,递归函数中每次只可进行一次递归调用(if-else判断条件),由于递归函数中的除2操作,使得递归深度为logn,同时递归函数中操作的时间复杂度为1(无循环等),因此该算法的时间复杂度为O(1 * logn) = O(logn)。
这里再给出一个使用递归函数计算累加和的代码示例:
int sum(int n) {
if (n == 0) {
return 0;
}
return n + sum(n - 1);
}
在上述代码中,递归深度随着n的增加而线性递增,即递归深度为n,同时递归函数中操作的时间复杂度为1(无循环等),因此该算法的时间复杂度为O(1 * n) = O(n)。该代码的运行过程示例图如下:
3.1.2 多次递归调用
递归算法中比较难计算的是多次递归调用的情况,在这种情况下,通常需要根据具体情况分析算法时间复杂度。
先看下面一段示例代码:
int f(int n) {
if (n == 0) {
return 1;
}
return f(n - 1) + f(n - 1);
}
在上述这段代码中,递归算法包含了2次递归调用,递归树中节点数就是代码操作的调用次数。假设n=3,则上述代码的运行过程示意图如下:
在上述示意图中,代码操作的调用次数计算公式如下:
1 + 2 + 4 + 8 = 15
从中可以寻找出如下规律:
2 ^ 0 + 2 ^ 1 + 2 ^ 2 + ... + 2 ^ n
= 2 ^ (n + 1) – 1
= 2 ^ n + 1
因此,该算法的时间复杂度为O(2^n)。
再看一个归并排序的代码示例:
void mergeSort(int sourceArr[], int tempArr[], int startIndex, int endIndex)
{
if(startIndex < endIndex)
{
int midIndex = startIndex + (endIndex-startIndex) / 2;
mergeSort(sourceArr, tempArr, startIndex, midIndex);
mergeSort(sourceArr, tempArr, midIndex+1, endIndex);
merge(sourceArr, tempArr, startIndex, midIndex, endIndex);
}
}
在上述这段代码中,递归算法包含了2次递归调用。假设n=8,则上述代码的运行过程示意图如下:
通过观察上面的归并排序的递归树可知:
-
归并排序的递归树深度为logn;
-
归并排序的递归树中每个节点处理的数据规模是逐渐缩小的,但每一层的时间复杂度保持不变,为O(n);
因此,归并排序的算法时间复杂度为O(nlogn)。
说明:归并排序算法时间复杂度的计算方式与3.1.1节“一次递归调用”中的计算方式类似。这里要强调的是,涉及到对此递归调用的时间复杂度计算时,需根据实际情况进行分析。
3.2 最好/最坏情况时间复杂度
最好或最坏情况下的时间复杂度指的是特殊情况下的时间复杂度。
现有如下示例代码(作用是查找某数值在数组中首次出现时的位置):
int find(int array[], int n, int x) {
for (int i = 0; i < n; i++) {
if (array[i] == x) {
return i;
break;
}
}
return -1;
}
上述代码的运行过程示意图如下:
通过上图可知,当数组中第一个元素就是要查找的数值时,时间复杂度就是O(1);而当最后一个元素才是要找的数值时,时间复杂度则是O(n)。
简单总结一下,最好情况时间复杂度就是在最理想情况下执行代码的时间复杂度,它所消耗的时间是最短的;最坏情况时间复杂度就是在最糟糕情况下执行代码的时间复杂度,它所消耗的时间是最长的。
3.3 平均情况时间复杂度
最好、最坏时间复杂度反应的是极端条件下的时间复杂度,通常发生的概率不大,不能代表平均水平。因此为了更好的表示一般情况下的算法时间复杂度,就需要引入平均情况时间复杂度。
平均情况时间复杂度可用代码在所有可能情况下执行次数的加权平均值表示。
这里仍以3.2节中的find函数为例,从概率的角度看, 数值x在数组中每一个位置出现的可能性是相同的,都是1/n,同时对应位置的权重为n的实际值(如1、2、3…),因此,该算法的平均情况时间复杂度就可以用如下的方式计算:
(1/n)*1 + (1/n)*2 + (1/n)*3 + ... + (1/n)*n
= (n+1)/2
因此,find函数的平均时间复杂度为O(n)。
3.4 均摊时间复杂度
这里通过一个动态数组的push_back操作来理解均摊时间复杂度。示例代码如下:
template <typename T>
class MyVector {
private:
T* data;
// 存储数组中的元素个数
int size;
// 存储数组中可以容纳的最大的元素个数
int capacity;
// 复杂度为O(n)
void resize(int newCapacity) {
T *newData = new T[newCapacity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
capacity = newCapacity;
}
public:
MyVector() {
data = new T[100];
size = 0;
capacity = 100;
}
// 平均复杂度为 O(1)
void push_back(T e) {
if (size == capacity) {
resize(2 * capacity);
}
![img](https://img-blog.csdnimg.cn/img_convert/aec76162d04f7246e4f55b1222db8e33.png)
![img](https://img-blog.csdnimg.cn/img_convert/134303a8f11e4a53cb3b0d277c3ba4cd.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**
void push_back(T e) {
if (size == capacity) {
resize(2 * capacity);
}
[外链图片转存中...(img-AP1DuMrS-1715676568264)]
[外链图片转存中...(img-PfMZ0AYY-1715676568264)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**