C语言数据结构之复杂度(时间复杂度和空间复杂度)

目录

1.数据结构引入

2.算法的效率

3.时间复杂度

3.1.时间复杂度的定义

3.2.大O的渐进表示法

3.3.最好、平均、最坏情况(执行次数不固定的情况)

3.4.总结

4.空间复杂度

4.1.常见的空间复杂度

4.2.递归的空间复杂度

4.3.补充

5.复杂度OJ练习题

5.1.消失的数字

5.2.旋转数组


前言:

        本文介绍了包含了数据结构的导入部分、算法效率、程序度时间复杂度、空间复杂度和部分关于复杂的OJ例题。若有错误,请各位指正;不想看引入,可以直接跳到第三节时间复杂度。

1.数据结构引入

1.1.数据结构的定义

       数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的
集合。    

在内存中管理数据,开发软件时候需要将数据存储起来,数据存起来展示,或者以供搜索需要不同的数据结构进行存储;

在内存中因为不同场景要以不同的形式存储数据;

例如:  通讯录 需要将每个人的通讯信息存储,可以选择一组结构体将数据按照不一样的数据结构存储。

1.2.算法

        算法: 就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。
简单说算法就是一系列的计算步骤,用来将输入数据转化成输出结果;
如何对数据进行处理的方法,例如:折半查找(二分查询);冒泡排序等等。

2.算法的效率

        如何衡量一个算法的好坏。从算法复杂度的上来分析;那什么是算法复杂度呢?

        单看算法在一台计算机上处理大量数据所用的时间是无法体现算法的效率的;存在硬件的干扰项;为此有了空间复杂度和时间复杂度。

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

3.时间复杂度

3.1.时间复杂度的定义
        算法的时间复杂度是一个函数;一个带有未知数N的表达式,N是程序中未知量的数值;算法所花费的时间和执行次数成正比;因此算法中的基本操作的执行次数就可以当作算法的时间复杂度。
举例:计算出示例1中基本操作的 执行次数和N之间的数学表达式(函数),也叫做时间复杂度。
示例1
// 请计算一下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;
    }
    printf("%d\n", count);
}

++count语句一共运行了

     F(N)=N^{^{2}}+2*N+10

在上述例子上进行了分析

NF(N)
10130
10010210
10001002010

        根据上述的例子分析:在处理数据越多的情况下,不同的项在执行次数下的权重变得比较分化,高阶项占比较大,也可以看出每个项的量级都不一样了,甚至有的像计算出的次数可以忽略不计;因此引出了大O的渐进表示法。

        如何简化函数式,根据大O渐进法,时间复杂度为O(N^2

O(N^2)不是代表算法跑N^2次,而是算法的量级。

3.2.大O的渐进表示法
O:是用于描述函数渐进行为的数学符号。大O渐进法
推导大 O 阶方法:
  1. 用常数1取代运行时间中的所有加法常数。
  2. 在修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。

        随着N的无限制的增大;将对结果影响不大的项去掉,确定的常数是不要的,在N无限大的基础上;2和100没有区别

        根据大O渐进法第二条,示例1的时间复杂度为O(N^2);忽略掉后两项。

示例2

void Func2(int N)
{
     int count = 0;
     for (int k = 0; k < 2 * N ; ++ k)//运算2*N次
     {
         ++count;
     }
     int M = 10;
     while (M--)                       //运算10次
     {
         ++count;
     }
     printf("%d\n", count);
}

   F(N)=2*N+10

  当N无限大的情况下,系数对结果的影响不大。

  100*N,在N趋于无限的时,对最后的结果也没影响。


  根据大O渐进法,系数2对总体的计算结果影响不大,且10为常数可以忽略不记;时间复杂度为O(N).

示例3:

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

根据大O渐进法 时间复杂度为 O(N+M);

分为三种情况

  1. 当M=N时候;复杂度为O(N)
  2. 当N远远大于M时候;二者不是一个量级的,复杂度为O(N);
  3. 当M远远大于N时候;二者不是一个量级的,复杂度为O(M);

示例4

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

根据大O渐进法第一条,用常数1取代运行时间中的所有运行常数,所以复杂度为O(1)。 

O(1),不代表程序执行一次,表示的是常数次。

3.3.最好、平均、最坏情况(执行次数不固定的情况)

最坏情况:任意输入规模最大运行次数;

平均情况:任意输入规模最小运行次数;

最好情况:任意输入规模最小运行次数。

一般情况下关注的是算法的最坏的运行情况。

示例1

// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
//释义
while (*str)
		{
			if (character == (*str))
				return str;

				str++;
		}

strchr函数 :找到字符串中第一个出现的字符,返回指向字符串 str 中第一个出现的 character 的指针。

在未知长度的字符串中寻找一个字符,未知字符串长度设为N。

最好的情况:1次找到;  abcdefghijk.........../0,寻找a

平均的情况:N/2次找到;

最坏的情况:N次找到

因此按最坏的情况 strchr的时间复杂度为O(N).

示例2:冒泡排序

// 计算BubbleSort的时间复杂度?
void BubbleSort(int* a, int n)
{
     assert(a);
     for (size_t end = n; end > 0; --end)//没有长度默认为N
     {
         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;
     }
}

最好的情况:N次交换完成;例如是一个有序数组,{1,2,3,4,5,6,7,8,9,10},是不需要进行交换的,因此判断了N次。exchange =  0 是不变的

最坏的情况:(N*(N-1))/2次找到。例如是个倒序数组{10,9,8,7,6,5,4,3,2,1}每个元素都要进行交换。

因此按最坏的情况 ,根据大O渐进法时间复杂度为O(N^2)。

示例3:二分查找
int BinarySearch(int* a, int n, int x)
{
     assert(a);
     int begin = 0;
     int end = n-1;
     while (begin < end)
     {
         int mid = begin + ((end-begin)>>1);
         if (a[mid] < x)
             begin = mid+1;
         else if (a[mid] > x)
             end = mid;
         else
             return mid;
     }
     return -1;
}

最好的情况:1次交换完成;例如数组{1,2,3,4,5,6,7,8,9,10,11},中找6。

最坏的情况:log_{2}N;

假设数组长度为N  每一次折半查找相当于N/2;当N/2/2……=1的时候,找到最后,元素为1。

因此得出 2^{x}=N;转换成次数就是x=log_{2}N

因此按最坏的情况 ,根据大O渐进法时间复杂度为O(log_{2}N)。

二分查找实际上是不使用的;使用的前提是数组排序。

        log_{2}N,平常编写不方便,一般情况下写成\log N

数据1000100W1亿
O(N)1000100W1亿
O(logN)102030

示例4 :n的阶乘
long long Fac(size_t N)
{
     if(0 == N) //0!为1
     return 1;
 
     return Fac(N-1)*N;
}

递归N次,每一次递归的函数的的时间复杂度为O(1)常数值;

因此这个函数的时间复杂度为O(N)

示例5:斐波那契递归
long long Fib(size_t N)
{
     if(N < 3)
     return 1;
     return Fib(N-1) + Fib(N-2);
}

根据大O渐进法时间复杂度为O(2^N)

根据斐波那契数的递归的思想画图得

求斐波那契函数的时间复杂度是个等比数列;右侧的递归会提前结束,但是对于整体而言可以忽略不记,可以记为常数。因此时间复杂度为O(2^N)

  没用的算法 ,2^30大约为10亿。

3.4.总结
  1. 计算时间复杂度最好是代码的底层思想;画图可以解决问题;
  2. 常见的复杂度对比
表达式复杂度
100                 O(1)    常数阶

N^2

O(N)线性阶
3N^2+4N+5O(N^2)平方阶
log_{2}N+2O(log_{2}N)对数阶
N*log_{2}N+5O(N*log_{2}N)nlogn阶
3N^3+4N+5O(N^3)立方阶
2^{N}+2N+3        O(2^{N})指数阶

如图:

4.空间复杂度

        空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 空间复杂度算的是变量的个数 和时间复杂度的计算类似,使用 O 渐进表示法
注意: 函数运行时所需要的栈空间 ( 存储参数、局部变量、一些寄存器信息等 ) 在编译期间已经确定好了, 此空间复杂度主要通过函数在运行时候申请的额外空间来确定。
4.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;
     }
}

总共创建了3个局部变量


 size_t end = n
 int exchange = 0;
 size_t i = 1;

但是对于大O渐进法来说是O(1)。

示例2:

// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
long long* Fibonacci(size_t n)
{
     if(n==0)
         return NULL;
 
     long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));
     fibArray[0] = 0;
     fibArray[1] = 1;
     for (int i = 2; i <= n ; ++i)
     {
         fibArray[i] = fibArray[i - 1] + fibArray [i - 2];
     }
     return fibArray;
}

额外创建了一个N+1的数组,所以空间复杂度为O(N)

通常情况下,不是O(1)就是O(N)。

4.2.递归的空间复杂度

示例1:

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

单个函数基本没有空间复杂度,递归建立栈帧,每个栈帧有常数个空间,从Fac(N)到Fac(0)总共创建N+1个栈帧空间;所以空间复杂度是O(N);另外栈帧创建时候的寄存器也算入,但是也是常数个,可以忽略的。

示例2:

// 计算斐波那契递归Fib的空间复杂度?
long long Fib(size_t N)
{
     if(N < 3)
         return 1;
 
     return Fib(N-1) + Fib(N-2);
}

上述代码每一次的开辟新的空间的时候,都会额外开辟一个空间复杂度为1的空间,空间复杂度为O(N);斐波那契函数伴随着开辟和销毁栈帧;最多开辟的栈帧是N个,因此空间复杂度是O(N)

时间是无法重复利用的;

空间是可以重复利用的。

时间复杂度计算的是大概的运算次数,空间复杂度是大概的变量多少。

4.3.补充

        printf的结果是一样的函数在使用完函数之后,会释放栈帧空间;下一次的函数调用会条用同一块的空间。

#include<stdio.h>
void F1()
{
	int a = 2;
	printf("%p\n", &a);
}
void F2()
{
	int a = 2;
	printf("%p\n", &a);
}

int main()
{
	F1();
	F2();
	return 0;
}

递归的方式

        函数中再次调用函数自身的方式叫做递归;递归的释放空间是先释放后开辟的空间,所以二者结果不可能相同。

#include<stdio.h>
void F2()
{
	int a = 2;
	printf("%p\n", &a);
}
void F1()
{
	int a = 2;
	printf("%p\n", &a);
	F2();
}
int main()
{
	F1();
	return 0;
}

5.复杂度OJ练习题

5.1.消失的数字

面试题 17.04. 消失的数字 - 力扣(LeetCode)

思路1:  排序+二分查找

        快排sqort()的时间复杂度O(N*log_{2}N),示例,用sqort函数对数据进行了排序,再寻找确缺失的数字。

#include <stdio.h>
int compar(const void* a, const void* b)
{
	return *(int*)a - *(int*)b;
}
int main()
{
	int arr[] = { 0,1,3,2,6,4,5,9,10,7 };
	int sz = sizeof(arr)/sizeof(arr[0]);
	int arr2[20] = { 0 };
	qsort(arr,sz,sizeof(arr[0]),compar);
	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", arr[i]);
	}
	printf("\n");

	//比较环节
	for (i = 0; i < sz; i++)
	{
		if (arr[i] != i)
		{
			printf("%d",i);
			break;
		}
	}
	return 0;
}

思路2  :按位异或 异或不需要考虑顺序 异或支持交换律 时间复杂度为O(N),

            空间复杂度为O(1)

int missingNumber(int* nums, int numsSize) 
{
    int val = 0;
    int i = 0;
    for(i = 0; i < numsSize;i++)
    {
        val ^= nums[i];

    }
     for(i = 0; i <= numsSize;i++)
    {
         val ^= i;
    }
    return val;
}

思路3: 0-N项求和 减去 数组的值

int missingNumber(int* nums, int numsSize) 
{
    int sum = (numsSize*(numsSize+1))/2;
    int i = 0;
    for(i = 0; i < numsSize;i++)
    {
        sum -= nums[i];

    }
    return sum;
}

时间复杂度为O(N);空间复杂度为O(1)。

5.2.旋转数组

189. 轮转数组 - 力扣(LeetCode)

方法一:暴力求解的时间复杂度为O(N^2)

方法二:使用反转求解,时间复杂度是O(N)。

void reverse(int* nums,int begin,int end)
{
    int tmp = 0;
    while(begin < end)
    {
        tmp = nums[begin];
        nums[begin] = nums [end];
        nums[end] = tmp ;
        end--;
        begin++;
    }
}

void rotate(int* nums, int numsSize, int k) 
{
    if(k > numsSize)
      k %= numsSize;
    reverse(nums,0,numsSize-k-1);
    reverse(nums,numsSize-k,numsSize-1);
    reverse(nums,0,numsSize-1);
}

时间复杂度O(N)空间复杂度O(1)。

方法三:以空间换时间,时间复杂度和空间复杂度都为O(N)

void rotate(int* nums, int numsSize, int k) 
{
    if(k > numsSize)
      k %= numsSize;
    int* tmp=(int*)malloc(sizeof(int)*numsSize);
    //替换
    memcpy(tmp,nums+numsSize-k,sizeof(int)*k);
    memcpy(tmp+k,nums,sizeof(int)*(numsSize-k));
    memcpy(nums,tmp,sizeof(int)*numsSize);


}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值