0


“入门”数据结构—时间复杂度

( •̀ ω •́ )✧今天我们来学习一下数据结构最初始部分-时间复杂度。

1:为什么要引入时间复杂度概念?

🎈:算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。

🎈:时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。

🎈:时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度

看了上面的概念性东西后想必大家对于时间复杂度还不是非常的了解,接下来我们将用例子来帮助大家理解。

算法的时间复杂度是一个函数,这个函数并非是我们C语言的函数,而是关于算法中基本操作执行次数的函数,通俗的说就是一个带未知数的数学函数式。

下面我们来举第一个例子:

请计算一下Func1中++count语句总共执行了多少次?

void Func1(int N)
{
 int count = 0;
 for (int i = 0; i < N ; ++ i)
 {
  for (int j = 0; j < N ; ++ j)
  {
    ++count;
  }
 }
 
for (int k = 0; k < 2 * N ; ++ k)
{
     ++count;
}
int M = 10;
while (M--)
{
 ++count;
}

我们可以发现count语句执行的次数与N有关,关于执行次数的函数表达式我们可以从上面代码推断出:F(n)=NN+2N+10;而这里的F(n)就是我们所说的时间复杂度,它描述的是算法中的基本操作的执行次数。

2:大O的渐进表示法

Func1 执行的基本操作次数 :F(N)=NN+2N+10

N = 10 F(N) = 130

N = 100 F(N) = 10210

N = 1000 F(N) = 1002010

从上面的表达式中我们可以看出:随着N的变大,后两项对整个结果的影响变小当N无限大的时候,后两项对结果的影响可以忽略不计。实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

大O符号(Big O notation):是用于描述函数渐进行为的数学符号。

因为当N趋于无穷时NN对函数结果起主导作用,所以使用大O的渐进表示法以后,Func1的时间复杂度为:*O(n²)

推导大O阶方法:

1、用常数1取代运行时间中的所有加法常数。

2、在修改后的运行次数函数中,只保留最高阶项。

3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

⭐ :1、用常数1取代运行时间中的所有加法常数。

    计算Func4的时间复杂度:
void Func4(int N)
{
 int count = 0;
 for (int k = 0; k < 100; ++ k)
 {
 ++count;
 }
 printf("%d\n", count);
}

根据以上代码我们可以推断出算法次数为100,无论算法的执行次数是100,1000,或者10000,只要是常数,我们表示时间复杂度时统一用1替代常数:Func4的时间复杂度为:O(1)

⭐ :2、在修改后的运行次数函数中,只保留最高阶项。

 参考:Func1

⭐ :3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

  计算Func2的时间复杂度:
void Func2(int N)
{
 int count = 0;
 for (int k = 0; k < 2 * N ; ++ k)
 {
 ++count;
 }
 int M = 10;
 while (M--)
 {
 ++count;
 }
 printf("%d\n", count);
}

根据以上代码我们能推断出算法执行次数:F(n)=2*N+10 当n趋近无穷时10可以忽略不计,且2*n与n相差不大,所以Func2的时间复杂度为:O(n)

3:典例

1: 计算Func3的时间复杂度:

void Func3(int N, int M)
{
 int count = 0;
 for (int k = 0; k < M; ++ k)
 {
 ++count;
 }
 for (int k = 0; k < N ; ++ k)
 {
 ++count;
 }
 printf("%d\n", count);
}

m和n都为变量,我们无法判断出m和n谁更大,所以Func2的时间复杂度为:O(m+n)

如果给出条件m>>n,则Func2的时间复杂度为:**O(m) 如果给出条件m和n差不多大,则根据推导大O阶方法3求出Func2的时间复杂度为:O(m) **或者 O(n)

通过上面的例子我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。

另外有些算法的时间复杂度存在最好、平均和最坏情况:

最坏情况:任意输入规模的最大运行次数(上界)

平均情况:任意输入规模的期望运行次数

最好情况:任意输入规模的最小运行次数(下界)

在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)

2:下面我们计算一下strchr函数的时间复杂度:

char * strchr ( const char * str, int character );

首先我先来介绍一下strchr函数的实现原理:

char * strchr ( const char * str, int character )
{

 while(*str)
 {
  if(*str==character)
    return str;
  else
    str++;
 }
 return NULL;

通过对上面代码分析我们知道:

如果s字符串包含c字符,该函数返回指向s字符串首次出现的c字符的指针(末尾的空字符也是字符串的一部分,所以在查找范围内)。
如果在字符串s中未找到c字符,该函数则返回空指针。

我们假设字符串的长度为n,而n是一个未知数,算法的最坏运行情况是遍历整个字符数组,所以strchr函数的时间复杂度为:O(n)

3:计算冒泡排序BubbleSort的时间复杂度:

void BubbleSort(int* a, int n)
{
 assert(a);
 for (size_t end = n; end > 0; --end)
 {
     int exchange = 0;
   for (size_t i = 1; i < end; ++i)
   {
    if (a[i-1] > a[i])
     {
       Swap(&a[i-1], &a[i]);
       exchange = 1;
     }
   }
 if (exchange == 0)
  break;
 }
}

我们知道冒泡排序的实现原理:第一次的比较次数为n-1,第二次为n-2,第 三次为n-3......直到比较次数为1。我们可以发现比较次数为等差数列,所以比较的总次数为F(N)=(N-1)*N/2,这里的F(N)属于最坏的情况,如果我们数组本身就是有序的,那我们只需要比较N-1次。即:F(N)=N-1.

冒泡排序最坏情况下时间复杂度为:**O(n²) 最好情况下时间复杂度为:O(n) 因为在实际中一般情况关注的是算法的最坏运行情况,所以冒泡排序的时间复杂度为:O(n²) **

4:计算二分查找BinarySearch的时间复杂度:

int BinarySearch(int* a, int n, int x)
{
  assert(a);
  int begin = 0;
  int end = n;
  while (begin < end)
   {
    int mid = begin + ((end-begin)>>1);
    if (a[mid] < x)
      begin = mid+1;
    else if (a[mid] > x)
      end = mid;
    else
      return mid;
   }
 return -1;
}

在最好的情况下,我们查找一次刚好查找到,此时时间复杂度:O(1)

下面我们来分析一下最坏的情况:查找不到。

假设我们所需要查找的数组的长度为n,我们找了x次,那么根据二分查找的原理,则2^x=n

即:

5:计算阶乘递归Fac的时间复杂度:

long long Fac(size_t N)
{
 if(0 == N)
 return 1;
 
 return Fac(N-1)*N;
}

从上图中递归的依次调用我们可以推断出Fac的时间复杂度为:O(n)

那我们来变换一下,如果在Fac函数里面加一个循环,结果又是多少呢?

long long Fac(size_t N)
{
 int i=0;
 if(0 == N)
 return 1;
 for(i=0;i<n;i++)
  {
   printf("%d\n",i);
  }
  return Fac(N-1)*N;
}

函数每次调用,执行次数减1,所以总次数为:F(N)=n+(n-1)+(n-2)+......1,为等差数列的和,所以函数的时间复杂度为:O(n²)

🍎:递归算法时间复杂度计算:

1:每次函数调用是O(1),那么就看他的递归次数。

2:每次函数调用不是O(1),那么就看他的递归调用中次数的累加。

下面我们再来看一个递归函数:

计算斐波那契递归Fib的时间复杂度:

long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}

每一次函数调用执行次数都是二次,随着递归的展开,执行次数也在不断的增加:

我们可以用下图来表示:

则其执行次数:F(n)=1+2+4+8+....+2^(n-2), 我们可以看出F(n)是一个等比数列的前n项和,所以我们通过计算可以大致推断出Fib的时间复杂度为:O(2^n)

4:结语:

今天的时间复杂度就介绍到这里,下一节我们介绍一下空间复杂度。现在是中午12点了,写这个东西写了半天,肚子都咕咕叫了~~,如果大家觉得有用的话记得点个赞噢🎈,有什么不懂的欢迎私信我😊 我要去干饭了! !!

标签: 数据结构

本文转载自: https://blog.csdn.net/weixin_60718941/article/details/123299359
版权归原作者 Fan~Fan 所有, 如有侵权,请联系我们删除。

“&ldquo;入门&rdquo;数据结构&mdash;时间复杂度”的评论:

还没有评论