算法 - 空间复杂度 时间复杂度

关于算法空间复杂度的问题

‘算法空间复杂度’,别以为这个东西多么高大上,我保证你看完这篇文章就能明白。

最近在啃算法,发现非常有趣。在我学习的过程中发现了一个问题,那就是空间复杂度的问题,它绝对是效率的杀手。

关于空间复杂度的介绍(摘自百度)

空间复杂度(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。

堆排,快排时很多时候都会运用到这种思想。不知道大家有没得到一些帮助呢?平时编程的时候是否也要注意到呢?

分类: 算法&数据结构

好文要顶 关注我 收藏该文  

Poiuyt_cyc
关注 - 4
粉丝 - 74

+加关注

0

0

« 上一篇:构建属于自己的ORM框架之二--IQueryable的奥秘
» 下一篇:解析哈希表

https://www.cnblogs.com/irenebbkiss/p/4243715.html

随笔- 70  文章- 0  评论- 1 

算法的性能评价------空间复杂度和时间复杂度

一个算法的优劣往往通过算法复杂度来衡量,算法复杂度包括时间复杂度和空间复杂度。

时间复杂度是算法的所需要消耗的时间,时间越短,算法越好。可以对算法的代码进行估计,而得到算法的时间复杂度。

一般来说,算法代码简短精悍可以用来减少算法的时间复杂度!

空间复杂度指的是算法程序在执行时所需要的存储空间。空间复杂度可以分为以下两个方面!

1.程序的保存所需要的存储空间资源。即程序的大小;

2.程序在执行过程中所需要消耗的存储空间资源,如中间变量等;

一般来说,程序的大小越小,执行过程中消耗的资源越少,这个程序就越好!

下面为时间复杂度和空间复杂度的计算

复制代码

时间复杂度是总运算次数表达式中受n的变化影响最大的那一项(不含系数)
比如:一般总运算次数表达式类似于这样:
a*2n+b*n3+c*n2+d*n*lg(n)+e*n+f
a ! =0时,时间复杂度就是O(2n);
a=0,b<>0 =>O(n3);
a,b=0,c<>0 =>O(n2)依此类推
例子:
(1)   for(i=1;i<=n;i++)   //循环了n*n次,当然是O(n2)
            for(j=1;j<=n;j++)
                 s++;
(2)   for(i=1;i<=n;i++)//循环了(n+n-1+n-2+...+1)≈(n2)/2,因为时间复杂度是不考虑系数的,所以也是O(n2)
            for(j=i;j<=n;j++)
                 s++;
(3)   for(i=1;i<=n;i++)//循环了(1+2+3+...+n)≈(n^2)/2,当然也是O(n2)
            for(j=1;j<=i;j++)
                 s++;
(4)   i=1;k=0;//循环了n-1≈n次,所以是O(n)
      while(i<=n-1){
           k+=10*i;
i++; }
(5) for(i=1;i<=n;i++)
             for(j=1;j<=i;j++)
                 for(k=1;k<=j;k++)
                       x=x+1;
//
循环了(1

2

+2

2

+3

2

+...+n

2

)=n(n+1)(2n+1)/6(这个公式要记住哦)≈(n

3

)/3,不考虑系数,自然是O(n

3

)
另外,在时间复杂度中,log

2

n与lg(n)(同lg

10

(n))是等价的,因为对数换底公式:
log

a

b=log

c

b/log

c

a
所以,log

2

n=log

2

10 * lg(n),忽略掉系数,二者当然是等价的
二、计算方法
1.一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。
但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。
并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。
一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。
2.一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),
因此,算法的时间复杂度记做:T(n)=O(f(n))。随着模块n的增大,算法执行的时间的增长率和f(n)的增长率成正比,
所以f(n)越小,算法的时间复杂度越低,算法的效率越高。
在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,
再找出T(n)的同数量级(它的同数量级有以下:1,Log

2

n ,n ,nLog

2

n ,n的平方,n的三次方,2的n次方,n!),
找出后,f(n)=该数量级,若T(n)/f(n)求极限可得到一常数c,则时间复杂度T(n)=O(f(n))。
3.常见的时间复杂度
按数量级递增排列,常见的时间复杂度有:
常数阶O(1),  对数阶O(log

2

n),  线性阶O(n),  线性对数阶O(nlog

2

n),  平方阶O(n

2

),
 立方阶O(n

3

),..., k次方阶O(n

k

), 指数阶O(2

n

) 。
其中,
1.O(n),O(n

2

), 立方阶O(n

3

),..., k次方阶O(n

k

) 为多项式阶时间复杂度,分别称为一阶时间复杂度,二阶时间复杂度。。。。
2.O(2

n

),指数阶时间复杂度,该种不实用
3.对数阶O(log

2

n),   线性对数阶O(nlog

2

n),除了常数阶以外,该种效率最高
例:算法:
  for(i=1;i<=n;++i)
  {
     for(j=1;j<=n;++j)
     {
         c[ i ][ j ]=0; //该步骤属于基本操作 执行次数:n

2

          for(k=1;k<=n;++k)
               c[ i ][ j ]+=a[ i ][ k ]*b[ k ][ j ]; //该步骤属于基本操作 执行次数:n

3

     }
  }
  则有 T(n)= n

2

+n

3

,根据上面括号里的同数量级,我们可以确定 n

3

为T(n)的同数量级
  则有f(n)= n

3

,然后根据T(n)/f(n)求极限可得到常数c
  则该算法的 时间复杂度:T(n)=O(n

3

)
四、定义:
如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n),它是n的某一函数 T(n)称为这一算法的“时间复杂性”。
当输入量n逐渐加大时,时间复杂性的极限情形称为算法的“渐近时间复杂性”。
我们常用大O表示法表示时间复杂性,注意它是某一个算法的时间复杂性。大O表示只是说有上界,由定义如果f(n)=O(n),那显然成立f(n)=O(n^2),它给你一个上界,但并不是上确界,但人们在表示的时候一般都习惯表示前者。
此外,一个问题本身也有它的复杂性,如果某个算法的复杂性到达了这个问题复杂性的下界,那就称这样的算法是最佳算法。
“大O记法”:在这种描述中使用的基本参数是 n,即问题实例的规模,把复杂性或运行时间表达为n的函数。这里的“O”表示量级 (order),比如说“二分检索是 O(logn)的”,也就是说它需要“通过logn量级的步骤去检索一个规模为n的数组”记法 O ( f(n) )表示当 n增大时,运行时间至多将以正比于 f(n)的速度增长。
这种渐进估计对算法的理论分析和大致比较是非常有价值的,但在实践中细节也可能造成差异。
例如,一个低附加代价的O(n2)算法在n较小的情况下可能比一个高附加代价的 O(nlogn)算法运行得更快。
当然,随着n足够大以后,具有较慢上升函数的算法必然工作得更快。
O(1)
Temp=i;i=j;j=temp;                    
以上三条单个语句的频度均为1,该程序段的执行时间是一个与问题规模n无关的常数。
算法的时间复杂度为常数阶,记作T(n)=O(1)。如果算法的执行时间不随着问题规模n的增加而增长,
即使算法中有上千条语句,其执行时间也不过是一个较大的常数。此类算法的时间复杂度是O(1)。
O(n

2

) 
1) 交换i和j的内容      
sum=0;                 (一次)
      for(i=1;i<=n;i++)       (n次 )   
      for(j=1;j<=n;j++) (n

2

次 )    
      sum++;       (n^2次 ) 
解:T(n)=2n^2+n+1 =O(n^2)
2)        
for (i=1;i<n;i++)     {
         y=y+1;         //频度是n-1   
     for (j=0;j<=(2*n);j++) 
               x++;       //频度是(n-1)*(2n+1)=2n

2

-n-1 
         }         
    f(n)=2n

2

-n-1+(n-1)=2n

2

-2           该程序的时间复杂度T(n)=O(n

2

).         
O(n)                                                              
3)
    a=0;     b=1;                     //频度:2
    for (i=1;i<=n;i++) //频度: n
    {
         s=a+b;    //频度: n-1
        b=a;     //频度:n-1
         a=s;     //频度:n-1
     }                 
 T(n)=2+n+3(n-1)=4n-1=O(n). 
                                                                                                  
O(log

2

n )
4)
      i=1;       //频度是1 
    while (i<=n)
        i=i*2; //频度是f(n),   
则:2

f(n)

<=n;f(n)<=log

2

n               取最大值f(n)= log

2

n,           T(n)=O(log

2

n )
O(n

3

)
5)
  for(i=0;i<n;i++)     {
      for(j=0;j<i;j++)          { 
          for(k=0;k<j;k++)
              x=x+2; 
         }
     } 
解:当i=m, j=k的时候,内层循环的次数为k当i=m时, j 可以取 0,1,...,m-1 , 所以这里最内循环共
进行了0+1+...+m-1=(m-1)m/2次所以,i从0取到n, 则循环共进行了: 0+(1-1)*1/2+...+(n-1)n/2=n(n+1)(n-1)/6
所以时间复杂度为O(n

3

).                                   
我们还应该区分算法的最坏情况的行为和期望行为。如快速排序的最坏情况运行时间是 O(n

2

),
但期望时间是 O(nlogn)。通过每次都仔细 地选择基准值,我们有可能把平方情况 (即O(n

2

)情况)
的概率减小到几乎等于 0。在实际中,精心实现的快速排序一般都能以 (O(nlogn)时间运行。
 下面是一些常用的记法:
 访问数组中的元素是常数时间操作,或说O(1)操作。一个算法如 果能在每个步骤去掉一半数据元素,
如二分检索,通常它就取 O(logn)时间。用strcmp比较两个具有n个字符的串需要O(n)时间。
常规的矩阵乘算法是O(n

3

),因为算出每个元素都需要将n对 元素相乘并加到一起,所有元素的个数是n

2

。
 指数时间算法通常来源于需要求出所有可能结果。例如,n个元 素的集合共有2n个子集,所以要求出
所有子集的算法将是O(2n)的。指数算法一般说来是太复杂了,除非n的值非常小,因为,在 这个问题
中增加一个元素就导致运行时间加倍。不幸的是,确实有许多问题 (如著名的“巡回售货员问题” ),
到目前为止找到的算法都是指数的。如果我们真的遇到这种情况,通常应该用寻找近似最佳结果的算法替代之。

复制代码

 二.空间复杂度

复制代码

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。
一个算法在计算机存储器上所占用的存储空间,
包括程序代码所占用的空间,输入数据所占用的空间和辅助变量所占用的空间这三个方面。
算法的输入输出数据所占用的存储空间是由要解决的问题决定的,是通过参数表由调用函数传递而来的,
它不随本算法的不同而改变。存储算法本身所占用的存储空间与算法书写的长短成正比,要压缩这方面的存储空间,
就必须编写出较短的算法。算法在运行过程中临时占用的存储空间随算法的不同而异,有的算法只需要占用少量的临时工作单元,
而且不随问题规模的大小而改变,我们称这种算法是“就地"进行的,是节省存储的算法,如这些介绍过的几个算法都是如此;
有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,
例如将在第九章介绍的快速排序和归并排序算法就属于这种情况。
    分析一个算法所占用的存储空间要从各方面综合考虑。如对于递归算法来说,一般都比较简短,算法本身所占用的存储空间较少,但运行时需要一个附加堆栈,从而占用较多的临时工作单元;若写成非递归算法,一般可能比较长,算法本身占用的存储空间较多,但运行时将可能需要较少的存储单元。
    一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。若一个算法为递归算法,其空间复杂度为递归所使用的堆栈空间的大小,它等于一次调用所分配的临时存储空间的大小乘以被调用的次数(即为递归调用的次数加1,这个1表不开始进行的一次非递归调用)。算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(log

2

n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。
    对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,当=i自求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。

 
空间复杂度是程序运行所以需要的额外消耗存储空间,也用o()来表示

  比如插入排序的时间复杂度是o(n

2

),空间复杂度是o(1)
  而一般的递归算法就要有o(n)的空间复杂度了,因为每次递归都要存储返回信息
  一个算法的优劣主要从算法的执行时间和所需要占用的存储空间两个方面衡量,算法执行时间的度量不是采用算法执行的绝对时间来计算的,因为一个算法在不同的机器上执行所花的时间不一样,在不同时刻也会由于计算机资源占用情况的不同,使得算法在同一台计算机上执行的时间也不一样,所以对于算法的时间复杂性,采用算法执行过程中其基本操作的执行次数,称为计算量来度量。
  算法中基本操作的执行次数一般是与问题规模有关的,对于结点个数为n的数据处理问题,用T(n)表示算法基本操作的执行次数.在评价算法的时间复杂性时,不考虑两算法执行次数之间的细小区别,而只关心算法的本质差别:
  为此,引入一个所谓的O() 记号,则T1(n)=2n=O(n),T2(n)=n+1=O(n)。一个函数f(n)是O(g(n))的,则一定存在正常数c和m,使对所有的n>m,都满足f(n)<c*g(n)。

复制代码

好文要顶 关注我 收藏该文  

尹福建
关注 - 0
粉丝 - 0

+加关注

0

0

« 上一篇:idea 快捷键大全
» 下一篇:mysql中的自增列和默认字段值为系统时间

posted @ 2017-07-06 15:38 尹福建 阅读(2350) 评论(0) 编辑 收藏

刷新评论刷新页面返回顶部

注册用户登录后才能发表评论,请 登录 或 注册访问网站首页。

【推荐】超50万VC++源码: 大型组态工控、电力仿真CAD与GIS源码库!
【福利】华为云4核8G云主机免费试用
【活动】华为云会员节云服务特惠1折起
【活动】腾讯云+社区开发者大会12月15日首都北京盛大起航!

腾讯云1129

相关博文:
· 2018年9月全国计算机公共基础题库
· 算法的性能评价
· 算法(1)
· 时间复杂度
· 时间复杂度和空间复杂度

https://www.cnblogs.com/yinfj/p/7126643.html

猜你喜欢

转载自blog.csdn.net/xuheng8600/article/details/84871981