数据结构——拿捏复杂度

耕耘 :C、C++、嵌入式技术领域

🔥我的个人主页

❄️个人专栏:《C语言专栏》 《嵌入式专栏》 《数据结构专栏》

✨**不要等待机会,而要创造机会!**✨

📽博主简介:

✨✨一位热爱生活的阳光大男孩.✨✨

前言

本文系统讲解C语言文件操作核心知识,涵盖文件概念、流与文件指针机制,重点解析fopen、fclose及错误处理,结合fseek、ftell实现随机读写,并深入剖析缓冲区原理与fflush刷新机制,强调打开后判空、关闭置空等最佳实践,为高效、安全的文件操作提供完整技术框架。

文章目录

  • 前言
  • 1.数据结构前⾔
    • [1.1 数据结构](#1.1 数据结构)
    • [1.2 算法](#1.2 算法)
  • [2. 算法效率](#2. 算法效率)
    • [2.1 复杂度的概念](#2.1 复杂度的概念)
  • [3. 时间复杂度](#3. 时间复杂度)
    • [3.1 ⼤O的渐进表⽰法](#3.1 ⼤O的渐进表⽰法)
    • [3.2 时间复杂度计算⽰例](#3.2 时间复杂度计算⽰例)
      • [3.2.1 ⽰例1](#3.2.1 ⽰例1)
      • [3.2.2 ⽰例2](#3.2.2 ⽰例2)
      • [3.2.3 ⽰例3](#3.2.3 ⽰例3)
      • [3.2.4 ⽰例4](#3.2.4 ⽰例4)
      • [3.2.5 ⽰例5](#3.2.5 ⽰例5)
      • [3.2.6 ⽰例6](#3.2.6 ⽰例6)
      • [3.2.7 ⽰例7](#3.2.7 ⽰例7)
  • [4. 空间复杂度](#4. 空间复杂度)
    • [4.1 空间复杂度计算⽰例](#4.1 空间复杂度计算⽰例)
      • [4.1.1 ⽰例1](#4.1.1 ⽰例1)
      • [4.1.2 ⽰例2](#4.1.2 ⽰例2)
  • [5. 常⻅复杂度对⽐](#5. 常⻅复杂度对⽐)
  • [6. 复杂度算法题](#6. 复杂度算法题)
    • [6.1 旋转数组](#6.1 旋转数组)
  • 结语

1.数据结构前⾔

1.1 数据结构

数据结构是计算机存储、组织数据的⽅式,指相互之间存在⼀种或多种特定关系的数据元素的集合。没有⼀种单⼀的数据结构对所有⽤途都有⽤,所以我们要学各式各样的数据结构,

如:线性表、树、图、哈希等

1.2 算法

算法就是定义良好的计算过程,他取⼀个或⼀组的值为输⼊,并产⽣出⼀个或⼀组值作为输出。简单来说算法就是⼀系列的计算步骤,⽤来将输⼊数据转化成输出结果。

2. 算法效率

思路:轮转数组,循环K次将数组所有元素向后移动⼀位

1、用循环的嵌套

c 复制代码
void rotate(int* nums, int numsSize, int k) {
	while (k--)
	{
		//保存数组最后一个数据
		int end = nums[numsSize - 1];
		for (int i = numsSize - 1; i > 0; i--)
		{
			nums[i] = nums[i - 1];
		}
		nums[0] = end;
	}
}
int main()
{
	int arr[7] = { 1,2,3,4,5,6,7 };
	for (int i = 0; i < 7; i++)//轮转前
	{
		printf("%d ", arr[i]);
	}
	printf("\n");
	rotate(arr, 7, 3);//轮转3次
	for (int i = 0; i < 7; i++)//轮转后
	{
		printf("%d ", arr[i]);
	}
	return 0;
}

2.1 复杂度的概念

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

**时间复杂度主要衡量⼀个算法的运⾏快慢,⽽空间复杂度主要衡量⼀个算法运⾏所需要的额外空间。**在计算机发展的早期,计算机的存储容量很⼩。所以对空间复杂度很是在乎。但是经过计算机⾏业的迅速发展,计算机的存储容量已经达到了很⾼的程度。所以我们如今已经不需要再特别关注⼀个算法的空间复杂度。

3. 时间复杂度

定义:在计算机科学中,算法的时间复杂度是⼀个函数式T(N),它定量描述了该算法的运⾏时间。时间复杂度是衡量程序的时间效率。这个T(N)函数式计算了程序的执⾏次数。

程序执行时间 = 二进制指令运行时间 * 执行次数

执⾏次数就可以代表程序时间效率的优劣。⽐如解决⼀个问题的算法a程序T(N) = N,算法b程序T(N) = N^2,那么算法a的效率⼀定优于算法b。

注意:时间复杂度描述的是一个程序运动的趋势,而不是他整体运行的时间。

案例:

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;
	}
}

总执行了T (N) = N2+ 2 ∗ N + 10

通过对N取值分析,对结果影响最⼤的⼀项是 N2

实际中我们计算时间复杂度时,计算的也不是程序的精确的执⾏次数,上⾯我

们已经看到了当N不断变⼤时常数和低阶项对结果的影响很⼩,所以我们只需要计算程序能代表增⻓量级的⼤概执⾏次数,复杂度的表⽰通常使⽤⼤O的渐进表⽰法。

3.1 ⼤O的渐进表⽰法

⼤O符号(Big O notation):是⽤于描述函数渐进⾏为的数学符号

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

3.2 时间复杂度计算⽰例

3.2.1 ⽰例1

c 复制代码
// 计算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) = 2N + 10

根据推导规则第3条得出Func2的时间复杂度为: O(N) **

3.2.2 ⽰例2

c 复制代码
// 计算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都不知道次数,所以他们可能相等,则可能T(N)=2N或2M)
因此:Func2的时间复杂度为: O(N) 或 O(M)

3.2.3 ⽰例3

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

T (N) = 100
根据推导规则第1条得出Func2的时间复杂度为: O(1)

3.2.4 ⽰例4

c 复制代码
// 计算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的渐进表⽰法在实际中⼀般情况关注的是算法的上界,也就是最坏运⾏情况。

3.2.5 ⽰例5

c 复制代码
// 计算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;
	}
}

(1)若数组有序,则:T (N) = N
(2)若数组有序且为降序,则:T (N) =2N ∗ (N + 1)
(3)若要查找的字符在字符串中间位置,则:
因此:BubbleSort的时间复杂度取最差情况为: O(N2 )

3.2.6 ⽰例6

c 复制代码
void func5(int n)
{
	int cnt = 1;
	while (cnt < n)
	{
		cnt *= 2;
	}
}

当n=2时,执⾏次数为1
当n=4时,执⾏次数为2
当n=16时,执⾏次数为4
假设执⾏次数为 x ,则 2x= n
因此执⾏次数: x = log n
因此:func5的时间复杂度取最差情况为:O(log2 n)

注意课件中和书籍中 log2 n 、 log n 、 lg n 的表⽰

当n接近⽆穷⼤时,底数的⼤⼩对结果影响不⼤。因此,⼀般情况下不管底数是多少都可以省略不写,即可以表⽰为 log n

不同书籍的表⽰⽅式不同,以上写法差别不⼤,我们建议使⽤ log n

3.2.7 ⽰例7

c 复制代码
// 计算阶乘递归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)

4. 空间复杂度

空间复杂度也是⼀个数学表达式,是对⼀个算法在运⾏过程中因为算法的需要额外临时开辟的空间。空间复杂度不是程序占⽤了多少bytes的空间,因为常规情况每个对象⼤⼩差异不会很⼤,所以空间复杂度算的是变量的个数。

空间复杂度计算规则基本跟实践复杂度类似,也使⽤⼤O渐进表⽰法。

注意:函数运⾏时所需要的栈空间(存储参数、局部变量、⼀些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运⾏时候显式申请的额外空间来确定

4.1 空间复杂度计算⽰例

4.1.1 ⽰例1

c 复制代码
// 计算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)

4.1.2 ⽰例2

c 复制代码
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
    if (N == 0)
        return 1;
    return Fac(N - 1) * N;
}

Fac递归调⽤了N次,额外开辟了N个函数栈帧,每个栈帧使⽤了常数个空间
因此空间复杂度为: O(N)

5. 常⻅复杂度对⽐

6. 复杂度算法题

6.1 旋转数组

思路1
时间复杂度 O(n2 )
循环K次将数组所有元素向后移动⼀位

c 复制代码
void rotate(int* nums, int numsSize, int k)
{
	while (k--)
	{
		int end = nums[numsSize - 1];
		for (int i = numsSize - 1; i > 0; i--)
			{
			nums[i] = nums[i - 1];
			}
		 nums[0] = end;
	}
}

思路2:
空间复杂度 O(n)
申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中

c 复制代码
void rotate(int* nums, int numsSize, int k)
{
	int newArr[numsSize];
	for (int i = 0; i < numsSize; ++i)
	{
		newArr[(i + k) % numsSize] = nums[i];
	}
	for (int i = 0; i < numsSize; ++i)
	{
		nums[i] = newArr[i];
	}
}

思路3:
空间复杂度 O(1)
• 前n-k个逆置:4 3 2 15 6 7
• 后k个逆置 :4 3 2 17 6 5
• 整体逆置 :5 6 7 1 2 3 4

c 复制代码
void reverse(int* nums, int begin, int end)
{
	while (begin < end) {
		int tmp = nums[begin];
		nums[begin] = nums[end];
		nums[end] = tmp;
		begin++;
		end--;
	}
}
void rotate(int* nums, int numsSize, int k)
{
	k = k % numsSize;
	reverse(nums, 0, numsSize - k - 1);
	reverse(nums, numsSize - k, numsSize - 1);
	reverse(nums, 0, numsSize - 1);
}

结语

愿你收获满满,点赞、收藏、转发三连不断,好运常伴!
完.

相关推荐
瑞雪兆丰年兮2 分钟前
JavaWeb-day14-SpringBootWeb原理篇
java·开发语言·spring boot·spring·bean·mybatis·javaweb
每月一号准时摆烂8 分钟前
服务器运维笔记(一)——使用shell编写实时查看BMC功率的脚本
运维·服务器·笔记
深圳智根网络程序开发13 分钟前
WordPress固定链接怎么设?改不对谷歌收录直接崩
经验分享
神仙别闹15 分钟前
基于 Java GUI 贪心策略直接搜索算法和极大极小博弈树算法的智能人机博弈五子棋游戏
java·毕业设计·gui·贪心策略·极大极小博弈树
德思特1 小时前
如何提升 5G 移动性切换验证效率?德思特 Amarisoft Callbox 新增 NR SA LTM 测试功能详解
经验分享·无线通信·车载通信
唐青枫1 小时前
C 语言 strcpy() 用法详解与实战 Demo
c语言
Flynn Liang1 小时前
RAG 评估体系:黄金集、检索指标与 LLM-as-Judge 落地
java·langchain·rag·评估·langchain4j·检索指标
2601_967979121 小时前
机器学习必修课:经典算法与Python实战
python·算法·机器学习·实战·课程
SomeOtherTime1 小时前
Rust生命周期介绍(AI回答)
开发语言·后端·rust
程序猿打工仔2 小时前
计算机基础(C语言)考编、专技岗
c语言·开发语言·算法