算法效率的钥匙:从大O看复杂度计算

article/2025/8/15 3:24:21

目录

1.数据结构与算法

1.1数据结构介绍

1.2算法介绍 

2.算法效率

2.1复杂度

2.1.1时间复杂度

2.1.1.1时间复杂度计算示例1

 2.1.1.2时间复杂度计算示例2

  2.1.1.3时间复杂度计算示例3

  2.1.1.4时间复杂度计算示例4

   2.1.1.5时间复杂度计算示例5

   2.1.1.6时间复杂度计算示例6

    2.1.1.7时间复杂度计算示例7

2.1.2空间复杂度 

2.1.1.1空间复杂度计算示例1

 2.1.1.2空间复杂度计算示例2

2.2常见复杂度对比

2.3复杂度笔试题 


在之前的博文中,我们基本介绍完了C语言的语法知识,例如分支循环,指针和结构体等知识,今天我们终于要进入到学习数据结构的知识殿堂中,一起加油!!!


1.数据结构与算法

1.1数据结构介绍

数据结构是什么,它对我们有什么用处,我们为什么要学习它?抱着这样的疑问,我们先来介绍数据结构的概念。

数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在⼀种或多种特定关系的数据元素的集合。没有⼀种单⼀的数据结构对所有用途都有用,所以我们要学各式各样的数据结构,
如:线性表、树、图、哈希等

数据结构,数据与结构,其实就是各种数据结合形成了不同的结构,数据本身是离散的、无组织的,而通过不同的结构设计,我们可以将数据以特定方式组织起来,从而实现高效的存储、访问和操作。在C语言中,结构体(struct)和指针(*)是实现复杂数据结构的关键工具。我们接下来学习每一种数据结构基本都要用到这两项工具,所以结构体和指针的知识一定要掌握扎实。


1.2算法介绍 

什么是算法?
用通俗的话说,算法就是解决问题的明确步骤。就像烹饪食谱一样,算法规定了一系列操作,将输入(如食材)通过有限步骤转化为输出(如菜肴)。在编程中,程序就是一道道算法的具体体现。

我们之所以学习数据结构就是为了学习优质的算法解决问题,比如数组,他帮助我们将一类数据连续存储在内存中,方便我们查找,修改,销毁。利用结构的优势设计出高效的设计,减少冗余代码。


2.算法效率

如何衡量一个算法的好坏呢?

我们来看一个题:给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。这个代码的实现并不难,我们只需要循环 k 次将数组所有元素向后移动一位就行了,代码如下:

void rotate1(int* arr, int sz, int k)
{for (int i = 0; i < k; i++)//循环k次{int tmp = arr[sz-1];for (int j = sz - 1; j > 0; j--)//数组元素向后移动一位{arr[j] = arr[j - 1];}arr[0] = tmp;}
}int main()
{int arr[5] = { 1,2,3,4,5 };int sz = sizeof(arr) / sizeof(arr[0]);int k = 0;scanf("%d", &k);rorate(arr, sz, k);for (int i = 0; i < sz; i++){printf("%d ", arr[i]);}return 0;
}

上面这个代码已经可以满足题目的要求,但是有没有感觉这个程序的效率太低了,如果 k 值很大并且数组长度很长,那么这个循环简直不敢想象会进行多少次,有没有办法优化一下,经过观察,我们可以发现,如果 k 值等于数组长度的时候,旋转完后相当于没有旋转,所以我们可以这样改进:

void rotate2(int* arr, int sz, int k)
{int new_arr[5];//将排好的数组先存入新数组中for (int i = 0; i < sz; i++){new_arr[(i + k) % sz] = arr[i];//不管k为多大,(i+k)%sz都不会大于sz}for (int i = 0; i < sz; i++){arr[i] = new_arr[i];}
}

rorate2 和 rorate1 实现效果相同,但在效率上要比 rorate1 强上很多,重复代码运行次数大大减少,那到底好多少呢?有没有一个定义,当然有,接下来我们就要引进复杂度的概念。


2.1复杂度

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

2.1.1时间复杂度

定义:在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运⾏时间。时间复杂度是衡量程序的时间效率,那么 为什么不去计算程序的运行时间呢
1. 因为程序运⾏时间和编译环境和运⾏机器的配置都有关系,⽐如同⼀个算法程序,⽤⼀个⽼编译器进⾏编译和新编译器编译,在同样机器下运⾏时间不同。
2. 同⼀个算法程序,⽤⼀个⽼低配置机器和新⾼配置机器,运⾏时间也不同。
3. 并且时间只能程序写好后测试,不能写程序前通过理论思想计算评估。
那么算法的时间复杂度是⼀个函数式T(N)到底是什么呢?这个T(N)函数式计算了程序的 执行次数 。通过c语⾔编译链接章节学习,我们知道算法程序被编译后⽣成⼆进制指令,程序运⾏,就是cpu执行这些编译好的指令。那么我们通过程序代码或者理论思想计算出程序的执⾏次数的函数式T(N),假设每句指令执⾏时间基本⼀样(实际中有差别,但是微乎其微),那么执行次数和运行时间就是等比正相关,这样也脱离了具体的编译运行环境。执行次数就可以代表程序时间效率的优劣。比如解决⼀个问题的算法a程序T(N) = N,算法b程序T(N) = N^2,那么算法a的效率⼀定优于算法b。
我们来看一个案例:
实际中我们计算时间复杂度时,计算的也不是程序的精确的执行次数,精确执行次数计算起来是很麻烦的(不同的⼀句程序代码,编译出的指令条数都是不⼀样的),计算出精确的执行次数意义也不大,因为我们计算时间复杂度只是想比较算法程序的增长量级,也就是当N不断变⼤时T(N)的差别,上面我们已经看到了 当N不断变大时常数和低阶项对结果的影响很小,所以我们只需要计算程序能代表增长量级的大概执行次数,复杂度的表示通常使用大O的渐进表示法。

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

推导大O阶规则
1. 时间复杂度函数式T(N)中,只保留最高阶项,去掉那些低阶项,因为当N不断变大时,
低阶项对结果影响越来越小,当N无穷大时,就可以忽略不计了。
2. 如果最高阶项存在且不是1,则去除这个项目的常数系数,因为当N不断变大,这个系数
对结果影响越来越小,当N无穷大时,就可以忽略不计了。
3. T(N)中如果没有N相关的项目,只有常数项,⽤常数1取代所有加法常数。

通过以上方法,可以得到 Func1 的时间复杂度为: O(N^2 )

2.1.1.1时间复杂度计算示例1
// 计算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);
}
Func2执⾏的基本操作次数:
T ( N ) = 2 N + 10
根据推导规则第2条和第3条得出
Func2的时间复杂度为: O ( N )
 2.1.1.2时间复杂度计算示例2
// 计算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);
}
Func3执⾏的基本操作次数:
T ( N ) = M + N

在这里M和N都是变量,我们并知道它们的大小,所以并不能轻易删去任何一个

Func2的时间复杂度为: O(M+N)

如果M>>N,那么时间复杂度为O(M)

如果M<<N,那么时间复杂度为O(N)

如果M==N,那么时间复杂度为O(M+N)(并不是完全相等,是对计算机来说指M和N的差值并不大)

  2.1.1.3时间复杂度计算示例3
// 计算Func4的时间复杂度?
void Func4(int N)
{int count = 0;for (int k = 0; k < 100; ++k){++count;}printf("%d\n", count);
}
Func4执⾏的基本操作次数:
T ( N ) = 100
根据推导规则第1条得出
Func2的时间复杂度为: O (1)
注意:无论这里执行次数是一万还是一亿,最后的时间复杂度都是O(1)
  2.1.1.4时间复杂度计算示例4
// 计算strchr的时间复杂度?
const char* strchr(const char* str, int character)
{const char* p_begin = s;while (*p_begin != character){if (*p_begin == '\0')return NULL;p_begin++;}return p_begin;
}
注意:这个代码的时间复杂度为多少取决于他要找的那个字符在字符串的什么位置。
strchr执⾏的基本操作次数:
1)若要查找的字符在字符串第⼀个位置,则: T ( N ) = 1
2)若要查找的字符在字符串最后的⼀个位置, 则: T ( N ) = N
3)若要查找的字符在字符串中间位置,则: T ( N ) = N/2
因此:strchr的时间复杂度分为:
最好情况: O (1)
最坏情况: O ( N )
平均情况: O ( N )
大O的渐进表示法在实际中⼀般情况取的是算法的上界,也就是最坏运行情况。
所以strchr的时间复杂度为O(N)
   2.1.1.5时间复杂度计算示例5
// 计算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;}
}

 BubbleSort执行的基本操作次数:

如果数组是有序数组,只需要进行n-1次比较,T(N)=N

如果数组有序但为降序,则需要进行n-1轮比较,第k轮需要比较n-k次,所以T(N)=(n*(n-1))/2

取平均情况,则进行约n^2/2次比较,T(N)=n^2/2

因此:BubbleSort 的时间复杂度分为:
最好情况: O (N)
最坏情况: O ( N^2 )
平均情况: O ( N^2 )
   2.1.1.6时间复杂度计算示例6
// 计算Func5的时间复杂度?
void func5(int n)
{int cnt = 1;while (cnt < n){cnt *= 2;}
}
当n=2时,执⾏次数为1
当n=4时,执⾏次数为2
当n=16时,执⾏次数为4
假设执⾏次数为 x ,则 2 ^x   = n
因此执⾏次数: x = log2  n
因此:func5的时间复杂度取最差情况为:
O (log 2 n )
注意log2 n 、 log n 、 lg n 的表表示
当n接近无穷大时,底数的大小对结果影响不大。因此,⼀般情况下不管底数是多少都可以省略不 写,即可以表示为 log n 不同书籍的表示方式不同,以上写法差别不大,我们建议使用 log n
    2.1.1.7时间复杂度计算示例7
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{if (0 == N)return 1;return Fac(N - 1) * N;
}
调⽤⼀次Fac函数的时间复杂度为 O (1)
⽽在Fac函数中,存在n次递归调⽤Fac函数
因此:
阶乘递归的时间复杂度为: O ( n )

我们需要掌握一些简单程序的时间复杂度的计算方法,以上示例都比较重要,需要自己能够独立算出。


2.1.2空间复杂度 

空间复杂度也是⼀个数学表达式,是对⼀个算法在运⾏过程中因为算法的需要额外临时开辟的空间。空间复杂度不是计算程序占用了多少bytes的空间,因为常规情况每个对象大小差异不会很⼤,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使⽤大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
2.1.1.1空间复杂度计算示例1
// 计算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;}
}
函数栈帧在编译期间已经确定好了,只需要关注函数在运行时额外申请的空间。
BubbleSort额外申请的空间有exchange等有限个局部变量,使用了常数个额外空间
因此空间复杂度为 O (1)
 2.1.1.2空间复杂度计算示例2
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{if (N == 0)return 1;return Fac(N - 1) * N;
}
Fac递归调⽤了N次,额外开辟了N个函数栈帧,每个栈帧使⽤了常数个空间
因此空间复杂度为: O ( N )

2.2常见复杂度对比

2.3复杂度笔试题 

 这个题在我们介绍复杂度的时候已经解答过,但是我们看该题的进阶,使用复杂度为O(1)的原地算法解题?这是什么意思呢?我们先将rotate1rotate2两个函数拿过来,根据前面所学的知识,计算一下它们的时间复杂度和空间复杂度。

//申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中
void rotate2(int* arr, int sz, int k)
{int new_arr[5];//将排好的数组先存入新数组中for (int i = 0; i < sz; i++){new_arr[(i + k) % sz] = arr[i];//不管k为多大,(i+k)%sz都不会大于sz}for (int i = 0; i < sz; i++){arr[i] = new_arr[i];}
}//循环K次将数组所有元素向后移动⼀位
void rotate1(int* arr, int sz, int k)
{for (int i = 0; i < k; i++)//循环k次{int tmp = arr[sz-1];for (int j = sz - 1; j > 0; j--)//数组元素向后移动一位{arr[j] = arr[j - 1];}arr[0] = tmp;}
}

经过计算,得出: 

rotate1函数rotate2函数
时间复杂度O(N^2)O(N)
空间复杂度O(1)O(N)

我们可以看到 rotate1 函数的空间复杂度为O(1),但是时间复杂度为O(N^2),而 rotate2 函数的时间复杂度仅为为O(N),但是空间复杂度却为O(N),有没有一种算法可以将时间复杂度控制为O(N),空间复杂度又为O(1)呢?

void reverse(int* arr, int begin, int end)
{while (begin < end) {int tmp = arr[begin];arr[begin] = arr[end];arr[end] = tmp;begin++;end--;}
}void rotate3(int* arr, int sz, int k)
{k = k % sz;reverse(arr, 0, sz - k - 1);reverse(arr, sz - k, sz - 1);reverse(arr, 0, sz - 1);
}
算法思路:
假设数组为arr[5] = {1,2,3,4,5},k==2
前sz-k个逆置: 3 2 1  4 5
后k个逆置 :    3 2 1 5 4
整体逆置 :       4 5 1 2 3
rotate3的时间复杂度为O(N),空间复杂度为 O(1)

http://www.hkcw.cn/article/lXrbbNnyEU.shtml

相关文章

A*算法详解【附算法代码与运行结果】

算法背景 A*算法是一种在图形平面上&#xff0c;有多个路径中寻找一条从起始点到目标点的最短遍历路径的算法。它属于启发式搜索算法&#xff08;Heuristic Search Algorithm&#xff09;&#xff0c;因为它使用启发式方法来计算图中的节点&#xff0c;从而减少实际计算的节点…

【leetcode】逐层探索:BFS求解最短路的原理与实践

前言 &#x1f31f;&#x1f31f;本期讲解关于力扣的几篇题解的详细介绍~~~ &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 &#x1f525; 你的点赞就是小编不断更新的最大动力 &#x1f386;那么废话不…

七大排序算法深度解析:从原理到代码实现

1.排序 排序算法是计算机科学中最基础的技能之一&#xff0c;无论你是编程新手还是经验丰富的开发者&#xff0c;理解这些算法都能显著提升代码效率。本文将用最简单的方式&#xff0c;带你快速掌握七大经典排序算法的核心原理与实现。 1.1排序概念及其运用 排序是指将一组数据…

Python的情感词典情感分析和情绪计算

一.大连理工中文情感词典 情感分析 (Sentiment Analysis)和情绪分类 (Emotion Classification&#xff09;都是非常重要的文本挖掘手段。情感分析的基本流程如下图所示&#xff0c;通常包括&#xff1a; 自定义爬虫抓取文本信息&#xff1b;使用Jieba工具进行中文分词、词性标…

C++之vector类(超详细)

这节我们来学习一下&#xff0c;C中一个重要的工具——STL&#xff0c;这是C中自带的一个标准库&#xff0c;我们可以直接调用这个库中的函数或者容器&#xff0c;可以使效率大大提升。这节我们介绍STL中的vector。 文章目录 前言 一、标准库类型vector 二、vector的使用 2.…

C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

&#x1f4da; 本文主要总结了一些常见的C面试题&#xff0c;主要涉及到语法基础、STL标准库、内存相关、类相关和其他辅助技能&#xff0c;掌握这些内容&#xff0c;基本上就满足C的岗位技能&#xff08;红色标记为重点内容&#xff09;&#xff0c;欢迎大家前来学习指正&…

『C++成长记』string模拟实现

🔥博客主页:小王又困了 📚系列专栏:C++ 🌟人之为学,不日近则日退 ❤️感谢大家点赞👍收藏⭐评论✍️ ​ 目录 一、存储结构 二、默认成员函数 📒2.1构造函数 📒2.2析构函数 📒2.3拷贝构造 📒2.4赋值重载 三、容量操作 📒3.1获取有效字符长度…

多态的使用和原理(c++详解)

一、多态的概念 多态顾名思义就是多种形态&#xff0c;它分为编译时的多态&#xff08;静态多态&#xff09;和运行时的多态&#xff08;动态多态&#xff09;&#xff0c;编译时多态&#xff08;静态多态&#xff09;就是函数重载&#xff0c;模板等&#xff0c;通过不同的参数…

C++ 底层实现细节隐藏全攻略:从简单到复杂的五种模式

目录标题 1 引言&#xff1a;为什么要“隐藏实现”1.1 头文件暴露带来的三大痛点1.2 ABI 稳定 vs API 兼容&#xff1a;先分清概念1.3 选型三问法——评估你到底要不要隐藏 2 模式一&#xff1a;直接按值成员 —— “裸奔”也能跑2.1 典型写法与最小示例2.2 何时按值最合适&…

使用国内镜像网站在线下载安装Qt(解决官网慢的问题)——Qt

国内镜像网站 中国科学技术大学&#xff1a;http://mirrors.ustc.edu.cn/qtproject/清华大学&#xff1a;https://mirrors.tuna.tsinghua.edu.cn/qt/北京理工大学&#xff1a;http://mirror.bit.edu.cn/qtproject/ 南京大学&#xff1a;https://mirror.nju.edu.cn/qt腾讯镜像&…

超全超详细!JDK 安装及环境配置(Java SE 8 保姆级教程)

一、JDK 简介 JDK&#xff08;Java Development Kit&#xff09;是用于开发 Java 程序的工具包&#xff0c;包括编译器 javac、Java 运行环境&#xff08;JRE&#xff09;以及各种开发工具。安装和配置 JDK 是学习和使用 Java 编程的第一步&#xff0c;以下是 Java 和 JDK 的具…

Java 大视界 -- 基于 Java 的大数据分布式数据库在社交网络数据存储与查询中的架构设计与性能优化(225)

&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

C++协程从入门到精通

文章目录 一、C协程入门知识&#xff08;一&#xff09;基本概念&#xff08;二&#xff09;特点&#xff08;三&#xff09;应用场景 二、C协程精通知识&#xff08;一&#xff09;高级特性&#xff08;二&#xff09;优化技巧&#xff08;三&#xff09;错误处理机制&#xf…

蓝桥杯第十六届c组c++题目及个人理解

本篇文章只是部分题目的理解&#xff0c;代码和思路仅供参考&#xff0c;切勿当成正确答案&#xff0c;欢迎各位小伙伴在评论区与博主交流&#xff01; 目录 题目&#xff1a;2025 题目解析 核心提取 代码展示 题目&#xff1a;数位倍数 题目解析 核心提取 代码展示 …

C++日新月异的未来代码:C++11(上)

文章目录 1.统一的列表初始化1.1 普通{ }初始化1.2 initializer_list 2.声明2.1 auto、nullptr2.2 decltype 3.左值右值3.1 概念3.2 左值引用与右值引用比较3.3 左值引用与右值引用的应用3.4 完美转发 希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力&#xf…

C++从入门到实战(十二)详细讲解C++如何实现内存管理

C从入门到实战&#xff08;十二&#xff09;详细讲解C如何实现内存管理 前言一、C内存管理方式1. new/delete操作内置类型2. 异常与内存管理的联系&#xff08;简单了解&#xff09;3. new和delete操作自定义类型 二、 operator new与operator delete函数&#xff08;重点&…

【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)

文章目录 【2025年最新版】Java JDK安装、环境配置教程 &#xff08;图文非常详细&#xff09;1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序&#xff1a;6.2 编译和运行程序&#xff1a;6.3 在显示或更改文件的…

【Linux系统】从 C 语言文件操作到系统调用的核心原理

文章目录 前言lesson 15_基础IO一、共识原理二、回顾C语言接口2.1 文件的打开操作2.2 文件的读取与写入操作2.3 三个标准输入输出流 三、过渡到系统&#xff0c;认识文件系统调用3.1 open 系统调用1. 比特位标志位示例 3.2 write 系统调用1. 模拟实现 w 选项2. 模拟实现 a 选项…

JavaSwing之--JTextField

JavaSwing之–JTextField JTextField 是一个允许编辑单行文本的轻量级组件&#xff0c;它提供了一系列的构造方法和常用方法用来编写可以存储文本的文本框满足程序功能的需求。 以下在简要介绍常用构造方法、普通方法后详解各种方法的应用及举例。 一、构造方法 方法名称功…

Windows系统之VHD安装

环境准备 工具说明Dism部署系统、提取和转换系统镜像等等&#xff0c;还有很多功能大家可以自行探索。这里只用到Dism的部署系统功能。 Releases Chuyu-Team/Dism-Multi-language GitHubbcdedit.exe自带工具 C:\Windows\System32\bcdedit.exe 创建虚拟磁盘 首先右键点击我…