一、时间复杂度
1、推算方法
- 统计完整的操作数量 T ( n ) = 2 n 2 + 7 n + 3 T(n) = 2n^2+7n+3 T(n)=2n2+7n+3
- 忽略常数项和非常数项系数 T ( n ) = n 2 + n T(n) = n^2+n T(n)=n2+n
- 保留最高阶项 T ( n ) = n 2 T(n) = n^2 T(n)=n2
- 时间复杂度为 O ( n 2 ) O(n^2) O(n2)
void algorithm(int n)
{
int a = 1; // +1
a = a + n; // +1
for (int i = 0; i < 5 * n + 1; i++)
{
cout << 0 << endl; // +n
}
for (int i = 0; i < 2 * n; i++)
{
for (int j = 0; j < n + 1; j++)
{
cout << 0 << endl; // +n*n
}
}
}
2、复杂度比较
3、常见类型
- 常数阶
O
(
1
)
O(1)
O(1)
操作数量与输入数据大小n无关,不随n的变化而变化
// 1+1+100000
int constant(int n)
{
int count = 0;
int size = 100000;
for (int i = 0; i < size; i++)
count++;
return count;
}
- 线性阶
O
(
n
)
O(n)
O(n)
操作数量相对于输入数据大小n以线性级别增长
// 1+n
int linear(int n)
{
int count = 0;
for (int i = 0; i < n; i++)
count++;
return count;
}
- 平方阶
O
(
n
2
)
O(n^2)
O(n2)
操作数量相对于输入数据大小n以平方级别增长
// 1+n*n
int quadratic(int n)
{
int count = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
count++;
}
}
return count;
}
- 指数阶
O
(
2
n
)
O(2^n)
O(2n)
在每次循环中一分为二
// 指数阶的循环实现
int exponential(int n)
{
int count = 0, base = 1;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < base; j++)
{
count++;
}
base *= 2; // 一分为二
}
return count;
}
// 指数阶的递归实现
int expRecur(int n)
{
if (n == 1)
return 1;
return expRecur(n - 1) + expRecur(n - 1) + 1; // 一分为二
}
- 对数阶
O
(
l
o
g
(
n
)
)
O(log(n))
O(log(n))
在每次循环中缩减一半
// 对数阶的循环实现
int logarithmic(int n)
{
int count = 0;
while (n > 1)
{
n = n / 2; // 缩减一半
count++;
}
return count;
}
- 线性对数阶
O
(
n
l
o
g
(
n
)
)
O(nlog(n))
O(nlog(n))
嵌套循环,两层循环的时间复杂度分别为 O ( l o g ( n ) ) O(log(n)) O(log(n))和 O ( n ) O(n) O(n)
int linearLogRecur(int n)
{
if (n <= 1)
return 1;
int count = linearLogRecur(n / 2) + linearLogRecur(n / 2); // O(log(n))
for (int i = 0; i < n; i++)
{
count++; // O(n)
}
return count;
}
- 阶乘阶
O
(
n
!
)
O(n!)
O(n!)
给定n个互不重复的元素,求其所有可能的排列方案
int factorialRecur(int n)
{
if (n == 0)
return 1;
int count = 0;
// 从 1 个分出 n 个递归
for (int i = 0; i < n; i++)
{
count += factorialRecur(n - 1);
}
return count;
}
二、空间复杂度
1、运行空间分类
- 输入空间:用于存储算法的输入数据。
- 暂存空间:用于存储算法在运行过程中的变量、对象、函数上下文等数据。
a.暂存数据:用于保存算法运行过程中的各种常量、变量、对象等。
b.栈帧空间:用于保存调用函数的上下文数据。
c.指令空间:用于保存编译后的程序指令,在实际统计中忽略不计。 - 输出空间:用于存储算法的输出数据。
2、推算方法
- 通常只关注最差空间复杂度
- 以最差输入数据为准
- 以算法运行中的峰值内存为准
3、常见类型
- 常数阶
O
(
1
)
O(1)
O(1)
常见于元素数量与输入数据大小n无关的常量、变量、对象
void constant(int n)
{
// 常量、变量、对象占用 O(1) 空间
const int a = 0;
int b = 0;
vector<int> nums(10000);
ListNode node(0);
// 循环中的变量占用 O(1) 空间
for (int i = 0; i < n; i++)
{
int c = 0;
}
// 循环中的函数占用 O(1) 空间
for (int i = 0; i < n; i++)
{
func();
}
}
- 线性阶
O
(
n
)
O(n)
O(n)
常见于元素数量与n成正比的数组、链表、栈、队列等
void linear(int n)
{
// 长度为 n 的数组占用 O(n) 空间
vector<int> nums(n);
// 长度为 n 的列表占用 O(n) 空间
vector<ListNode> nodes;
for (int i = 0; i < n; i++)
{
nodes.push_back(ListNode(i));
}
// 长度为 n 的哈希表占用 O(n) 空间
unordered_map<int, string> map;
for (int i = 0; i < n; i++)
{
map[i] = to_string(i);
}
}
- 平方阶
O
(
n
2
)
O(n^2)
O(n2)
常见于矩阵和图,元素数量与n成平方关系
/* 平方阶 */
void quadratic(int n)
{
// 二维列表占用 O(n^2) 空间
vector<vector<int>> numMatrix;
for (int i = 0; i < n; i++)
{
vector<int> tmp;
for (int j = 0; j < n; j++)
{
tmp.push_back(0);
}
numMatrix.push_back(tmp);
}
}
// 平方阶的递归实现
int quadraticRecur(int n)
{
if (n <= 0)
return 0;
vector<int> nums(n);
return quadraticRecur(n - 1);
}
- 指数阶
O
(
2
n
)
O(2^n)
O(2n)
常见于二叉树
// 指数阶,建立满二叉树
TreeNode *buildTree(int n)
{
if (n == 0)
return nullptr;
TreeNode *root = new TreeNode(0);
root->left = buildTree(n - 1);
root->right = buildTree(n - 1);
return root;
}
- 对数阶
O
(
l
o
g
(
n
)
)
O(log(n))
O(log(n))
常见于分治算法
例如归并排序,输入长度为n的数组,每轮递归将数组从中点处划分为两半,形成高度为 l o g ( n ) log(n) log(n) 的递归树,使用 O ( l o g ( n ) ) O(log(n)) O(log(n))栈帧空间