数据结构(C语言)超详细知识点笔记5---算法效率的度量(计算时间复杂度和空间复杂度)

四 算法效率的度量

在这里插入图片描述

前言

这个章节是数据结构正式学习的敲门砖,其意义不仅在于帮助我们理解 “如何评价算法好坏”,更在于指导我们在实际问题中选择合适的数据结构和设计高效的算法.我会尽可能仔细地向大家介绍该章节内容,这个文章非常的长,希望大家能够有耐心看下去,如果能够静心研究的话,一定会对你有很大的帮助的,祝大家都能够学会😘

1.为什么要学习复杂度

(1)选择最优算法

  • 在解决问题时,通常会有多种算法可供选择

    例如在对一个数组进行排序时,有冒泡排序、插入排序、快速排序等多种算法

    不同的算法在处理相同数据量时所消耗的时间和空间资源是不同的

  • 通过学习复杂度,我们可以分析每种算法在不同数据规模下的性能表现,从而选择出最适合特定问题和数据规模的算法

  • 例如,在处理大规模数据时,快速排序通常比冒泡排序更高效,因为它的平均时间复杂度更低

(2)预测算法性能

复杂度分析可以帮助我们预测算法在不同数据规模下的运行时间和空间占用情况,我们可以提前对算法进行优化或者选择更合适的算法,以确保系统在处理大量数据时仍能保持较好的性能

(3)优化算法设计

复杂度分析是一个重要的指导工具。

通过对算法复杂度的分析,我们可以发现算法中的瓶颈所在,进而对算法进行改进和优化

举例说明

(1)查找算法的实现
(a)顺序查找
// 顺序查找函数,返回目标元素的索引,未找到返回-1
int linearSearch(int arr[], int n, int target) {
    for (int i = 0; i < n; i++) {
        if (arr[i] == target) {
            return i;  // 找到目标,返回索引
        }
    }
    return -1;  // 未找到目标
}
(b)二分查找
int binarySearch(int arr[], int left, int right, int target) {
    while (left <= right) {
        int mid = left + (right - left) / 2;  // 防止溢出,等价于(left + right) / 2
        
        if (arr[mid] == target) {
            return mid;  // 找到目标,返回索引
        } else if (arr[mid] < target) {
            left = mid + 1;  // 目标在右半部分
        } else {
            right = mid - 1;  // 目标在左半部分
        }
    }
    return -1;  // 未找到目标
}

例如,当数据量为 100 万时:

  • 顺序查找在最坏情况下需要比较 100 万次

  • 二分查找在最坏情况下只需要约 20 次比较

    可见,顺序查找的效率远远不如二分查找,那么我们如何判断一个算法的效率和度量标准呢?这就讲到我接下来要介绍的时间复杂度和空间复杂度了😃

2.算法效率分析方法

在引入时间复杂度和空间复杂度之前,我先来讲述一下如何衡量一个算法的优劣

不要嫌我废话,我尽可能将看我文章的所有人都能看会,尽可能讲得详细一点,有些文章直接就开始将如何计算复杂度什么的一堆例子,但是对于概念的理解,也是很重要的,我觉得你们应该也不想看一个全是代码例子的文章吧,我希望能够讲得生动有趣易懂😽

算法的效率分析主要分为两个方面:

(1)时间效率

时间复杂度用于衡量算法的运行次数,可以理解为 “做事的步骤的多少

比如:找一本书里的某个字:

  • 顺序找(像翻书逐页找):最多翻完一整本书,步骤 = 书的页数,,效率明显非常地低
  • 索引找(先看目录,再翻对应页):步骤 = 目录层级数,这个方法明显快于顺序找

这就是时间复杂度的差距 ——步骤越少,跑得越快

在这里插入图片描述

(2)空间效率

空间复杂度用于衡量算法所需要的额外空间,可以理解为 “做事需要的临时空间

  • 比如:搬家打包:
    • 用大箱子装所有东西(像数组,空间集中,复杂度 低)
    • 用很多小袋子分装(像链表,每个节点存指针,额外空间多,复杂度更高)

在这里插入图片描述

随着时代的发展,到现在的电脑和手机内存已经很大了,更关注 “时间效率”,“空间不够用” 的场景越来越少,但 “时间不够用” 的场景却越来越多

比如刷短视频时,APP 需要瞬间加载 thousands 条数据,如果算法时间复杂度高(比如 O(n²)),手机会卡顿甚至崩溃

而如果只是多占一点内存(比如从 100M 涨到 200M),用户可能根本感知不到

3.时间复杂度

(1)时间复杂度是什么

时间复杂度为算法中的基本操作的执行次数随输入规模n的增长趋势

不计算算法的==实际运行时间和具体执行次数==,因为实际运行时间会受电脑性能、编程语言影响(比如超级计算机跑代码比旧手机快)

执行次数是算法本身的属性(比如循环执行 100 次,不管在哪台电脑上都是 100 次)

至于为什么不计算总执行次数,而是趋势在下面计算会讲述

(2)如何计算时间复杂度?

有的情况下,算法中基本操作重复执行的次数还随着问题的输入数据集不同而不同

计算时间复杂度一般考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。

  • 最坏情况时间复杂度:指在最坏情况下,算法的时间复杂度最低
  • 平均时间复杂度:指在所有可能输入实例在等概率出现的情况下,算法的期望运行时间
  • 最好情况时间复杂度:指在最好的情况下,算法的时间复杂度

计算时间复杂度其实非常简单记住以下几个步骤即可,具体为什么要这么做这么写,我会一一解释不要着急哦😸

(a)计算总执行次数
(b)找最高阶项,忽略低阶项

(若有常数将常数视为 ‘‘常数 * n0’’ 的零阶项)

©去掉最高阶项的系数,外面加个O()

来个例子说明一下吧:

int a = 32;
int b = 25;
int n;

scanf("%d",&n);//输入变量n的值

//执行a次
while(a > 0){
    printf("HaHa"!);
    a--;
}
    
//执行b*n次
for(int i = 0;i < n;i++){
	printf("HelloWorld ! ");
    for(int j = 0;j < b;j++){
        printf("Ni hao !");
    }
}

/*
计算时间复杂度:
1.计算总执行次数f(n) = a + b * n
2.找最高阶项,忽略常数和低阶项b * n
3.去掉最高阶项的系数,外面加个O() : O(n)
得出时间复杂度T(n) = O(n)
*/

解释:

  1. 为什么要找最高阶项,忽略常数和低阶项?

    学过高数极限的知识点的应该可以理解,n是个变量,在你未输入之前,它可以取无限大的值,在n无限增大的趋势下,常数和低阶项可以忽略不计

    • 例子
      算法 C 的时间是 ,算法 D 的时间是 n² + 1000n

      • 当 n=100 时:C 用 10000 次,D 用 10000 + 100000 = 110000 次(D 比 C 慢 10 倍)

      • 当 n=100000 时:C 用 10¹⁰次,D 用 10¹⁰ + 10⁸ = 1.01×10¹⁰次(D 只比 C 慢 1%,低阶项几乎可以忽略)

  2. 为什么要去掉最高阶项的系数,外面加个O()?

    • 计算时间复杂度关注 “趋势”,而非 “精确次数”,时间复杂度的意义是判断数据规模变大时,算法会变慢多少,所以要去掉最高阶项的系数

    • 大 O 符号(O(f(n)))的严格定义是:

      若存在常数 Cn₀,当 n ≥ n₀ 时,算法的执行次数 ≤ C × f(n),则称算法的时间复杂度T(n)为 O(f(n))

(3)经典时间复杂度例题

例 1:常数阶 O (1)
for(int i = 0;i <= 9999;i++){ 
    printf("Hello World!");//循环9999 * n0次
}
int a = 9999;//a是常数
//无论常数a取多大,代码时间复杂度均为O(1)
for(int i = 0;i <= a;i++){
    printf("Hello World!");//循环a(9999) * n0次
}

去掉最高阶项系数后得n0 = 1

时间复杂度为T(n) = O(1)

例 2:线性阶 O (n)
int n;//n是变量
int a = 9999;//a是常量
int b = 9999;//b是常量
scanf("%d",&n);
//无论常数a取多大,代码时间复杂度均为O(n)
for(int i = 0;i <= a;i++){
    printf("Hello World!");//循环a次
}
for(int i = 0;i <= b * n;i++){
    printf("Hello World!");//循环b * n次
}
  1. 计算总执行次数 : a + b * n

  2. 找最高阶项,忽略低阶项 : b * n

  3. 去掉最高阶项的系数 : n

  4. 得时间复杂度为T(n) = O(n)

常见场景:单循环遍历数组、链表等。

例 3:平方阶 O (n²)
int n;//n是变量
int a = 9999;//a是常量
int b = 9999;//b是常量
int c = 9999;//c是常量
scanf("%d",&n);

for(int i = 0;i <= a;i++){
    printf("Hello World!");//循环a次
}

for(int i = 0;i <= b * n;i++){
    printf("Hello World!");//循环b * n次
}

for(int i = 0;i <= c * n;i++){
    for(int j = 0; j <= c * n;j++){
            printf("Hello World!");//循环c * n * n次
    }
}

  1. 计算总执行次数 : a + b * n + c * n2

  2. 找最高阶项,忽略低阶项 : c * n2

  3. 去掉最高阶项的系数 : n2

  4. 得时间复杂度为T(n) = O(n2)

常见场景:嵌套循环(如冒泡排序、插入排序)。

例 4:对数阶 O (logn)
int arr[n];

void binary_search(arr,len, target){
    int left = 0;
    int right = len- 1;
    while (left <= right){
        int mid = (left + right) / 2;
        if (arr[mid] == target){
            return mid;
        }
        elif (arr[mid] < target){
            left = mid + 1;
        }
        else{
            right = mid - 1;
        }
    }
    return -1
}

二分查找每次将查找范围缩小一半( n=8→4→2→1)

  1. 计算总执行次数 : log2n

  2. 得时间复杂度为T(n) = O(log n)

例5: 线性对数阶 O (n log n)
// 模拟n×log n的操作(类似归并排序的外层+内层结构)
void example(int n) {
    for (int i = 0; i < n; i++) {  // 外层循环n次
        int j = 1;
        while (j < n) {  // 内层循环log n次(每次j翻倍)
            printf("i=%d, j=%d\n", i, j);
            j *= 2;  // j从1→2→4→...→n,共log2(n)次
        }
    }
}

int main() {
    example(4);  // n=4时,外层4次,内层2次(log2(4)=2),总8次操作
    return 0;
}

  1. 计算总执行次数 : n * log2n

  2. 得时间复杂度为T(n) = O(n log n)

(5)常见时间复杂度对比

O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(n3) < O(2n) < O(n!)

(6)多变量计算时间复杂度

(a)并列关系相加
for(int i = 0;i <= 4 * n;i++){
    printf("Hello World!");//循环4 * n次
}

for(int i = 0;i <= 7 * m;i++){
    printf("Hello World!");//循环7 * m次
}

时间复杂度为T(n) = O(n + m)

(b)嵌套关系相乘
for(int i = 0;i <= 4 * n;i++){
    for(int j = 0;j <= 7 * m;j++){
    	printf("Hello World!");//循环28 * n * m次
	}
}

时间复杂度为T(n) = O(n * m)

4.空间复杂度

空间复杂度用于衡量一个算法在运行过程中临时额外占用存储空间的大小,简单来说就是计算变量的个数,不是计算程序本身占用了多少字节的存储空间

计算空间复杂度

(1)计算总变量个数
(2)找最高阶项,忽略低阶项
(3)去掉最高阶项的系数,外面加个O()

经典时间复杂度例

(1) 常数阶 O (1)
#include <stdio.h>

// 计算两个整数的和
int add(int a, int b) {
    return a + b;
}

int main() {
    int num1 = 5;
    int num2 = 10;
    int sum = add(num1, num2);
    printf("The sum is: %d\n", sum);
    return 0;
}
  1. 计算总变量个数 : 3 * n0

  2. 去掉最高阶项的系数 : O(n0 = 1)

  3. 得空间复杂度 : S(n) = O(1)

(2)线性阶 O (n)
// 创建一个长度为n的整数数组并返回
int* createArray(int n) {
    int* arr = (int*)malloc(6 * n * sizeof(int));
    for (int i = 0; i < 6 * n; i++) {
        arr[i] = i;
    }
    return arr;
}
  1. 计算总变量个数 : 6 * n

  2. 去掉最高阶项的系数 : O(n)

  3. 得空间复杂度 : S(n) = O(n)

(3) 平方阶 O (n2)
// 创建一个二维数组,大小为n×n
int** create2DArray(int n) {
    int** matrix = (int**)malloc(n * sizeof(int*));
    for (int i = 0; i < n; i++) {
        matrix[i] = (int*)malloc(n * sizeof(int));
        for (int j = 0; j < n; j++) {
            matrix[i][j] = i * j;
        }
    }
    return matrix;
}
  1. 计算总变量个数 : n2

  2. 去掉最高阶项的系数 : O(n2)

  3. 得空间复杂度 : S(n) = O(n2)

我已经尽可能将枯燥的概念性知识转化为有趣的例子和图片,希望你们能够理解,如果喜欢的话.麻烦点一个赞,谢谢😘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值