文章目录
- 🎯引言
- 👓算法复杂度
-
- 1.数据结构与算法
- 2.算法效率
- 3.时间复杂度
-
- 3.1大O的渐进表示法(适用于时间复杂度和空间复杂度)
-
- [3.1.1 选择最高阶:](#3.1.1 选择最高阶:)
- [3.1.2 常数系数:](#3.1.2 常数系数:)
- [3.1.3 常数项化一](#3.1.3 常数项化一)
- 3.1.4执行函数中有两个变量
- 3.1.5多种情况
- [3.1.6 相关列题](#3.1.6 相关列题)
- 3.1.7递归相关
- 3.2常见复杂度对比,选择最高阶时进行比较,对复杂度进行简化
- 4.空间复杂度
- 5.轮转数组解析
- 🥇结语
🎯引言
欢迎来到HanLop博客的C语言数据结构初阶系列。在这个系列中,我们将深入探讨各种基本的数据结构和算法,帮助您打下坚实的编程基础。作为开篇,我们先从算法复杂度这个核心概念讲起。算法复杂度是评估算法效率的关键标准,通过它我们可以定量分析算法的运行时间和所需的额外空间,从而在编写和优化代码时做出明智的选择。本篇文章将带您了解算法复杂度的基本概念、时间复杂度和空间复杂度的定义及其计算方法,并通过一些实际的示例来帮助您更好地掌握这些重要知识。
👓算法复杂度
1.数据结构与算法
1.1什么是数据结构
**数据结构(Data Structure)**是计算机科学中的一种组织、管理和存储数据的方式,以便能高效地访问和修改数据。它是由一组数据元素(数据对象)以及描述数据元素之间关系的集合(数据关系)组成的。数据结构不仅仅包括存储数据的容器,还包括一系列用于操作这些数据的方法或函数。
1.2什么是算法
**算法(Algorithm)**是一个有限的、明确的步骤序列,用于解决特定问题或完成特定任务。算法是一种描述如何从输入数据得到输出结果的系统方法。它可以用自然语言、伪代码、流程图或编程语言来表示。
1.3数据结构和算法的关系
数据结构是算法的基础,算法依赖于数据结构来高效地组织和操作数据。
具体来说,数据结构提供了存储和组织数据的方式,而算法利用这些结构来实现对数据的操作和处理。选择合适的数据结构可以显著提高算法的效率和性能。
图示:
2.算法效率
如何衡量一个算法的好坏呢?
下面的题目是力扣上的一道题,我们可以先行自己思考出接替思路
然后去力扣:轮转数组验证自己答案的正确性
给定一个整数数组 nums
,将数组中的元素向右轮转 k
个位置,其中 k
是非负数。
示例 1:
c
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
解释
示例 2:
输入:nums = [-1,-100,3,99], k = 2
输出:[3,99,-1,-100]
解释:
向右轮转 1 步: [99,-1,-100,3]
向右轮转 2 步: [3,99,-1,-100]
图示解题思路:
按照上面的思路我们通常会写出下面的代码,但是我们放在力扣上进行测试发现,他没有通过全部案列的测试,这是为什么?
不着急我们等学完后面的内容将为你解答.
c
void rotate(int* nums, int numsSize, int k) {
while(k--)
{
int temp=nums[numsSize-1];
int i=0;
for(i=numsSize-1;i>0;i--)
{
nums[i]=nums[i-1];
}
nums[0]=temp;
}
}
2.1复杂度的概念
复杂度是衡量算法在处理数据时所需资源的概念,通常包括时间复杂度和空间复杂度。复杂度反映了算法的效率和性能,是评估算法好坏的重要标准。
时间复杂度 :描述了算法在处理输入数据时所需的时间量级。它直接关注算法执行步骤的数量,即算法在最坏情况下执行的时间。空间复杂度:描述了算法在执行过程中所需的额外内存空间与输入规模之间的关系。它指的是算法在运行过程中所占用的内存大小。 综合考虑时间复杂度和空间复杂度的目的是找到一个在时间和空间两个方面都能够达到平衡的算法。通常情况下,算法的时间复杂度越低,执行效率越高;而空间复杂度越低,则在内存消耗上越经济。
3.时间复杂度
**定义:**在计算机科学中,算法的时间复杂度是一个函数式T(N),用来定量描述算法在处理输入规模为N的问题时的运行时间。具体来说,时间复杂度描述了算法执行时间随着输入规模增长而变化的趋势。既然是描述程序的运行时间的,那为什么不直接去计算程序运行时间呢?
1.程序运行时间和编译环境以及运行机器配置都有关系.同一算法程序,在同一机器,不同编译器下,运行的时间不同
2.如果测量运行时间只能在程序写好后测试,不能再写程序前通过理论思想计算评估
我们通过时间复杂度定量去评估程序的运行时间,那么T(N)是什么?其实T(N)函数式计算机了程序的执行次数.程序中的代码通过编译后实际上也就是一句句的指令,对于计算机来说,每一句的指令执行时间基本一致,那么执行次数就和程序整体的执行时间成正相关.那么执行次数就可以代表程序时间效率的优劣.
示例:
c
//计算count++执行了多少次
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; i++)//N次
{
//N次中的每一次进行N次循环
for (int j = 0; j < N; j++)
{
count++;//该循环内执行了N*N
}
}
for (int n = 0; n < 2 * N; n++)
{
count++;//进行了2N次循环
}
int m = 5;
while (m--)
{
count++;//进行了5次循环
}
}
我们由上面可知Func1执行的基本操作次数:
T(N)=N^2^+2*N+10 (用大O渐进表示法为O(N^2^):看不懂后面会有讲解)
下面我们观察N在不同的取值下,T(N)的结果是多少
N=10 T(N)=130
N=100 T(N)=10210
N-1000 T(N)=1002010
我们可以发现对T(N)值影响最大的是N^2^项
在计算复杂度的时候,无需太过与精确,因为我们只是通过时间复杂度比较算法程序的增长量级,也就是计算程序能代表增长量级的大概执行次数,复杂度的标识通常使用大O的渐进表示法.
3.1大O的渐进表示法(适用于时间复杂度和空间复杂度)
3.1.1 选择最高阶:
时间复杂度函数时T(N)中,只保留最高项阶,去掉那些低阶项,因为当N不断变大时,低阶项对结果影响越来越小,当N无穷大时,就可以忽略不记了.
c
#include <stdio.h>
void example(int n) {
// 执行n次
for (int i = 0; i < n; i++) {
printf("Hello, World!\n");
}
//执行n^2次
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
printf("Hello, World!\n");
}
}
}
example执行的基本操作次数:T(n)=n^2^+n 根据选择最高阶规则 该函数的复杂度为:O(n^2^)
3.1.2 常数系数:
如果最高项存在且不是1,则去除这个项目的常数系数,因为当N不断变大,这个系数对结果影响越来越小,当N无穷大时,就可以忽略不计了.
c
#include <stdio.h>
void example(int n) {
printf("Hello, World!\n");
//执行2n次
for (int i = 0; i < 2 * n; i++) {
printf("Hello, World!\n");
}
}
example执行的基本操作次数:T(n)=2n 根据常数系数 该函数的复杂度为:O(n)
3.1.3 常数项化一
T(N)中如果没有N相关的项目,只有常数项,用常数1取代所有加法常数.
c
#include <stdio.h>
void example(int n) {
int n=10;
//执行10次
while(n--)
{
printf("Hello, World!\n");
}
}
example执行的基本操作次数:T(n)=10 根据常数项化一 该函数的复杂度为:O(1)
3.1.4执行函数中有两个变量
c
#include <stdio.h>
void example(int n,int m) {
printf("Hello, World!\n");
//执行2n次
for (int i = 0; i < 2 * n; i++) {
printf("Hello, World!\n");
}
//执行2m次
for (int i = 0; i < 2 * m; i++) {
printf("Hello, World!\n");
}
}
example执行的基本操作次数:T(n)=2n+2m 由于两个是不同的变量 我们都要保留下来 且常数项系数要是1 则: O(m+n)
3.1.5多种情况
c
#include <stdio.h>
// 冒泡排序算法
void bubbleSort(int arr[], int N)
{
for (int i = 0; i < N - 1; i++) //外层循环 第1次 第2次 第3次 ..... 第N-1次
{
int exchange = 0;
for (int j = 0; j < N - i - 1; j++) //内存循环 N-1次 N-2次 N-3次 ...... 1次
{
if (arr[j] > arr[j + 1])
{
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
exchange=1;
}
}
if(exchange == 0)
{
break;
}
}
}
(1)当数组有序的,则只要经行一次内存循环 该情况下时间复杂度是最好的 T(N)=N-1 也就是O(N)
(2)若数组的元素是降序则 该情况下时间复杂度是最差的T(N)=N^2^/2 也就是O(N^2^)
(3)若要查找的字符在字符串中间位置:O(N)
像这种不确定的情况下,我们取最差的情况做为该程序的时间复杂度,也就是O(N)=N^2^
3.1.6 相关列题
c
#include <stdio.h>
void example(int n) {
// O(log n) 循环
int count = 1;
while (count < n) {
printf("Hello, World!\n");
count *= 2;
}
}
假设执行次数为x 当2^x^>=n循环猜执行结束
因此执行次数x=long~2~n 所以时间复杂度取O(long~2~n)
其中O(log n) O(long~2~n) O(lgn)的表示都相同
3.1.7递归相关
c
long long Fac(size_N)
{
if(N==0)
return 1;
return Fac(N-1)*N;
}
图示:
T(N)=N 所以时间复杂度为O(N)
3.2常见复杂度对比,选择最高阶时进行比较,对复杂度进行简化
如:当O(n^2^+2^n^)可以写成O(2^2^)
4.空间复杂度
定义:
空间复杂度 S(N)S(N)S(N) 是一个函数,描述了算法在执行过程中所需的额外空间或者内存的量。它包括了算法本身使用的固定空间(常数空间)和动态分配的空间(例如数组、链表等数据结构),但不包括输入数据占用的空间。
4.1计算方法
在计算空间复杂度时,通常需要考虑以下几个方面:
- 固定额外空间: 算法中固定分配的空间,如常量、全局变量等,它们的空间占用量与输入规模 NNN 无关,通常记作 O(1)O(1)O(1)。
- 动态分配的空间: 算法在执行过程中根据输入规模 NNN 动态分配的空间,例如数组、链表等数据结构。这部分空间的占用量通常与输入规模 NNN 相关,需要根据具体情况来分析。
- 递归调用的栈空间: 如果算法使用递归,递归调用会占用系统栈空间,每一层递归调用的空间复杂度与递归深度相关。
- 输入数据占用的空间: 空间复杂度通常不考虑输入数据本身占用的空间,因为它们属于输入参数。
示例:
让我们通过一个简单的示例来说明空间复杂度的概念:
c
#include <stdio.h>
// 函数示例:计算数组元素之和
int sum(int arr[], int n) {
int result = 0; // 固定额外空间,常数级别的空间复杂度 O(1)
for (int i = 0; i < n; i++) {
result += arr[i];
}
return result;
}
int main() {
int arr[] = {1, 2, 3, 4, 5}; // 假设输入规模 N = 5
int n = 5;
int total = sum(arr, n);
printf("Sum of array elements: %d\n", total);
return 0;
}
在这个例子中:
- 固定额外空间: 函数
sum
中除了整型变量result
外,没有额外的固定空间分配,因此固定额外空间为 O(1)O(1)O(1)。 - 动态分配的空间: 函数
sum
中并没有动态分配空间,所以动态分配空间的占用量为 O(0)O(0)O(0),也可以认为是 O(1)O(1)O(1)。 - 递归调用的栈空间: 函数
sum
不涉及递归调用,因此栈空间占用量为 O(0)O(0)O(0),即 O(1)O(1)O(1)。
根据上述分析,函数 sum
的空间复杂度为 O(1)O(1)O(1),因为它所需的额外空间是固定的,不随输入规模 NNN 的增加而增加。
考虑数组空间的示例
假设我们有一个函数,用于创建一个大小为 n 的整数数组,并对数组进行初始化:
c
#include <stdio.h>
#include <stdlib.h>
// 创建一个大小为 n 的整数数组并初始化
int* createArray(int n) {
int* arr = (int*)malloc(n * sizeof(int)); // 动态分配空间,占用的空间与 n 成正比
for (int i = 0; i < n; i++) {
arr[i] = i + 1; // 初始化数组元素为 1, 2, 3, ..., n
}
return arr;
}
int main() {
int n = 5; // 假设输入规模 N = 5
int* myArray = createArray(n);
// 使用数组,这里只是示例,并不影响空间复杂度的计算
for (int i = 0; i < n; i++) {
printf("%d ", myArray[i]);
}
printf("\n");
free(myArray); // 释放动态分配的内存
return 0;
}
空间复杂度分析
在上述示例中,我们需要分析函数 createArray
的空间复杂度:
- 固定额外空间: 函数中除了数组
arr
的指针外,没有额外的固定空间分配,因此固定额外空间为 O(1)O(1)O(1)。 - 动态分配的空间: 函数
createArray
中通过malloc
动态分配了一个大小为 nnn 的整数数组,其空间占用量与输入规模 nnn 成正比,因此动态分配的空间复杂度为 O(n)O(n)O(n)。 - 递归调用的栈空间: 函数
createArray
不涉及递归调用,因此栈空间占用量为 O(0)O(0)O(0),即 O(1)O(1)O(1)。
所以,函数 createArray
的总体空间复杂度由动态分配的空间决定,即 O(n)O(n)O(n)。这表示随着输入规模 nnn 的增加,函数 createArray
所需的额外空间将按线性比例增长。
5.轮转数组解析
经过学习完时间复杂度的 我们回头去看我们第一次写的代码, 它的时间复杂度为O(k*numsSize)时间复杂度较高
现在我们要写一个时间复杂度较低的程序.
代码思路:
c
void rotate(int* nums, int numsSize, int k) {
int* newArr=(int*)malloc(numsSize*sizeof(int));
for(int i=0;i<numsSize;i++)
{
newArr[(k+i)%numsSize]=nums[i];
}
for(int i=0;i<numsSize;i++)
{
nums[i]=newArr[i];
}
}
🥇结语
感谢您阅读完这篇关于算法复杂度的文章。理解算法复杂度是学习数据结构和算法的第一步,它不仅能帮助您编写更高效的代码,还能为您解决复杂问题提供强有力的工具。在接下来的系列文章中,我们将继续探讨C语言中的各种基本数据结构,包括数组、链表、栈、队列、树、堆以及排序算法等。希望通过这些内容,您能更深入地理解和应用数据结构与算法,提升您的编程技能。期待在下一篇文章中与您再次相见!