算法的时间复杂度与空间复杂度的讲解
什么是数据结构与算法
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的
数据元素的集合。
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为
输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般
是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
时间复杂度
算法中的基本操作的执行次数,为算法的时间复杂度。
时间复杂度,计算时为该算法的最坏运行情况,是一个稳健保守预期。
抓大头,取决定性结果的一项,是执行次数的量级,去掉那些对结果影响不大的项。忽略系数(N无限大时,系数对结果的影响可以忽略不计),常数次时(无论常数多大)复杂度为O(1)
常见的时间复杂度的计算
void Func3(int N,int M)
{
int count=0;
for(int k=0;k<3*N;k++)
{
++count;
}
for(int k=0;k<2*M;k++)
{
++count;
}
printf("%d\n",count);
}
因为N,M的量级并不清楚,时间复杂度为O(N+M),去除系数。
如果N和M一样大,则为O(N)或O(M),忽略系数
如果N远大于M,则为O(N)
如果N远小于M,则为O(M)
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 fi(a[mid]>x)
end=mid-1;
else
return mid;
}
return -1;
}
每次查找时,区间数据个数减半,最坏情况,查找区间缩放到只剩一个数据时(开头,结尾,或找不到)为最坏情况,假设查找了x次,则数据个数为2^x=N,则x=log_2 N,时间复杂度为 log_2 N
递归调用函数复杂度的计算
long long Fib(size_t N)
{
if(N<3)
return 1;
return Fib(N-1)+Fib(N-2);
}
执行次数=2 ^ 0 + 2 ^ 1……2^ (N-1) = 2^N-1
时间复杂度O(2^N)
消失的数字
思路一:遍历加排序,下一个数字不等于下一个数+1,则下一个数就是消失的数字。
时间复杂度:排序最快为O(N*log N)(qsort)+遍历O(N)
思路二:计算0到N的总和减去遍历数组的总和,结果为消失的数字。
时间复杂度:遍历O(N)
思路三:遍历异或
时间复杂度:遍历异或O(N)
int missnumber(int *nums,int numsize)
{
int N=numsize;
int sum=N*(N+1)/2; //计算0到N的总和
for(int i=0;i<numsize;i++)
{
int ret-=num[i]; //遍历减去数组中数的总和
}
return ret;
}
int missnumber(int *nums,int numsize)
{
int N=numsize;
int ret=0;
for(int i=0;i<N;i++)
{
ret=ret^num[i]^i; //遍历异或数组中数的与每个数,剩下的ret就是缺失数
}
ret=ret^N; //补上遍历时少异或了N
return ret;
}
空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用(额外开辟)存储空间大小的量度 。
递归函数的空间复杂度
long long* Fibonacci(size_t n)
{
if (n==0)
return 0;
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]=fibaarray[i-1]+fibarray[i-2];
}
return fibarray;
}
malloc函数额外开辟了n+1个字节的空间,空间复杂度为O(N)
long long Fac(size_t n)
{
if (n==0)
return 1;
return Fac(n-1)*n;
}
递归调用时,Fac(n)-》Fac(n-1)-》……-》Fac(1)-》Fac(0)在栈上共开辟了n个常数的空间,空间复杂度为O(N)
long long Fib(size_t N)
{
if(N<3)
return 1;
return Fib(N-1)+Fib(N-2);
}
递归最大问题:深度太深,容易栈溢出
时间是累计的,空间是可以重复利用的
从Fib(N)-》Fib(N-1)……Fib(3)-》Fib(2),调用到Fib(2)时返回值时释放空间,返回Fib(3),Fib(3)-》Fib(3-1)时,将原Fib(2)的空间给Fib(3-1)复用
额外的空间开辟为Fib最深的栈帧个数,为O(N)
void func1()
{
int a=0;
printf("%p\n",&a);
}
void func2()
{
int a=0;
printf("%p\n",&a);
}
int main()
{
func1();
func2();
return 0;
}
可以看出打印的地址相同,不同函数使用同一块空间
练习题
轮转数组oj
给定一个整数数组nums,将数组中的元素向右轮转k个位置,其中k时非负数
要求时间复杂度O(N),空间复杂度O(1)
方法一:循环右旋1次,合计右旋k%N次(右旋N次数组没有改变)
时间复杂度:最坏情况下右旋N-1次,O(N^2)
空间复杂度O(1)
方法二:创建一个与原数组相同大小的空间temp,将后k个元素放到temp前面,将剩下的k-1个元素放到temp后面
时间复杂度:O(N),遍历复制数组
空间复杂度:O(N),开辟N个大小空间的temp数组
方法三:前N-k个数组元素逆置,后k个元素逆置,整体逆置
时间复杂度:O(N)
空间复杂度:O(1)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void revolve(int* arr,int numbersize ,int k) //方法一
{
int n=numbersize ;
k %= n;
for (int i = 0; i < k; i++)
{
int temp = arr[n - 1];
for (int j = n-1; j >0; j--)
{
arr[j] = arr[j-1];
}
arr[0] = temp;
}
}
void rotate(int* arr, int numbersize, int k) //方法二
{
k %= numbersize;
int* temp = (int*)malloc(sizeof(int) * numbersize);
memcpy( temp+k, arr, sizeof(int)*(numbersize-k));
memcpy( temp, arr + numbersize - k,sizeof(int)*k);
memcpy(arr, temp, sizeof(int)*numbersize);
}
void reverse(int* arr, int left, int right) //方法三
{
while (left < right)
{
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
void inverse(int* arr, int numbersize, int k)
{
k %= numbersize;
reverse(arr, 0, numbersize - k-1);
reverse(arr, numbersize-k, numbersize-1);
reverse(arr, 0, numbersize - 1);
}
void print(int* arr, int numbersize) //输出
{
for (int i = 0; i < numbersize; i++)
{
printf("%d", arr[i]);
}
printf("\n");
}
void test1() //测试
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
revolve(arr, sizeof(arr) / sizeof(arr[0]), 2);
print(arr, sizeof(arr) / sizeof(arr[0]));
}
void test2()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
rotate(arr, sizeof(arr) / sizeof(arr[0]), 2);
print(arr, sizeof(arr) / sizeof(arr[0]));
}
void test3()
{
int arr[] = { 1,2,3,4,5,6,7,8,9 };
inverse(arr, sizeof(arr) / sizeof(arr[0]), 2);
print(arr, sizeof(arr) / sizeof(arr[0]));
}
int main()
{
test1();
test2();
test3();
return 0;
}