目录
一、前言
本系列主要参考和学习leetcode的《图解算法数据结构》一书,如有需要,可以跳转此链接https://leetcode-cn.com/leetbook/detail/illustration-of-algorithm/
如若涉及侵权,请告知作者删改。
本文主要探讨算法的复杂度,分时间和空间两个角度
二、时间复杂度
1. 概念
指输入数据大小为N时,算法运行的时间。但是却不是算法的绝对时间,而是统计算法计算操作数量。计算操作数量与运行绝对时间呈正相关。运行时间受编程语言、编译环境、使用的CPU和GPU的影响。
计算操作数量具体的是指操作数随数据大小N变化的变化数量。即与N的关系,如1次操作,10次操作都是O(1),常数性操作。而N次和100N次都是O(N),线性复杂度。
2. 符号表示
有最差、平均、最优三种情况,对应有O、Θ 和 Ω 三种符号。一般有O来表示
3. 常见种类
1) 复杂度大小关系
O(1)< O(log N) < O(N) < O(N^2) < O (2^N) < O(N!)
2)常数 O(1)
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
int count = 0;
int a = 10000;
// 计算操作次数与N无关,为常数性,无论多大都为O(1)
for(int i = 0; i < a; i ++)
{
count++;
}
return count;
}
3) 线性O(N)
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
int count = 0;
int a = 10000;
// 与N有关
for(int i = 0; i < N; ++ i)
{
// 第二层循环与N,无关
for(int j = 0; j < a; ++ j)
{
count++;
}
}
// 综上,只有一重循环与N有关,故O(N*1),即O(N)
return count;
}
4)平方O(N^2)
只需将上述代码第二重循环略加修改即可
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
int count = 0;
int a = 10000;
// 与N有关
for(int i = 0; i < N; ++ i)
{
// 第二层循环与N也有关
for(int j = 0; j < N; ++ j)
{
count++;
}
}
// 综上,两重循环独立,都与N相关,故为O(N*N),即O(N^2)
return count;
}
此处浅谈以下,常见的一种排序,冒泡排序的时间复杂度即为O(N^2)
代码如下:
vector<int> bubbleSort(vector<int>& nums)
{
// 获取数组的大小
int N = nums.size();
for(int i = 0; i < N - 1; i ++)
{
for(int j = 0; j < N - 1; ++ j)
{
if(nums[j] > nums[j + 1])
{
// 交换两个元素
swap(nums[j], nums[j + 1])
}
}
}
}
5)指数O(2^N)
常用于递归
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
if(N <= 0)
{
return 1;
}
// 当N大于0的时候,每次递归都会出现2次操作
int count_1 = alogorithm(N - 1);
int count_2 = alogorithm(N - 1);
return count_1 + count_2;
}
6)阶乘O(N!)
对应高中数学提到的全排列,常用递归实现,其原理:第一层分裂N个,第二层N-1个,……,直至第N层终止并回溯
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
if(N <= 0)
{
return 1;
}
int count = 0;
// 第一层分裂N次(N次循环),每次循环都是调用alogorithm(N-1)
for(int i = 0; i < N; ++ i)
{
count += alogorithm(N - 1);
}
return count;
}
7)对数O(logN)
与指数阶相反,指数时每次分裂出两倍,而对数则是每次排除一半,常用于二分、分支算法
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
int alogorithm(int N)
{
int count = 0;
float i = N;
// 每次循环,范围都变为原来的一半
while(i > 1)
{
i = i / 2;
count ++;
}
return count;
}
8)其他时间复杂度
通过上面的例子,相信你已经了解了一个道理:时间复杂度的加和与复合。类似于高等数学中的无穷小关系,在加和中结结果取较大的如:O(N) + O(1) = O(N)
而复合,则将复杂度做乘积,如:线性对数 O(NlogN) = O(N)* O(logN)
三、空间复杂度
1. 概念与定义
1)涉及到的空间类型
·输入空间:存储输入数据所需的空间大小
·暂存空间:算法运行过程中,存储所有中间变量和对象所需的空间大小
·输出空间:算法运行返回时,存储输出数据所需的空间大小
2)定义
通常情况下,指输入数据大小为N时,算法运行所使用的【暂存空间】+【输出空间】的总体大小
3)根据来源,可以分为三类
·指令空间:编译后,程序指令所使用的内存空间
·数据空间:算法中的各项变量使用的空间,包括:声明的常量、变量、动态数组、动态对象等使用的内存空间。
·栈帧空间:程序调用函数基于栈实现,调用期间,占用常量大小的栈帧空间,直至返回后释放。栈帧空间的累计常用于递归调用函数。
2. 符号表示
和时间复杂度一样采用O表示,为算法最坏情况下所用的内存。最坏情况有如下两层含义:(输入整数N)
1)最差输入数据
当N <= 10时,数组nums的长度恒为10,空间复杂度为O(1),当N > 10时,空间复杂度为 O(N);因此,空间复杂度为最差输入数据情况下的O(N)。
2)最差运行节点
在执行 nums = [0] * 10 时,算法仅使用O(1)的大小空间,而当执行 nums = [0] * N 时,算法使用O(N)的空间;因此,空间复杂度应为最差运行节点的O(N)。
3. 常见种类
1)复杂度大小关系
O(1) < O(logN) < O(N) < O(N^2) < O(2^N)
2) 常数 O(1)
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
// 定义节点 Node
struct Node
{
int val;
Node* next;
Node(int x) : val(x), next(NULL){}
};
// 函数test()
int test()
{
return 0;
}
int alogorithm(int N)
{
// 声明的变量都与N无关,皆使用常数大小的空间,O(1)
int num = 0;
int nums[1000];
Node* node = new Node(0);
unordered_map<int, string> dic;
dic.emplace(0, "0");
}
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
// 定义节点 Node
struct Node
{
int val;
Node* next;
Node(int x) : val(x), next(NULL){}
};
// 函数test()
int test()
{
return 0;
}
int alogorithm(int N)
{
for(int i = 0; i < N; ++ i)
{
// 虽然每次循环都会调用N,但是每轮调用后test()已返回,无栈帧空间的累计
test();
}
}
3)线性O(N)
常用于一维数组、链表、哈希表等
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
// 定义节点 Node
struct Node
{
int val;
Node* next;
Node(int x) : val(x), next(NULL){}
};
// 函数test()
int test()
{
return 0;
}
int alogorithm(int N)
{
// 一维数组
int nums_1[N];
int nums_2[N / 2 + 1];
// 链表
vector<Node*> nodes;
for(int i = 0; i < N; ++ i)
{
nodes.push_back(new Node(i));
}
// 哈希表
unordered_map<int, string> dic;
for(int i = 0; i < N; ++ i)
{
dic.emplace(i, to_string(i));
}
}
4)平方O(N^2)
常见于矩阵以及其他类型的与N呈二次方关系的集合
#include<iostream>
#include<iomanip>
#include<cstdlib>
#include<cmath>
using namespace std;
// 定义节点 Node
struct Node
{
int val;
Node* next;
Node(int x) : val(x), next(NULL){}
};
// 函数test()
int test()
{
return 0;
}
int alogorithm(int N)
{
vector<vector<int>> num_martix;
for(int i = 0; i < N; i ++)
{
vector<int> nums;
for(int j = 0; j < N; ++ j)
{
nums.push_back(0);
}
}
}
5)指数O(2^N)
常见于二叉树等
6)对数O(logN)
常见于分治算法的栈帧空间累计,数据类型转换等。如快速排序,和数字转字符串。
四、总结
对于算法的优劣和性能,需从其的时间复杂度和空间复杂度两个角度分析。而在实际解决算法问题的时候,同时优化两个复杂度是困难的,往往有所牺牲,这体现了算法中的时空权衡。
在很多竞赛题(蓝桥、acm、天梯),很多需要牺牲空间来换时间。