一、时间复杂度和空间复杂度的概念
1、算法效率
算法效率分析主要分为两种:一种是 时间效率(时间复杂度),一种是空间效率(空间复杂度)。
衡量标准:
时间复杂度:衡量的是一个算法的运行速度。
空间复杂度:衡量一个算法所需要的额外空间。
目前发展来看,更看重时间效率
2、时间复杂度的概念
算法中的基本操作的执行次数,为算法的时间复杂度
误区:并不是计算程序运行所需要的时间(s),与电脑硬件是没有关系的。
时间复杂度是一个估算,看表达式中影响最大的那一项
大O的渐进表示法
实际上我们计算时间复杂度时,并不一定要计算精确的执行次数,而只需要知道大概执行次数,那么,这里我们就会用到大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数
2、在修改后的运行次数函数中,只保留最高阶项
3、如果最高阶项存在且不是1,则去除这个项目相乘的常数,得到的结果就是大O阶
有点枯燥,不太明白?看几个例子,就明白了 👇👇👇
2。1、时间复杂度的简单计算练习
第一个例子,先带着走一遍。
例1:
#include<stdio.h>
void test(int n)
{
int count = 0;
int i = 0;
int j = 0;
int m = 10;
for (i = 0; i < n; i++)
{
for (j = 0; j < n; j++)
{
count++;
}
}
for (i = 0; i < 2 * n; i++)
{
count++;
}
while (m--)
{
count++;
}
printf("%d\n", count);
}
int main()
{
int n = 0;
scanf("%d", &n);
test(n);
return 0;
}
时间复杂度:O(n^2)
来看这个代码 ,它的算法中的基本操作的执行次数。首先进入test函数,看循环执行了多少次。
第一个for循环里面嵌套了一个for循环,也就是外层的for循环执行一次,里面就执行n次,一共就是n 2 ^{2} 2次,再外下看第三个for循环,执行了2n次,再往下,进入while循环,一共循环m次,即10次。所以一共加起来本次算法中的基本操作的执行次数为nn+2n+10次。也就是一个含有未知数n的函数,即F(n)=n 2 ^{2} 2+2n+10,利用数学中的极限思想来看,n趋近于正无穷时,表达式可近似为F(n)=n*n,另外两项对其最终结果的影响时非常小的,也就是上面推导大O阶方法中的第二条,只保留最高阶项。有点眉目了吧,再看几个,下面我们会针对每个规则来看几个例子。
规则1:用常数1取代运行时间中的所有加法常数🪂
例2:
void test(int n, int m)
{
int count = 0;
int i = 0;
for (i = 0; i < n; i++)
{
count++;
}
for (i = 0; i < m; i++)
{
count++;
}
printf("%d\n", count);
}
时间复杂度:O(n+m)
注:
(1、时间复杂度计算中,不一定只有一个未知数,有可能会有多个未知数的存在
(2、如果题目里面说,m远大于n,则他的复杂度就是,O(m)
(3、如果题目里面说,m和n是差不多大,则他的复杂度就是,O(m)或者是O(n)
例3:
void test(int n)
{
int count = 0;
int i = 0;
for (i = 0; i < 100; i++)
{
count++;
}
printf("%d\n", count);
}
时间复杂度:O(n+m)
循环100次,只要是确定的常数次,都是O(1) 。这里循环次数和传入的n是没有关系的,他的循环次数是固定的。
规则2:在修改后的运行次数函数中,只保留最高阶项🪂
例4:
const char* strch(const char* str, char ch)
{
while (*str != '\0')
{
if (*str == ch)
{
return str;
str++;
}
}
return NULL;
}
时间复杂度:O(n)
遍历字符串,查找字符串中是否存在字符ch。
规则3:如果最高阶项存在且不是1,则去除这个项目相乘的常数,得到的结果就是大O阶
例5:
void test(int n)
{
int i = 0;
int m = 20;
int count = 0;
for (i = 0; i < 2 * n; i++)
{
count++;
}
while (m--)
{
count++;
}
printf("%d\n", count);
}
时间复杂度:O(n) 【将最高阶项的系数除去】
例6:
void Bubblesort(int* a, int n)//起始地址,排序个数
{
assert(a);
int i = 0;
int j = 0;
for (j = 0; j <n-1;j++)
{
int flag = 0;
for (i = 0; i < n-1-j; i++)
{
if (a[i +1] > a[i])
{
Swap(&a[i - 1], &a[i]);//传入交换函数中
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
上述是我们熟知的冒泡排序,分析这个里面的时间复杂度,稍有难度。
当j=0时,里面的for循环会执行n-1次;j=1,里面的for循环会执行n-2次,
以此类推,j最大取n-2,此>时里面的for循环执行1次。如图:
再对上式简化:
时间复杂度:O(n 2 ^{2} 2)
通过上面的几个例子,我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均、最坏三种情况:
最好的情况:任意输入规模的最小运行次数(下界)
平均的情况:任意输入规模的期望运行次数
最坏的情况:任意输入规模的最大运行次数(上界)
例如:在长度为n的数组中搜索一个数据x:
最好的情况:1次找到
平均的情况:n/2次找到
最坏的情况:n次找到
在实际中一般情况关注的是算法的最坏运行情况
例7:
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;
}
有一定基础的同学会发现这是一个二分查找,也叫做折半查找,如果看不懂代码,可以去了解一下二分查找,不了解也没关系,我大致介绍一下思想。对于一组有序的数据(长度为n),查找当中的一个数据(x),常规方法就是暴力查找,一个一个从前往后查找 ,那最终的时间复杂度以最坏的情况来计算,所以数组中搜索数据时间复杂度为:就是O(n)。
减小时间复杂度,利用二分法查找,二分查找的意思就是:对于一组有序的数据(长度为n),查找当中的一个数据(x),每一次都取中间的值与我们要查找的值进行比较(最好的情况就是第一次就刚好找到),然后比较大小,就可以缩小一半我们需要查找的空间,(最坏的情况,一直缩小空间,直至空间中只有一个数据了。设查找了y次,找一次,数据减一半,则12222*2…*2=n,即2^y=n.则查找次数为log 2 {{\tiny{2}}} 2n【指的是log以2为底n的对数,在算法中,可以简写成O(logN)】。
例8:
递归调用:
long long Factorial(int n)
{
return n < 2 ? n : Factorial(n - 1) * n;
}
时间复杂度:O(n)
上述代码是一个递归,计算的是n的阶乘。 时间复杂度就是算法中的基本操作的执行次数,这里函数递归调用n次,则时间复杂度:O(n)
常见的复杂度----Big-OComplexity图:
3、空间复杂度的概念:
空间复杂度是一个对算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是程序占用了多少bytes的空间,因为这个没太大意义,所以空间复杂度算的是变量的个数(估算),空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法
推导方法也是一致的:
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数
2、在修改后的运行次数函数中,只保留最高阶项
3、如果最高阶项存在且不是1,则去除这个项目相乘的常数,得到的结果就是大O阶
2。1、空间复杂度的简单计算练习
例1:
void Bubblesort(int* a, int n)//起始地址,排序个数
{
assert(a);
int i = 0;
int j = 0;
for (j = 0; j <n-1;j++)
{
int flag = 0;
for (i = 0; i < n-1-j; i++)
{
if (a[i +1] > a[i])
{
Swap(&a[i - 1], &a[i]);//传入交换函数中
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
数变量个数,两个形参加上里面3个变量(i,j,flag),一共5个,则空间复杂度为O(1)。
注意:可能有人会注意到,第一个for每循环一次,里面不是都会创建一个变量flag吗?所以应该不止5个变量啊?所以一定要理解一句话,时间是累积的,但空间是不累计的。
详细来说就是,第一个for循环每结束一次,flag机会被销毁一次,再次进入循环就再创建一个,就是同一片空间,不停的开辟出来,释放掉,开辟出来,释放掉,即用的其实一直是同一片空间。
例2:
long long* Fibonacci(int n)
{
if (n == 0)
{
return NULL;
}
long long* fibarray = (long long*)malloc((n + 1) * sizeof(long long));
fibarray[0] = 0;
fibarray[1] = 1;
int i = 0;
for (i = 2; i <= n; i++)
{
fibarray[i] = fibarray[i = 1] + fibarray[i - 2];
}
return fibarray;
}
使用malloc动态内存开辟了n+1个,在加上变量n和i,所以一共是n+3个,即O(n)
例3:
long long Factorial(int n)
{
return n < 2 ? n : Factorial(n - 1) * n;
}
创建到最多的函数空间时,一共有n个,每个创建的函数里面的变量都是2个(常数),则空间复杂度为O(n)
二、leetcode刷题练习
题目1:消失的数字:
题目:数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
思路一🌫:
先排序,后判断后一个数是否比前一个数大1。
//先排序,后判断后一个数是否比前一个数大1。
int missingNumber1(int* nums, int numsSize)
{
int i = 0;
int j = 0;
for (i = 0; i < numsSize - 1; i++)
{
int flag = 1;//有序
for (j = 0; j < numsSize - 1 - i; j++)
{
if (nums[j] > nums[j + 1])
{
int tmp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = tmp;
flag = 0;//无序
}
}
if (flag == 1)
{
break;
}
}
for (i = 0; i < numsSize-1; i++)
{
if (nums[i] + 1 != nums[i + 1])
{
return nums[i] + 1;
}
}
return 0;
}
int main()
{
int arr[] = { 0,1,7,5,9,2,4,3,6 };
int size = sizeof(arr) / sizeof(arr[0]);
int x1=missingNumber1(arr, size);
printf("%d\n", x1);
return 0;
}
问题:时间复杂度不符合
排序–>最快排序的时间复杂度也是O(n*logn)
思路二🏝:
将0到n的数相加,然后减去数组中的所有整数
//将0到n的数相加,然后减去数组中的所有整数
int missingNumber2(int* nums, int numsSize)
{
int sum = 0;
int i = 0;
for (i = 0; i < numsSize + 1; i++)
{
sum += i;
}
for (i = 0; i < numsSize; i++)
{
sum -= nums[i];
}
return sum;
}
时间复杂度:O(N)
思路三🌩:异或
异或
回顾一下异或:
异或:按位异或,相同为0,相异为1
举例:
如果两个相同的数异或,结果为0
则可以将数组内的数和0到n的数进行异或,结果是谁就是谁,相同的数都被异或为了0,
不需要将数组中的数对应异或,我们会发现,如果将一组数据异或,其中相同的数都会被异或为0,如图:
代码如下:
int missingNumber(int* nums, int numsSize)
{
int i = 0;
int x= 0;
for (i = 0; i < numsSize; i++)
{
x ^= nums[i];
}
int j = 0;
for (j= 0; j <= numsSize; j++)
{
x ^= j;
}
return x;
}
运行代码:
时间复杂度:O(N)
题目2、轮转数组:
题目:给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
思路一🌫:
暴力解决
void rotate(int* nums, int numsSize, int k)
{
k %= numsSize;
for(int j=0;j<k;j++)
{
int tmp=nums[numsSize-1];
for(int i=numsSize-2;i>=0;i--)
{
nums[i+1]=nums[i];
}
nums[0]=tmp;
}
}
时间复杂度:O(n*k)
这个代码,运行是可以正常运行的,但是不满足题目中时间复杂度的限制
【题目要求为O(1)】
思路二🏝:
以空间换时间
//方法二:以空间换取时间
void rotate2(int* nums, int numsSize, int k)
{
int arr1[100] = { 0 };
int arr2[100] = { 0 };
int i = 0;
int j = 0;
int p = k;
k %= numsSize;
for (i = 0; i <numsSize-k; i++)
{
arr1[i] = nums[i];
}
for (i = numsSize-k , j = 0; i < numsSize; i++, j++)
{
arr2[j] = nums[i];
}
for (i = 0; i < k; i++)
{
nums[i] = arr2[i];
}
for (i = 0,j=k; i < numsSize-k ; i++,j++)
{
nums[j] = arr1[i];
}
}
时间复杂度:O(n-k+n+k+n-k)=O(n)
空间复杂度:O(n)
思路三🌩:逆置
将后k个逆置,前n-k个逆置,再整体逆置
void Reverse(int* nums,int left,int right)
{
while(left<right)
{
int tmp=nums[left];
nums[left]=nums[right];
nums[right]=tmp;
left++;
right--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k%=numsSize;
Reverse(nums,numsSize-k,numsSize-1);
Reverse(nums,0,numsSize-k-1);
Reverse(nums,0,numsSize-1);
}
时间复杂度:O(1)
总结:🪂
1、时间复杂度:算法中的基本操作的执行次数,为算法的时间按复杂度
2、空间复杂度:空间复杂度算的是变量的个数
3、大O渐进表示法