本文主要是介绍C++ 性能骨灰级优化(推荐),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
文章目录
- 一、前言
- 二、时间复杂度
- 三、C++ 写法优化
- 1、查找时用键值对代替数组
- 1)哈希数组
- 2)map
- 3)unordered_map
- 4)效率排行总结
- 2、关注常数优化
- 1)位运算
- 2)避免重复运算
- 3)加法代替乘法
- 4)再次优化取模
- 5)尽量少用 #define
- 6)循环终止条件
- 3、分而治之
- 4、了解 STL 的真实效率
- 1)vector 的插入
- 2)queue 的插入和取出
- 5、了解底层调用
- 6、记忆化
- 四、优化总结回顾
- 五、思考题答案
一、前言
- 一款游戏上线分钱,它的奖金来源如下:
奖
金
=
流
水
−
分
成
−
不
可
描
述
的
内
容
−
人
力
成
本
−
资
源
成
本
−
服
务
器
成
本
奖金 = 流水 - 分成 - 不可描述的内容 - 人力成本 - 资源成本 - 服务器成本
奖金=流水−分成−不可描述的内容−人力成本−资源成本−服务器成本
- 前面几项我们做技术的都无法改变,但是服务器成本这块,我们是有责任和义务去减少的;
- 所谓能省则省嘛,你的代码写的越高效,服务器执行效率越高,用到的服务器 CPU 核数越少、内存越少,就越省钱,那么今天就来讲讲怎么省钱吧~~
(本文涉及的所有内容都不含有复杂算法,全部是简单易懂的内容,适合 C++ 零基础)
二、时间复杂度
1、定义
- 在计算机科学中,时间复杂性,又称时间复杂度,算法的时间复杂度是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大
O
O
O 符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。
2、举例
- 1)简单赋值语句(或表达式)的时间复杂度为:
O
(
1
)
O(1)
O(1),如下:
s = a[0];
- 2)遍历一维数组的时间复杂度为:
O
(
n
)
O(n)
O(n),如下:
for(i = 0; i < n; ++i) {
s += a[i];
}
- 3)遍历二维数组的时间复杂度为:
O
(
n
2
)
O(n^2)
O(n2),高维数组以此类推,如下:
for(i = 0; i < n; ++i) {
for(j = 0; j < n; ++j) {
s += a[i][j];
}
}
- 4)二分查找的时间复杂度为:
O
(
l
o
g
n
)
O(log_n)
O(logn)(下文说到的所有对数,都是以
2
2
2 为底)如下:
l = 1, r = n;
while(l <= r) {
mid = (l + r) >> 1;
if(a[mid] <= v) {
r = mid + 1;
}else {
l = mid + 1;
}
}
- 5)斐波那契数列递归枚举的时间复杂度:
O
(
2
n
)
O(2^n)
O(2n),如下:
int fib(unsigned int n) {
if(n <= 1) {
return n;
}
return fib(n-1) + fib(n-2);
}
- 6)深搜枚举全排列的时间复杂度:
O
(
n
!
)
O(n!)
O(n!),如下:
int stk[MAXN], top, has[MAXN];
void dfs(int n, int depth) {
int i;
if (depth == n) {
for (i = 0; i < n; ++i) {
printf("%d", stk[i]);
}
puts("");
}
for (i = 0; i < n; ++i) {
if (!has[i]) {
has[i] = 1;
stk[top++] = i+1;
dfs(n, depth + 1);
--top;
has[i] = 0;
}
}
}
- 7)各种情况嵌套以后形成的综合场景,比如
O
(
n
2
n
)
、
O
(
n
l
o
g
n
)
、
O
(
n
2
l
o
g
n
)
O(n2^n)、O(nlog_n)、O(n^2log_n)
O(n2n)、O(nlogn)、O(n2logn),不再一一举例;
三、C++ 写法优化
1、查找时用键值对代替数组
1)哈希数组
【场景1】从长度为
n
n
n 的数组中查找某个数字是否存在
- 利用枚举的方式,遍历所有元素进行判断,时间复杂度
O
(
n
)
O(n)
O(n),代码实现如下:
const int n = 100000;
void init() {
for (int i = 0; i < n; ++i) {
a[i] = 5 * i;
}
}
bool find(int target) {
for (int i = 0; i < n; ++i) {
if (target == a[i]) {
return true;
}
}
return false;
}
- 这个时间复杂度是会以乘法的形式累乘到调用方的时间复杂度上的,比如调用方调用
m
m
m 次,那么整体下来的时间复杂度就是
O
(
m
n
)
O(mn)
O(mn),调用代码如下:
int countBy() {
int cnt = 0, m = 100000;
while(m--) {
if (find(6857112) ) { // 这里举了个最坏的例子,永远找不到的情况
++cnt;
}
}
return cnt;
}
- 当
n
=
100000
n = 100000
n=100000 时,调用
100000
100000
100000 次
f
i
n
d
find
find 函数,最坏情况是一次都找不到,实测时间:
20313
m
s
20313 ms
20313ms;
- 所以是需要想办法优化的;
【优化1】利用哈希数组的索引来代替数组的遍历
- 观察到数组里的数据都是整数,而且最大不会超过
500000
500000
500000,所以可以把数组的数据映射到一个最大为
500000
500000
500000 的哈希表中,查找的时候只需要利用下标索引,时间复杂度
O
(
1
)
O(1)
O(1),代码如下:
const int n = 100000;
void init() {
memset(hashtbl, 0, sizeof(hashtbl));
for (int i = 0; i < n; ++i) {
hashtbl[ 5 * i ] = 1;
}
}
bool find(int target) {
return hashtbl[target];
}
- 当
n
=
100000
n = 100000
n=100000 时,调用
100000
100000
100000 次
f
i
n
d
find
find 函数,实测时间:
0
m
s
0ms
0ms;
- 但是需要注意数组下标越界的问题,一旦越界就会导致程序崩溃或者让程序进入不确定性,安全性比较低;
2)map
【场景2】从长度为
n
n
n 的数组中查找某个字符串是否存在
- 时间复杂度:
O
(
n
)
O(n)
O(n)
- 实现代码类似【场景1】,不再累述;
【优化2】利用 STL 的 map 来代替数组的遍历
- 观察到数组里的数据不是数字,无法直接映射到数组中,所以我们借助
S
T
L
STL
STL 的
m
a
p
map
map,查找的时间复杂度
O
(
l
o
g
n
)
O(log_n)
O(logn),代码如下:
const int n = 100000;
map<string, bool> maphash;
void init() {
maphash.clear();
for (int i = 0; i < n; ++i) {
maphash[ str[i] ] = 1;
}
}
bool find(string target) {
return maphash.find(target) != maphash.end();
}
-
S
T
L
STL
STL 的
m
a
p
map
map 底层实现是红黑树,在进行查找的时候涉及的常数会比较大,再加上
k
e
y
key
key 是字符串,计算哈希函数的时间也要算上,所以实际的时间复杂度是
O
(
C
l
o
g
n
)
O(Clog_n)
O(Clogn);
- 当
n
=
100000
n = 100000
n=100000 时,调用
100000
100000
100000 次
f
i
n
d
find
find 函数,实测时间:
563
m
s
563ms
563ms;
3)unordered_map
【优化3】利用 STL 的 unordered_map 来代替数组的遍历
-
u
n
o
r
d
e
r
e
d
_
m
a
p
unordered\_map
unordered_map 的用法和
m
a
p
map
map 基本一致,代码不再累述;
-
u
n
o
r
d
e
r
e
d
_
m
a
p
unordered\_map
unordered_map 底层实现是哈希表,同样如果
k
e
y
key
key 是字符串,也有常数运算在,总体来说效率优于
m
a
p
map
map,时间复杂度
O
(
C
)
O(C)
O(C);
- 当
n
=
100000
n = 100000
n=100000 时,调用
100000
100000
100000 次
f
i
n
d
find
find 函数,实测时间:
157
m
s
157ms
157ms;
4)效率排行总结
- 效率排行:
哈
希
数
组
>
u
n
o
r
d
e
r
e
d
_
m
a
p
>
m
a
p
>
数
组
哈希数组 > unordered\_map > map > 数组
哈希数组>unordered_map>map>数组;
- 普适性排行:
数
组
>
u
n
o
r
d
e
r
e
d
_
m
a
p
=
=
m
a
p
>
哈
希
数
组
数组 > unordered\_map == map > 哈希数组
数组>unordered_map==map>哈希数组;
- 安全性:
u
n
o
r
d
e
r
e
d
_
m
a
p
=
=
m
a
p
>
数
组
>
哈
希
数
组
unordered\_map == map > 数组 > 哈希数组
unordered_map==map>数组>哈希数组;
【思考题1】
- 1)以下代码时间复杂度是多少?
- 2)请想办法把它优化到
O
(
n
+
m
)
O(n+m)
O(n+m);
int sum = 0;
for (int i = 0; i < n; ++i) {
for(int j = 0; j < m; j++) {
if(A[i] == B[j]) {
sum += (A[i] + B[j]);
}
}
}
2、关注常数优化
- 项目大了,写的人多了,尤其是各种交叉调用的逻辑,用别人接口的时候也关注下别人实现的时间复杂度,很多逻辑其实都是乘法逻辑,一层一层叠上来,数据量大了就很恐怖了。
- 当然,提供接口的人也要多想想调用方会以什么方式来调用,如果什么都不考虑就写出
O
(
n
)
O(n)
O(n) 或者
O
(
n
2
)
O(n^2)
O(n2) 时间复杂度的代码,这种情况下如果调用方对任何其中一个接口用法不正确,实现者至少也得负一部分责任。
- 当外层循环大量嵌套的时候,内层的一个简单的逻辑可能会被调用千百万次,这时候一些基础接口已经到了无法优化的地步,就只能考虑常数优化了;
1)位运算
【场景3】计算某个玩家的属性的时候,有一步运算是除上2的幂取除数和余数
- 基础四则运算中当属除法最为耗时,将除法单独提取出来,实现如下:
const int n = 3000000;
const int m = 30;
int doCount() {
int s = 0, cnt = n;
int pow2[MAXP] = { 1 };
for (int i = 1; i < m; ++i) {
pow2[i] = pow2[i - 1] * 2;
}
while (cnt --) {
for (int i = 0; i < m; ++i) {
s += rand() / pow2[i];
s += (rand() % pow2[i]);
}
}
return s;
}
- 这个函数的时间复杂度
O
(
n
m
)
O(nm)
O(nm),但是内层循环的 除法 和 取余 操作占据了大量时间,实测时间:
3406
m
s
3406 ms
3406ms;
- 这里有两个优化的点:
- 1)对 2 的幂的除法可以用右移对应位数进行优化;
- 2)对 2 的幂取余数可以用位与 2的幂减1 进行优化;
【优化4】利用位运算进行常数优化
const int n = 3000000;
const int m = 30;
int doCount() {
int s = 0, cnt = n;
int pow2[MAXP] = { 1 };
for (int i = 1; i < m; ++i) {
pow2[i] = pow2[i - 1] * 2;
}
while (cnt --) {
for (int i = 0; i < m; ++i) {
s += rand() >> i;
s += rand() & (pow2[i]-1);
}
}
return s;
}
- 时间复杂度还是
O
(
n
m
)
O(nm)
O(nm),但是将除法和取余替换成位运算后,实测时间:
2706
m
s
2706 ms
2706ms;
【思考题2】
int get(int x) {
int c = 0;
while (x % 2 == 0) {
c++;
x /= 2;
}
return (1 << c);
}
2)避免重复运算
【场景4】循环内部大量调用同一个函数,参数也相同
for (i = 0; i < n; ++i) {
for(j = 0; j < m; j++) {
int v = Cal(MAX_COUNT);
A[i][j] = v * i + j;
}
}
- 这里
C
a
l
Cal
Cal 函数的时间复杂度假设为
X
X
X,那么整个运算过程的时间复杂度就是
O
(
n
m
X
)
O(nmX)
O(nmX);
- 观察发现,
C
a
l
Cal
Cal 函数的计算和
i
i
i、
j
j
j 无关,所以可以提到循环外面来,修改后如下:
【优化5】改变运算顺序避免重复运算
int v = Cal(MAX_COUNT);
for (i = 0; i < n; ++i) {
for(j = 0; j < m; j++) {
A[i][j] = v * i + j;
}
}
- 修改完后的时间复杂度为:
O
(
n
m
+
X
)
O(nm + X)
O(nm+X);
【思考题3】
for (i = 0; i < n; ++i) {
for(j = 0; j < m; j++) {
A[i][j] = Cal(i) + Del(j);
}
}
3)加法代替乘法
- 乘法运算的效率低于加法运算,原因很好解释:你可以随便拿两个三位数在纸上试一下,乘法的运算步骤会比加法的运算步骤复杂得多;
- 假设一个十进制数字位数为
n
n
n 位,加法的时间复杂度为
O
(
n
)
O(n)
O(n),乘法则是
O
(
n
2
)
O(n^2)
O(n2) 的;
【优化6】如果可行尽量用加法代替乘法
- 还是以上面的例子为例,我们发现
A
[
i
]
[
j
]
A[i][j]
A[i][j] 满足某种规律;
A
[
i
]
[
j
]
=
{
j
i
=
0
A
[
i
−
1
]
[
j
]
+
v
0
<
i
<
n
A[i][j] = \begin{cases} j & i=0\\ A[i-1][j] + v & 0<i<n\\ \end{cases}
A[i][j]={jA[i−1][j]+vi=00<i<n
- 那么我们可以通过递推的方式将乘法改成加法,实现代码如下:
int v = Cal(MAX_COUNT);
for (i = 0; i < n; ++i) {
for(j = 0; j < m; j++) {
if(!i) {
A[i][j] = j;
}else {
A[i][j] = (A[i-1][j] + v);
}
}
}
【思考题4】
- 不用乘法和除法求解组合数,组合数定义如下:
C
n
m
=
n
!
m
!
(
n
−
m
)
!
C_n^m = \frac {n!}{m!(n-m)!}
Cnm=m!(n−m)!n!
4)再次优化取模
【场景5】需要对一批数据进行求和取模
- 取模满足 模’+’ 运算,所以可以边加边取模,实现代码如下:
int doSumMod() {
int s = 0;
for (int i = 0; i < n; ++i) {
s = (s + val[i]) % MOD;
}
return s;
}
- 当
n
=
100000000
n = 100000000
n=100000000 时,该代码的运行实测时间为:
891
m
s
891 ms
891ms;
【优化7】不必要时不进行取模运算
- 取模运算当被取模数小于模时,不需要进行取模,所以上面的写法可以改成如下:
int doSumMod() {
int s = 0;
for (int i = 0; i < n; ++i) {
s += val[i];
if (s >= MOD) s %= MOD;
}
return s;
}
- 当
n
=
100000000
n = 100000000
n=100000000 时,该代码的运行实测时间为:
187
m
s
187 ms
187ms;
【思考题5】
ans = -520 % 1314; //???
5)尽量少用 #define
【场景6】求两个数字中的大者
#define MAX(a,b) ((a)>(b)?(a):(b))
- 因为
#
d
e
f
i
n
e
\#define
#define 是宏替换,所以这里的
a
a
a 或者
b
b
b 会计算两次,想象下如下的调用:
MAX(for(int i = 0; i <100000; ++i) s += i, for(int i = 0; i <100000; ++i) s += i);
- 你可能会说:谁会写出这样的代码???
- 然而,项目大了,什么代码都会有,你永远无法预料调用你代码的人的水平是怎么样的;
【优化8】宁以 const 或者 函数代替 #define
int Max(int a, int b){
return a > b ? a : b;
}
- 如果觉得普适性不强,想要支持
c
h
a
r
、
s
h
o
r
t
、
f
l
o
a
t
、
d
o
u
b
l
e
char、short、float、double
char、short、float、double,就搞个模板:
template <class T>
T Max(T a, T b){
return a > b ? a : b;
}
【思考题6】
#define ABS(x) x<0?-x:x
6)循环终止条件
【场景7】循环的终止条件是一个表达式的时候
-
f
o
r
for
for 循环的终止条件是一个表达式的时候,每次循环都会判断条件是否满足,也就是每次循环都会进行一次表达式的计算,所以应该尽量简化表达式的计算方式,举个简单的例子如下:
int For() {
int s = 0;
for (int i = 0; i*i < n; ++i) {
for (int j = 0; j*j < n; ++j) {
for (int k = 0; k*k < n; ++k) {
// TODO
}
}
}
return s;
}
- 为了把注意力集中在这个条件判断上,我跑了个空的三层循环,即运算消耗都在循环终止条件的表达式上了,那么当
n
=
1000000
n = 1000000
n=1000000 时,实测的时间消耗为:
2125
m
s
2125 ms
2125ms;
【优化9】尽量最大限度的简化循环终止条件的逻辑运算
- 这段代码的终止条件基本都是平方运算,由于:
i
∗
i
<
n
等
价
于
i
<
n
i*i < n \ 等价于 \ i < \sqrt n
i∗i<n 等价于 i<n
- 我们可以把表达式进行一个转换,实现代码如下:
int For2() {
int s = 0;
int v = sqrt(n + 1e-8);
for (int i = 0; i < v; ++i) {
for (int j = 0; j < v; ++j) {
for (int k = 0; k < v; ++k) {
// TODO
}
}
}
return s;
}
- 简化循环终止条件的运算以后,当
n
=
1000000
n = 1000000
n=1000000 时,实测耗时:
1453
m
s
1453 ms
1453ms
【思考题7】
for(i = 0; i < vec.size(); i++) {
// TODO
}
3、分而治之
1)二分查找
【场景8】给定一个数,在一个有序数组中查找比它大的最小的数,不存在输出 -1
- 对于数组中进行数据查找的问题,本文一开始就已经提到了一些解决方案;不过这个问题比较特殊,给定的数组本身是一个有序数组,对于数组元素个数为
n
n
n 的情况,可以利用二分查找在
O
(
l
o
g
n
)
O(log_n)
O(logn) 的时间内进行求解;
【优化10】利用二分查找优化有序数组的查找效率
#define MAXA 10
int bin[MAXA] = { 1, 2, 4, 6, 8, 9, 11, 88, 520, 1314 };
int BinarySearch(int val) {
int l = 0, r = MAXA - 1;
int m, ans = -1;
while (l <= r) {
m = (l + r) >> 1;
if (bin[m] > val) {
ans = bin[m];
r = m - 1;
}
else {
l = m + 1;
}
}
return ans;
}
【思考题8】
- 如果要在一个有序数组中查找比它小的最大的数,上面的代码要怎么改?
2)二分快速幂
【场景9】计算
a
b
m
o
d
c
(
a
,
b
<
2
64
)
a^b \mod c(a,b<2^{64})
abmodc(a,b<264) ,一般用在加解密算法(例如 RSA )中
- 在这个问题上,又是乘方运算,又是取模运算,本身就已经很耗时了,再加上
a
,
b
a,b
a,b 的数据范围,如果仅仅是枚举遍历求解,以目前计算机的水平,肯定无法在规定时间内求解的;
【优化11】利用递归二分的性质将线性时间复杂度转换成对数时间复杂度
- 次幂运算是最满足二分递归性质的,因为它满足如下公式:
a
b
m
o
d
c
=
{
1
m
o
d
c
b
为
0
a
(
a
(
b
−
1
)
/
2
)
2
m
o
d
c
b
为
奇
数
(
a
b
/
2
)
2
m
o
d
c
b
为
正
偶
数
a^b \mod c = \begin {cases} 1 \mod c & b 为 0 \\ a(a^{(b-1)/2})^2 \mod c & b为奇数\\ (a^{b/2})^2 \mod c & b为正偶数\\ \end{cases}
abmodc=⎩⎪⎨⎪⎧1modca(a(b−1)/2)2modc(ab/2)2modcb为0b为奇数b为正偶数
所以可以利用递归把本来
O
(
b
)
O(b)
O(b) 的时间复杂度的问题转化成
O
(
l
o
g
b
)
O(log_b)
O(logb),具体实现可以参考以下文章:
夜深人静写算法(十九)- 快速幂
4、了解 STL 的真实效率
1)vector 的插入
-
S
T
L
STL
STL 的
v
e
c
t
o
r
vector
vector 意为动态数组,弥补了 C++ 原生不支持动态数组的缺陷;
- 但是它的效率实在不敢恭维,所以有时候如果不是很必要的情况下,能不用尽量不用;
- 测试一个
v
e
c
t
o
r
vector
vector 的插入效率代码如下:
const int MAXDC = 100;
void DataCollect() {
vector <int> v;
for (int i = 0; i < MAXDC; ++i) {
v.push_back(i);
}
}
- 对以上函数调用
100000
100000
100000 次,实测时间为:
3469
m
s
3469 ms
3469ms;
-
v
e
c
t
o
r
vector
vector 的内存分配策略采用的是倍增法,会预先申请一块内存,每次插入一个元素会对当前剩余内存进行一个预见检测,如果超出这个预分配内存,会对现有内存进行倍增,并且拷贝原有内存数据到新的内存上去,所以这里面会有一些申请堆空间、构造函数、内存拷贝函数的调用等等,不免会有许多常数时间调用,具体规则参见如下文章:STL vector 扩容策略
【优化12】对 vector 进行内存预分配
- 如果确定 vector 的数据一定至少有
n
n
n 个,那么我们可以利用
r
e
s
e
r
v
e
reserve
reserve 函数预先分配
n
n
n 个元素的内存出来,这样起码会减少很多内存重分配的次数,具体实现代码如下:
const int MAXDC = 100;
void DataCollect() {
vector <int> v;
v.reserve(MAXDC);
for (int i = 0; i < MAXDC; ++i) {
v.push_back(i);
}
}
- 同样,对以上函数调用
100000
100000
100000 次,实测时间为:
1437
m
s
1437 ms
1437ms,效率提升了不少;
【优化13】小数组尽量不用 vector
- 我们发现上面涉及的数组往往比较小,如果直接用一个固定长度为
100
100
100 的数组也能够满足需求,那就尝试用数组来实现一下,实现代码如下:
const int MAXDC = 100;
void DataCollect() {
int stk[MAXDC], top = 0;
for (int i = 0; i < MAXDC; ++i) {
stk[top++] = i;
}
}
- 同样,对以上函数调用
100000
100000
100000 次,惊人的发现,实测时间为:
31
m
s
31 ms
31ms;
- 所以可以看出,如果数据量比较小,并且频繁重复调用同一个数组的数据插入时,不建议用
v
e
c
t
o
r
vector
vector;
- 但是需要注意的是,如果用数组,一定要确保数组下标在安全范围内,否则有可能引起进程的崩溃;
2)queue 的插入和取出
-
S
T
L
STL
STL 的队列
q
u
e
u
e
queue
queue 实现了
(
F
I
F
O
)
(FIFO)
(FIFO) 先进先出 的数据结构;
- 内存分配策略基本上和
v
e
c
t
o
r
vector
vector 是一致的,所以也会面临内存重分配和数据拷贝;
- 基于效率考虑,不建议直接用
S
T
L
STL
STL 的
q
u
e
u
e
queue
queue,可以自己实现一个支持扩容的队列;
【思考题9】
string s;
for(i = 0; i < n; ++i) {
s += "K";
}
5、了解底层调用
1)慎用三角函数
- 三角函数的计算用的是
C
o
r
d
i
c
Cordic
Cordic 算法,整体来说是一个迭代,一些位移 和 加减法,所以如果一些常用,精度要求不高的,能够用常量代替的可以尽量用常量,比如 :
3.1415927
3.1415927
3.1415927 代替
a
c
o
s
(
−
1.0
)
acos(-1.0)
acos(−1.0);
const int MAXPI = 100000000;
double CalcCircle() {
double s = 0;
for (int i = 0; i < MAXPI; ++i) {
s += acos(-1.0) * i * i;
}
return s;
}
- 以上代码实测时间:
1532
m
s
1532 ms
1532ms;
【优化14】精度允许的情况尽量用常量代替三角函数
const int MAXPI = 100000000;
double CalcCircle() {
double s = 0;
const double PI = 3.1415927;
for (int i = 0; i < MAXPI; ++i) {
s += PI * i * i;
}
return s;
}
- 以上代码实测时间:
672
m
s
672 ms
672ms;
【思考题10】
- 浮点数计算的时候是有精度损失的,那么如何判断两个浮点数是否相等;
2)类型转换
【场景10】静态类型转换 和 动态类型转换 的时耗性
-
s
t
a
t
i
c
_
c
a
s
t
static\_cast
static_cast 和
d
y
n
a
m
i
c
_
c
a
s
t
dynamic\_cast
dynamic_cast 实测下来效率差距不大;
const int MAXT = 100000000;
void StaticCast_Test() {
Inher *p = new Inher();
for (int i = 0; i < MAXT; ++i){
Base *pkBase = static_cast<Base*>(p);
}
}
void Dynamic_Test() {
Inher *p = new Inher();
for (int i = 0; i < MAXT; ++i){
Base *pkBase = dynamic_cast<Base*>(p);
}
}
- 当调用
10000000
10000000
10000000 时,均为
16
m
s
16 ms
16ms 左右;
6、记忆化
1)记忆化搜索
- 记忆化搜索是动态规划(并不属于搜索范畴)的一种,思路就是将已经计算过的内容记录下来,避免下次遇到的时候重复计算,比较经典的就是递归版本的斐波那契数列的计算;
【场景11】斐波那契数列的计算
long long fib(unsigned int n) {
if(n <= 1) {
return n;
}
return fib(n-1) + fib(n-2);
}
- 虽然代码很短,但是这个函数的实现采用了递归,参考深度优先搜索,每次往下递归相当于扩展了两个二叉树的子节点,所以算法的时间复杂度是
O
(
2
n
)
O(2^n)
O(2n) 的,如下图所示:
n
n-1
n-2
n-2
n-3
n-3
n-4
n-3
n-4
n-4
n-5
n-4
n-5
n-5
n-6
【优化15】记忆化搜索将指数级时间复杂度转变为多项式级时间复杂度
- 来看下经过记忆化优化以后的递归版本的斐波那契数列计算:
void init() {
memset(f, -1, sizeof(f));
}
long long fib(unsigned int n) {
if(n <= 1) {
return n;
}
if(f[n] != -1) {
return f[n];
}
return f[n] = fib(n-1) + fib(n-2);
}
- 利用一个静态数组来记录斐波那契数列第n项的值,并且初始化为 -1 表示没有计算过,一旦计算出来就给它赋值,下次再访问的时候直接返回已经计算出来的值即可;
- 均摊时间复杂度
O
(
n
)
O(n)
O(n);
- 当然,还有比较常用的记忆化搜索,如区间动态规划、状态压缩,如果对动态规划感兴趣,可以参考这篇文章:夜深人静写算法(二)- 动态规划
2)预处理
- 记忆化 和 预处理 是两个截然相反的过程,预处理指的是在你需要某些运算信息的时候,它早就已经计算好了,而不是等到需要的时候再去重新计算,还是以斐波那契数列为例:
f[0] = 0, f[1] = 1;
for(int i = 2; i < MAXN; ++i) {
f[i] = f[i-1] + f[i-2];
}
- 这个时间复杂度很明确,是
O
(
n
)
O(n)
O(n) 的,并且在接下来每次询问中,都是O(1)的;
【优化16】预处理能够处理所有可预见范围内的计算
- 但是预处理需要确保一个事情,就是所有的询问必须保证数据范围在预处理的数据范围内,否则就起不到预处理的作用了;
3)前缀和
【场景12】计算斐波那契数列的第L项到第R项的和(L<=R)
- 朴素做法就是每次将
0
−
R
0 - R
0−R 项的和都计算出来,然后累加,时间复杂度
O
(
R
)
O(R)
O(R),代码实现如下:
f[0] = 0, f[1] = 1;
for(int i = 2; i <= R; ++i) {
f[i] = f[i-1] + f[i-2];
}
s = 0;
for(int i = L; i <= R; ++i) {
s += f[i];
}
- 这个时候,如果请求的数量很多,这里其实伴随着很多冗余计算,所以这里有个技巧叫做:前缀和;
【优化17】预处理数组前缀和可以做到询问时O(1)
- 1)利用预处理记录下所有斐波那契数列的前缀和,存储在 sum 数组,时间复杂度
O
(
n
)
O(n)
O(n);
sum[0] = 0;
for(int i = 1; i < MAXN; ++i) {
sum[i] = sum[i-1] + f[i];
}
- 2)询问时通过两个前缀和相减获得所求值,时间复杂度
O
(
1
)
O(1)
O(1);
NumberType getFib(NumberType L, NumberType R) {
return sum[R] - sum[L-1];
}
【思考题11】
- 给定一个
100
×
100
×
100
100 \times 100 \times 100
100×100×100 的三维数组,然后是
100000
100000
100000 次询问;
- 每次询问问的是
(
x
l
,
y
l
,
z
l
)
−
(
x
r
,
y
r
,
z
r
)
(x_l,y_l,z_l)-(x_r,y_r,z_r)
(xl,yl,zl)−(xr,yr,zr) 这个子立方数组的和,如何最高效的求解;
- 时间复杂度是多少;
四、优化总结回顾
【优化1】利用哈希数组的索引来代替数组的遍历
【优化2】利用
S
T
L
STL
STL 的
m
a
p
map
map 来代替数组的遍历
【优化3】利用
S
T
L
STL
STL 的
u
n
o
r
d
e
r
e
d
_
m
a
p
unordered\_map
unordered_map 来代替数组的遍历
【优化4】利用位运算进行常数优化
【优化5】改变运算顺序避免重复运算
【优化6】如果可行尽量用加法代替乘法
【优化7】不必要时不进行取模运算
【优化8】宁以
c
o
n
s
t
const
const 或者 函数代替
#
d
e
f
i
n
e
\#define
#define
【优化9】尽量最大限度的简化循环终止条件的逻辑运算
【优化10】利用二分查找优化有序数组的查找效率
【优化11】利用递归二分的性质将线性时间复杂度转换成对数时间复杂度
【优化12】对 vector 进行内存预分配
【优化13】小数组尽量不用
v
e
c
t
o
r
vector
vector
【优化14】精度允许的情况尽量用常量代替三角函数
【优化15】记忆化搜索将指数级时间复杂度转变为多项式级时间复杂度
【优化16】预处理能够处理所有可预见范围内的计算
【优化17】预处理数组前缀和可以做到询问时
O
(
1
)
O(1)
O(1)
五、思考题答案
字数太多描述不下了,答案见下期吧
这篇关于C++ 性能骨灰级优化(推荐)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!