数据结构——拿捏复杂度

耕耘 :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);
}

结语

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

相关推荐
一条破秋裤1 小时前
Linux 线程同步:条件变量
java·linux·jvm
Chen_harmony1 小时前
六、排序详解
c语言·数据结构·算法·排序算法
步行cgn1 小时前
Spring AOP 切面的先后顺序详解
java·spring
阿钱真强道1 小时前
25 嵌入式操作系统 | 设备面板:用 Qt 控件把“设备“画出来(本节不接串口)
开发语言·qt·flask·架构设计·协议契约
星恒随风1 小时前
Linux开发工具详解(一):软件包管理、Vim、GCC/G++、Makefile与进度条实战
linux·运维·笔记·学习·vim
-dzk-1 小时前
【贪心算法】LC 55.跳跃游戏
算法·游戏·贪心算法
ofoxcoding1 小时前
Jev 模型评测深度解析:核心得分、能力局限与最佳适用任务场景
人工智能·算法·机器学习·ai
小小、码农1 小时前
Ubuntu / Linux 软件安装完整笔记
linux·笔记·ubuntu
ym hyd 1111 小时前
宝宝早教服务系统源码 Java+SpringBoot+Vue3 前后分离
java·开发语言·spring boot