关于算法空间复杂度的问题
‘算法空间复杂度’,别以为这个东西多么高大上,我保证你看完这篇文章就能明白。
最近在啃算法,发现非常有趣。在我学习的过程中发现了一个问题,那就是空间复杂度的问题,它绝对是效率的杀手。
关于空间复杂度的介绍(摘自百度)
空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度,记做S(n)=O(f(n))。比如直接插入排序的时间复杂度是O(n^2),空间复杂度是O(1) 。而一般的递归算法就要有O(n)的空间复杂度了,因为每次递归都要存储返回信息。一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量。
拿插入排序来说。插入排序和我们现实中打扑克牌时理清牌的那一步很像。拿斗地主来说,我们经常会把顺子理出来,回想下我们是怎么理的?比如我们有这么5张牌9、8、10、7、6。过程应该是这样的:
9、8、10、7、6
从8开始,8发现9比它大,然后8插到9前面。
8、9、10、7、6
然后到10,10发现它比前面2个都大,所以不用动。
8、9、10、7、6
然后到7,7发现10比它大,然后跟10换位置。
8、9、7、10、6
然后7又发现9也比它大,于是又跟9换位置
8、7、9、10、6
然后7又发现8也比它大,于是又跟8换位置
7、8、9、10、6
等等,好像有点不对。到牌‘7’的那一步好像太麻烦了,我们平时是把7拿起来,直接插到8前面就完事了。简单快捷,绝对比一个个插要快。没错!这就是空间复杂度的问题。下面直接上2组代码来校验一下。
public static void InsertSort(List<int> list) { for (int i = 0; i < list.Count; i++) { for (int j = i; j - 1 >= 0; j--) { if (list[j - 1] > list[j]) { int temp = list[j - 1]; list[j - 1] = list[j]; list[j] = temp; Console.WriteLine(string.Join(",", list)); } else break; } } } static void Main(string[] args) { List<int> list = new List<int>() { 9,8,10,7,6 }; InsertSort(list); Console.ReadKey(); }
我们可以看到,这种方法真是很笨。。就是一个一个往前插。。这当然不是我们想要的。。我们再改进下
public static void InsertSort2(List<int> list) { for (int i = 0; i < list.Count; i++) { int j = i; int baseNumber = list[j];//先把牌抽出来 for (; j - 1 >= 0; j--) { if (list[j - 1] > baseNumber) { list[j] = list[j - 1];//后面的往前推 } else break; } list[j] = baseNumber;//结束后把牌插入到空位 } } static void Main(string[] args) { List<int> list = new List<int>() { 9,8,10,7,6 }; InsertSort2(list); }
其实思路就是先抽出1张牌(比如抽出的那张牌的位置为3,注意:现在3是空出来的),如果前一张牌(位置2)比它大,就把2移到3上面去。2就空出来了。
接着再前面那张(位置1)如果比抽出来的牌大,继续往前移。因为2空出来了,1移到2上。现在1空出来了。
然后把抽出来的牌放到1上,完成。
过程如下
8、9、10、7、6
7
8、9、10、 、6
8、9、 、10、6
8、 、9 、10、6
、8、9 、10、6
7、8、9 、10、6
再来看看执行效率方面到底差了多远
public static void InsertSort(List<int> list) { for (int i = 0; i < list.Count; i++) { for (int j = i; j - 1 >= 0; j--) { if (list[j - 1] > list[j]) { int temp = list[j - 1]; list[j - 1] = list[j]; list[j] = temp; } else break; } } } public static void InsertSort2(List<int> list) { for (int i = 0; i < list.Count; i++) { int j = i; int baseNumber = list[j];//先把牌抽出来 for (; j - 1 >= 0; j--) { if (list[j - 1] > baseNumber) { list[j] = list[j - 1];//后面的往前推 } else break; } list[j] = baseNumber;//结束后把牌插入到空位 } } static void Main(string[] args) { List<int> list = new List<int>(); List<int> list2 = new List<int>(); Random random = new Random(); for (int i = 0; i < 50000; i++) { var temp = random.Next(); list.Add(temp); list2.Add(temp); } Stopwatch watch = new Stopwatch(); watch.Start(); InsertSort(list); watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); watch.Reset(); watch.Start(); InsertSort2(list2); watch.Stop(); Console.WriteLine(watch.ElapsedMilliseconds); Console.ReadKey(); }
运行结果
快了将近1倍吧
第一种方法需要不短的交换2个元素。因为需要交换2个元素,所以我们还需要用1个临时变量来保存其中1个元素的值
int temp = list[j - 1];
list[j - 1] = list[j];
list[j] = temp;
第二种方法则是直接将后面的元素往前移。
list[j] = list[j - 1];
如果说第一个种方法元素交换的次数为n,那第二种方法交换的次数则为 n/2+1。
堆排,快排时很多时候都会运用到这种思想。不知道大家有没得到一些帮助呢?平时编程的时候是否也要注意到呢?