深入理解C语言算法的时间复杂度和空间复杂度

🦄个人主页:小米里的大麦-CSDN博客

🎏所属专栏:C语言数据结构_小米里的大麦的博客-CSDN博客

🎁代码托管:黄灿灿/数据结构 (gitee.com)

⚙️操作环境:Visual Studio 2022

目录

一、前言

二、算法效率

1. 如何衡量一个算法的好坏

2. 算法的复杂度

三、时间复杂度

1. 时间复杂度的常见类型

2. 时间复杂度的计算

3. 大O的渐进表示法(重要!!!)

3.1 渐进分析的基本概念

3.2 大O表示法

数学定义

举例

大O表示法的特点

3. 其他渐进表示法

4. 渐进表示法的应用

说了那么多,还是上代码吧,只有代码才能最直观的体现概念:

四、总结

共勉


一、前言

在C语言编程中,算法的效率是我们常常关注的一个核心问题。我们通常使用时间复杂度空间复杂度来衡量算法的效率。本文将深入探讨这两个概念,并详细讲解如何通过大O渐进表示法来分析算法的性能。

这一节概念较多,防止枯燥,先上一份凉菜开开胃:(【数据结构2】算法和复杂度_哔哩哔哩_bilibili

二、算法效率

算法效率指的是算法在执行过程中对资源的消耗情况,通常包括两部分:时间效率空间效率

  • 时间效率:即算法执行所需的时间。我们通常希望算法在尽可能短的时间内完成任务。
  • 空间效率:即算法执行过程中占用的内存空间。对于内存有限的系统,算法的空间效率显得尤为重要。

在实际开发中,我们常常需要在时间和空间之间进行权衡。比如,某些算法可以通过增加内存使用来换取更快的执行速度,而另一些算法则可能为了节省内存而牺牲部分执行时间。

1. 如何衡量一个算法的好坏

如何衡量一个算法的好坏呢?比如对于以下斐波那契数列:
long long Fib(int N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}
斐波那契数列的递归实现方式非常简洁,但简洁一定好吗?那该如何衡量其好与坏呢?

2. 算法的复杂度

  • 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
  • 时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。

 一般算法常见的复杂度如下:

常数阶5201314o(1)
线性阶3n+4o(n)
平方阶3n^2+4n+5o(n^2)
对数阶3log (2)n+4o(log n)
n log n阶2n+3n log (2)n+14o(n log n)
立方阶n^3+2n^2+4n+6o(n^3)
指数阶2^n

o(2^n)

三、时间复杂度

时间复杂度是用来描述算法执行时间随输入规模增长而变化的趋势。我们通常用大O记号(O-notation)来表示时间复杂度,这种表示法反映了算法在最坏情况下的运行时间增长趋势。

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

1. 时间复杂度的常见类型

  • O(1) - 常数时间复杂度

    示例:访问数组中的某个元素。无论数组有多大,访问时间都是恒定的。
    int arr[100];
    int x = arr[50]; // O(1)
    
  • O(n) - 线性时间复杂度
    示例:遍历一个长度为n的数组。每个元素都需要访问一次,所以时间复杂度与数组长度成正比。
    for (int i = 0; i < n; i++) {
        printf("%d\n", arr[i]); // O(n)
    }
    
  • O(n^2) - 平方时间复杂度
    示例:两个嵌套循环,每个循环运行n次。这种复杂度通常出现在简单的排序算法中,如冒泡排序。
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            printf("%d\n", arr[i] * arr[j]); // O(n^2)
        }
    }
    
  • O(log n) - 对数时间复杂度
    示例:在一个已排序的数组中进行二分查找。每次查找都会将问题规模减半,因此时间复杂度是对数级的。
    int binarySearch(int arr[], int n, int key) {
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == key) return mid;
            if (arr[mid] < key) left = mid + 1;
            else right = mid - 1;
        }
        return -1; // O(log n)
    }
    
  • O(n log n) - 线性对数时间复杂度
    示例:归并排序。每次递归将数组对半分开,并进行合并排序。
    void mergeSort(int arr[], int l, int r) {
        if (l < r) {
            int m = l + (r - l) / 2;
            mergeSort(arr, l, m);
            mergeSort(arr, m + 1, r);
            merge(arr, l, m, r); // O(n log n)
        }
    }
    

2. 时间复杂度的计算

要计算时间复杂度,我们通常遵循以下步骤:

  1. 分析算法中的基本操作:确定最耗时的操作。
  2. 计算这些操作的执行次数:根据输入规模确定基本操作的执行次数。
  3. 忽略低阶项和常数:只关注最高阶项,因为它决定了算法的增长趋势。
例如,在递归算法中,每次递归调用都会创建新的函数调用栈:
int factorial(int n) {
    if (n == 0) return 1;
    return n * factorial(n - 1); // O(n) 空间复杂度
}
这里,递归深度为n,因此空间复杂度为O(n)。

3. 大O的渐进表示法(重要!!!)

大O渐进表示法是一种用于描述算法复杂度的数学符号,它关注的是算法执行时间或所需空间随输入规模变化的增长趋势。大O表示法忽略常数系数和低阶项,只关注最高阶的项。大O表示法描述了算法的最坏情况,即当输入规模趋向无穷大时,算法的执行时间或空间需求的上界。

3.1 渐进分析的基本概念

渐进分析主要用来讨论算法在输入规模趋向无穷大时的表现。主要的渐进表示法有以下几种:

  • O-notation (大O表示法):表示最坏情况下的上界,描述算法的时间或空间复杂度的上界。
  • Ω-notation (大Ω表示法):表示最好的情况的下界,描述算法的时间或空间复杂度的下界。
  • Θ-notation (大Θ表示法):表示上下界一致的情况,即描述算法的时间或空间复杂度的确界。

在大多数情况下,我们使用大O表示法,因为我们通常更关心最坏情况下的表现。

3.2 大O表示法

大O表示法描述了当输入规模趋向无穷大时,算法的执行时间或空间需求的上界。用数学语言表示,即对于一个函数f(n),如果存在一个常数c和足够大的n0,使得对于所有的n ≥ n0,都有f(n) ≤ c * g(n),那么我们说f(n)O(g(n))的。

数学定义

概念解释:

如果存在正常数 cn0,使得对所有 n ≥ n0,都有

        f(n) ≤ c ⋅ g(n)

那么我们可以表示为:

        f(n) = O(g(n))

这意味着对于大规模输入,f(n)的增长速度不会超过g(n)的增长速度乘以某个常数因子c

示例解释:

如果存在常数 cn0,使得对于所有 n ≥ n0f(n) ≤ c * g(n),那么我们说 f(n)O(g(n)) 的。

  • 示例1:如果一个算法的时间复杂度为 f(n) = 3n + 2,我们可以将其表示为 O(n),因为当 n 很大时,3n 主导了增长趋势,常数项和系数可以忽略。
  • 示例2:对于一个算法 f(n) = n^2 + 10n + 1000,大O表示法为 O(n^2),因为 n^2 是最高阶项,其决定了复杂度的增长趋势。
举例
  1. 常数时间复杂度 O(1)

    • 例子:访问数组中的某个元素,无论数组有多大,访问操作的时间都是恒定的。
    • 表达式:T(n) = O(1)
  2. 线性时间复杂度 O(n)

    • 例子:遍历一个长度为n的数组,每个元素都需要访问一次。
    • 表达式:T(n) = O(n)
  3. 平方时间复杂度 O(n^2)

    • 例子:两个嵌套循环,每个循环运行n次。
    • 表达式:T(n) = O(n^2)
  4. 对数时间复杂度 O(log n)

    • 例子:二分查找,通过每次将问题规模减半来查找目标值。
    • 表达式:T(n) = O(log n)
  5. 线性对数时间复杂度 O(n log n)

    • 例子:归并排序,每次递归将数组对半分开并排序。
    • 表达式:T(n) = O(n log n)
大O表示法的特点
  • 忽略常数项:因为大O表示法关心的是增长趋势,常数项在输入规模极大时影响可以忽略不计。例如,T(n) = 3n + 5T(n) = n都可以表示为O(n),因为当n很大时,3nn的增长趋势相同,常数53并不影响最终的复杂度。
  • 忽略低阶项:低阶项对算法的影响在大规模输入时可以忽略。例如,T(n) = n^2 + 10n + 1000可以简化为O(n^2),因为n^2n非常大时主导了算法的时间复杂度。

3. 其他渐进表示法

除了大O表示法,还有其他的渐进符号,用于描述算法的不同表现情况:

  • Ω-notation (大Ω表示法)

    • 表示算法的下界,描述最好的情况。
    • 数学定义:如果存在正常数 cn0,使得对所有 n ≥ n0f(n) ≥ c * g(n),那么f(n) = Ω(g(n))
    • 例子:一个排序算法的比较次数下界是Ω(n log n)
  • Θ-notation (大Θ表示法)

    • 表示算法的确界,描述平均情况下的复杂度。
    • 数学定义:如果存在正常数 c1c2 以及 n0,使得对所有 n ≥ n0c1 * g(n) ≤ f(n) ≤ c2 * g(n),那么f(n) = Θ(g(n))
    • 例子:在最佳和最坏情况下,插入排序的复杂度都是Θ(n^2)
  • o-notation (小o表示法)

    • 表示严格小于某个增长率的情况,表示算法的上界比 g(n) 函数增长慢。
    • 数学定义:f(n)o(g(n)),当且仅当 f(n)/g(n) → 0n → ∞
  • ω-notation (小ω表示法)

    • 表示严格大于某个增长率的情况,表示算法的下界比 g(n) 函数增长快。
    • 数学定义:f(n)ω(g(n)),当且仅当 f(n)/g(n) → ∞n → ∞

其中较为重要的是:

  • Ω-notation (大Ω表示法):表示算法的下界,即最好的情况。

    • 示例:插入排序的最好情况是Ω(n),当数据已经有序时,只需要遍历一次即可。
  • Θ-notation (大Θ表示法):表示算法的确界,即在平均情况下的复杂度。

    • 示例:在平均和最坏情况下,归并排序的时间复杂度都是Θ(n log n)

4. 渐进表示法的应用

在算法分析中,渐进表示法帮助我们理解和比较不同算法的效率。例如,如果一个算法是O(n)而另一个算法是O(n^2),则对于大规模输入,第一个算法会明显更快。尽管在小规模输入时,两者的执行时间可能差别不大,但大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);
}
基本操作执行了2N+10次,通过推导大O阶方法知道,时间复杂度为 O(N)
// 计算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,时间复杂度为 O(N+M)
// 计算Func4的时间复杂度?
void Func4(int N)
{
 int count = 0;
 for (int k = 0; k < 100; ++ k)
 {
 ++count;
 }
 printf("%d\n", count);
}
3基本操作执行了10次,通过推导大O阶方法,时间复杂度为 O(1)
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
基本操作执行最好1次,最坏N次,时间复杂度一般看最坏,时间复杂度为 O(N)
// 计算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次,最坏执行了(N*(N+1)/2次,通过推导大O阶方法+时间复杂度一般看最
坏,时间复杂度为 O(N^2)
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
 assert(a);
 int begin = 0;
 int end = n-1;
 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;
}
基本操作执行最好1次,最坏O(logN)次,时间复杂度为 O(logN) 
ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。
(建议通过折纸查找的方式来体会logN是怎么计算出来的)
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
 if(0 == N)
 return 1;
 
 return Fac(N-1)*N;
}
通过计算分析发现基本操作递归了N次,时间复杂度为O(N)。
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
 if(N < 3)
 return 1;
 
 return Fib(N-1) + Fib(N-2);
}
通过计算分析发现基本操作递归了2^N次,时间复杂度为O(2^N)。

四、总结

理解和分析C语言算法的时间复杂度和空间复杂度是编程中一项重要的技能。通过使用大O渐进表示法,我们可以清晰地描述算法的性能,并在不同算法之间进行比较。这不仅帮助我们优化代码,还能为我们选择最合适的算法提供依据。无论是在时间还是空间的效率上,选择一个合适的算法往往是编程中的一个重要步骤。

在实际应用中,掌握这些理论知识,可以让我们在面对复杂问题时更加得心应手,设计出高效的解决方案。希望本文能帮助你深入理解C语言算法的复杂度分析,为你在编程之路上提供帮助!

共勉

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小米里的大麦

您的支持是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值