对于时间与空间复杂度,可以说是非常熟悉的概念了;但是在真正去使用的时候,在做到那些对于时间和空间的限制要求很高的编程题的时候还是会发现对于时间和空间复杂度的掌握与运用实在是不够,在这个方面的意识也不足。本文记录在做编程题的时候遇到的关于时间空间效率的问题并作记录。便于日后去巩固,不断加强自己对于算法效率的意识。
时间效率:
0.获取程序运行时间方法:
使用大数据量的数据+下面所述的库函数变量来测试——
c/c++ 计算程序运行时间,精确到毫秒_大白机器人的博客-CSDN博客_c++计算程序运行时间毫秒
1.没有循环终止条件。
就是说程序中存在一个循环,但是这个循环没有终止条件,或者说是条件太宽泛,在运行的时候虽然能输出结果,但这个循环会一直循环下去,导致运行时间过长。
2.函数调用超时。
例如:递归调用因为经常会调用自身很多次,所以时间的复杂度是指数级别的
如 调用一个递归函数来求阶乘,运行测试正确,却出现了超时情况。然后换了一种做法,用一个for循环去求阶乘,结果就通过了。
3.程序算法不够优化。
就是说自己的程序太复杂,存在更快更有效率的方法。所以在解决问题时,应尽量选择简单的算法。如果遇到了这种情况,只能换一个思路了。
4、关于vector容器的时间效率劣势:
(1)初始化的时候,使用reserve()设置vector的容量(capacity)
喜欢使用 vector,因为他们只需要往向容器中添加元素,而不用事先操心容器大小的问题。如果由一个容量为 0 的 vector 开始,往里面添加元素会花费大量的运行性能。如果你之前就知道 vector 需要保存多少元素,就应该提前为其分配足够的空间
在我的计算机中,未预分配空间的情况用了 5145 微秒(us),而预分配了空间的情况下只用了 1279 微秒,性能提高了 75.14%!!!
这个情况在 Scott Meyers 的书 Effective STL-50条有效使用STL的经验中有:
“对于 vector 和 string,在需要更多空间的时候,会做与 realloc 等效的事情。这种类似 realloc 的操作有4个步骤:
-
- 分别一个新的内存块,其容量是容器当前容量的数倍。多数实现中,vector 和 string 容量的提升因子在 1.5 和 2 之间。
-
- 从容器原来占用的内存中将元素拷贝到新分配的内存中。
-
- 释放原有内存中的对象。
-
- 释放原有内存。
有了所有这些操作:分配、回收、拷贝和释放,如果说这些步骤(对于性能)极其昂贵,你一点都不应该感到惊讶。当然,你肯定不希望频繁的进行这样的操作。如果这还没有打动你,那么想想每次进行这些步骤的时候,vector 和 string 中所有的迭代器、指针和引用都会失效。这意味着一个简单的插入操作,对于其它使用了当前 vector 或 string 中的迭代器、指针或引用的数据结构,都有可能引起对它们进行更新。”
一言以蔽之:vector内存不够用的时候,分配二倍的空间,然后把之前的copy到新的空间中
(2)删除清空的时候使用clear()
从性能上来说, 同样清空vector, resize的性能要比clear的性能差
注意,clear的同时会讲size改成0——》没必要在clear之后还resize:
参见:vector::clear - C++ Reference
Removes all elements from the vector (which are destroyed), leaving the container with a size of 0.
(3)增加元素的时候使用emplace_back替代push_back
不过只有一点点用处,效率差别并不大
(4)复制元素效率swap > copy > assign > 直接赋值 > push_back赋值
这里比较好的是swap和copy,但需要注意的是,swap是内存交换,这里经过swap后,src的内容会与dst发生交换。其次copy也不错,不过需要注意的是,swap需要提前分配好足够的内存,比如在声明是分配或者用resize分配(不能用reserve,二者区别可以去网上查),否则会导致程序崩溃。
C++中关于效率的讨论:vector中慎用push_back(T) 方式_王建博09的博客-CSDN博客
C++如何提高Vector效率的一些方法_AlwaysSimple的博客-CSDN博客_c++ vector效率
空间效率:
Memory Limit Exceeded(内存超限)或者空间复杂度高的情况:
一个算法在计算机中所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。
一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息
1、在循环内部重复申请新的数组等容器
类似于归并排序实现的时候需要在每一级规模的归并中使用临时数组来保存merge的序列;在循环外部一次性申请一个足够大小的临时容器比每次申请所使用的空间消耗要小很多很多,尤其是在大规模循环的时候
2、对于以结构体/类对象为元素类型的容器,使用指针类型,而不要使用对象本身;