目录
前言
一、算法的复杂度
二、时间复杂度
三、时间复杂度习题
例1、2、3、4:普通循环的时间复杂度
例5:二分查找法的时间复杂度
例6:冒泡排序的时间复杂度
例7:普通递归的时间复杂度
例8:求斐波那契数的时间复杂度
附加题:逆置数组
前言
C程序在实现某种特定的功能的时候,会有多种代码去实现,为了提高程序的效率,需要考虑代码执行的时间和代码存储需要的空间。而在当今存储设备的进步,人们不是很在意存储代码所花空间的大小,会更在意代码运行的时间。
算法的复杂度
我们熟知的斐波那契数列,用递归的方式去实现的时候,代码十分简洁,但是这简洁的代码能说明它的效率高吗?
long long Fib(int N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般 是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算 机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个函数 ,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有当程序执行起来,从它的执行快慢的情况下主观的判断。如果我们将每个代码都上机测试,那样将会很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法 的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
大 O 的渐进表示法
1.若运行次数为常数,无论大小,时间复杂度均记为O(1)
2.若计算的运行次数为N的表达式,则只保留最高阶项,并将系数视为1
上述方法可以去掉对结果影响不大的项,简单明了的表示出了执行次数,在某些情况下算法的时间复杂度存在最好、平均、最坏三种。
最坏情况 | 任意输入规模的最大运行次数 ( 上界 ) |
平均情况 | 任意输入规模的期望运行次数 |
最好情况 | 任意输入规模的最小运行次数 ( 下界 ) |
时间复杂度习题
例1
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i) //N
{
for (int j = 0; j < N; ++j) //N
{
++count;
}
}
//共N*N次
for (int k = 0; k < 2 * N; ++k) //2*N
{
++count;
}
int M = 10;
while (M--) //10
{
++count;
}
printf("%d\n", count);
}
经过计算count一共计算F(N)= N^2+2*N+10次
N=10 F(N)=130
N=100 F(N)=10210
N=1000 F(N)=1002010
上述中随着N的值不断增大,除了最高阶项,其余项对计算次数了影响很小,所以可以忽略不计。那么实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法可知时间复杂度为O(N^2)。
例2
// 计算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)
例3
// 计算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);
}
经过计算一共计算F(N)= M+N次,则时间复杂度为O(M+N),若M>>N,则时间复杂度为O(M),反之则为O(N)。
例4
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
程序总共计算了100次,次数为常数,所以时间复杂度为O(1)
例5
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;
}
二分查找法,最好的情况是第一次就找到了,或者几次就找到了,这样时间复杂度为O(1)
最坏的情况是最终找不到需要找到的值。
例6
冒泡排序
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;
}
}
执行次数为:(N-1)+(N-2)+(N-3)+.....+2+1=(N^N-N)/2,故时间复杂度为O(N^N)若数据本来就是有序的,则时间复杂度为O(N)。
例7
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
该递归调用时,N的值变化情况有:N、N-1、N-2、N-3......3、2、1,总共有N次变化情况,所以该递归的时间复杂度为O(N)。
例8
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
递归数为:2^0+2^1+2^2+......+2^(N-2)+2^(N-1)=-2^0+2^N=2^N+1
则斐波那契数的递归的时间复杂度为O(2^N)
附加题:轮转数组——只考虑时间复杂度
方法一:
int main()
{
int arr[] = { 1,2,3,4,5,6,7 };
int sz = sizeof(arr) / sizeof(arr[0]);
int k;
scanf("%d", &k);
while (k--)
{
int n = arr[sz-1];
for (int i = sz-1; i > 0; --i)
{
arr[i] = arr[i - 1];
}
arr[0] = n;
}
for (int j = 0; j < sz; j++)
printf("%d ", arr[j]);
return 0;
}
打印只是为了观察交换的结果,我们忽略打印的步骤,可以计算出。
k=1,时间复杂度为O(N)
未知k等于多少,时间复杂度为O(kN)
没说明k的值,时间复杂度为O(N^N)
方法二:牺牲空间,提高算法效率
void rotate(int* nums, int numsSize, int k)
{
int i = 0;
int j = 0;
if (k > numsSize)
k = k % numsSize;
if (k == numsSize)
{
for (i = 0; i < numsSize; ++i)
{
printf("%d", nums[i]);
}
}
else
{
int left = numsSize - k;
int right = k;
int* arr1 = (int*)malloc(sizeof(int) * left);
int* arr2 = (int*)malloc(sizeof(int) * right);
if(arr1==NULL || arr2==NULL)
{
printf("malloc fail");
return;
}
for (i = 0; i < numsSize; ++i)
{
if (i < left)
arr1[i] = nums[i];
else
{
arr2[j] = nums[i];
j++;
}
}
for (i = 0, j = 0; i < numsSize; ++i)
{
if (i < right)
nums[i] = arr2[i];
else
{
nums[i] = arr1[j];
j++;
}
}
free(arr1);
free(arr2);
}
}
该方法为:将需要倒叙的数组元素分别放在新创建两个数组中,最终再将两个新创建的数组按照需要的顺序放入原数组中,放入新数组执行了N次,再放入元素组执行力N次,一共执行了2N次,所以时间复杂度为O(2N)=O(N),该方法需要浪费一定的空间。
例如:k=3
原数组:int nums[ ]={1,2,3,4,5,6,7}
新数组:int arr1[ ]={1,2,3,4}
新数组:int arr2[ ]={5,6,7}
调整后的原数组:int nums[ ]={5,6,7,1,2,3,4}
方法三:最优解
void reverse(int* p, int left, int right)
{
while (right > left)
{
int tmp = p[left];
p[left] = p[right];
p[right] = tmp;
right--;
left++;
}
}
void rotate(int* p ,int sz, int k)
{
if (k > sz)
k %= sz;
reverse(p, 0, sz - k - 1);
reverse(p, sz - k , sz - 1);
reverse(p, 0, sz - 1);
}
该方法为先将左边部分逆序,再将右边部分逆序,然后再整体逆序。
例如:k=3
原数组:int nums[ ]={1,2,3,4,5,6,7}
前n-k个逆置:int nums[ ]={4,3,2,1,5,6,7}
后k个逆置:int nums[ ]={4,3,2,1,7,6,5}
整体逆置:int nums[ ]={5,6,7,1,2,3,4}