1 算法效率
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
2 时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数(指数学上的函数式),它定量描述了该算法的运行时间。一个算法执行所耗费的时间难以具体的算出来,它与环境有关,由于一个算法所花费的时间与其中语句的执行次数成正比例,于是我们定义算法中的基本操作的执行次数为算法的时间复杂度。
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语句总共执行了N^2+N*2+10次,我们得到一个函数式:F(N) = N^2+N*2+10
N = 10 F(N) = 130
N = 100 F(N) = 10210
N = 1000 F(N) = 1002010
N越大,后面两项对函数值的影响越小,(N较小的时候我们不考虑,认为他们是一样的,因为CPU跑得非常快,比较没有意义)故在实际中我们计算时间复杂度时,不一定要计算精确的执行次数,只需要知道大概执行次数,这里我们使用大O的渐进表示法(进行大概的估算,计算算法属于哪个量级),例如上述代码时间复杂度可用O(N^2)表示。
#include <stdio.h>
#include <time.h>
int main()
{
int begin1 = clock();//clock函数可捕捉从程序开始运行到调用clock()的ms数
int n = 100000000;
int x = 10;
for (int i = 0; i < n; i++)
{
x++;
}
int end1 = clock();
printf("%dms\n", end1-begin1);
return 0;
}
(0ms代码中间差异小于1ms)
2.1 大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
例1:
// 计算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);
}
F(N) = 2*N+10 -> 时间复杂度为O(N) (系数也要去掉)
例2:
// 计算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);
}
O(M+N)或者O(max(M+N))(如果有说明,M远大于N,O(M),N远大于M,O(N))
例3:
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
O(1),不是代表一次,是代表常数次
2.2 常见量级
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
//底层是查找一个字符
while(*str)
{
if(*str == character)
{
return str;
}
else
{
str++;
}
}
要分最好、最坏和平均去查找
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
最好是O(1),最坏是O(N),平均是O(N/2) (1+2+...+N)/N
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
2.3 例题
1、消失的数字
思路一:求和0到n,再依次减去数组中值(O(N))
int missingNumber(int* nums, int numsSize)
{
int N = numsSize;
int ret = (0 + N)*(N + 1) / 2;
for (int i = 0; i < numsSize; ++i)
{
ret -= nums[i];
}
return ret;
}
思路二:异或(相同为0,相异为1)
1、交换律: A ^ B = B ^ A
2、结合律: ( A ^ B ) ^ C = A ^ ( B ^ C )
3、自反性: A ^ B ^ B = A
int missingNumber(int* nums, int numsSize)
{
int N = numsSize;
int x = 0;
for (int i = 0; i < numsSize; i++)
{
x ^= nums[i];
}
for (int j = 0; j <= N; j++)
{
x ^= j;
}
return x;
}
2、轮转数组
用大O的渐进表示法,F(N) = k*(N-1),时间复杂度为O(k*N),因为k%=N,最好情况k = 0,最坏为k = N-1
思路一:暴力求解
void rotate(int* nums, int numsSize, int k) {
k = k%numsSize;
while(k--)
{
int tmp = nums[numsSize-1];
for(int i = numsSize -1;i>0;i--)
{
nums[i] = nums[i-1];
}
nums[0] = tmp;
}
}
思路二:
//先写一个逆置函数
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,0,numsSize-1-k);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
(解题写出思路选时间复杂度最优)
3、计算时间复杂度
void fun(int n)
{
int x = 0;
for(int i = 1;i < n;i*=2)
{
++x;
}
printf("%d\n",x);
}
O(log₂N),为了方便可省略底数O(log N),只有以2为底可省略
// 计算BinarySearch的时间复杂度?
int BinarySearch(int* a, int n, int x)
{
assert(a);
int begin = 0;
int end = n-1;
// [begin, end]:begin和end是左闭右闭区间,因此有=号 while (begin <= end),上面是左闭右闭,下面也要是,要保持上下区间一致
{
int mid = begin + ((end-begin)>>1);//防溢出
if (a[mid] < x)
begin = mid+1;
else if (a[mid] > x)
end = mid-1;
else
return mid;
}
return -1;
}
二分查找,当这个查找区间只剩一个值时最坏,N/2/2/.../2 = 1,假设查找x次,x = log₂N,时间复杂度为O(log₂N)
二分查找与暴力查找差别非常大:
二分查找的缺点是外强中干,实际不太中用,这与排序有关,但不是主要原因,因为排序只用排一次,后面查找可以一直用,主要原因是结构,它必须用数组结构,不方便插入删除,需要挪动数据,链表也不行,链表不能用二分查找,下标不能够随机访问