本文介绍算法复杂度的知识,以及相关练习
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。没有一种单一的数据结构对所有用途都有用,所以我们要学习各式各样的数据结构,如:线性表、树、图、哈希等。
算法(Algorithm):就是定义良好的计算过程,它取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
理解透彻每种常用算法后多刷题。
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢(不是运行时间),空间复杂度主要衡量一个算法运行所需要的额外空间,在当今计算机存储空间迅速发展的情况下,空间复杂度已经不是考虑重点。
复杂度在校招中的考察已经很常见,并且在刷题时,对复杂度的限制也是十分明显。
在计算机科学中,算法的时间复杂度是一个函数式T(N),它定量描述了该算法的运行时间。时间复杂度是衡量程序的时间效率,但不能直接计算程序的运行时间,原因如下:
T(N)函数式计算了程序的执行次数。通过计算程序的执行次数,可以代表程序时间效率的优劣。
// 请计算⼀下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;
}
}
实际中我们计算时间复杂度时,通常使用大O的渐进表示法,只需要计算程序能代表增长量级的大概执行次数。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号 。
推导大O阶规则:
Fun1
,算法复杂度为O(N^2)
;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);
}
Func2执行的基本操作次数: T ( N ) = 2 N + 10 T(N)=2N + 10 T(N)=2N+10,根据推导规则,Func2的时间复杂度为: O ( N ) O(N) O(N)。
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);
}
Func3执行的基本操作次数: T ( N ) = M + N T(N)=M + N T(N)=M+N,通常情况下时间复杂度为 O ( N + M ) O(N+M) O(N+M)
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
Func4执行的基本操作次数: T ( N ) = 100 T(N)=100 T(N)=100,根据推导规则1,Func4的时间复杂度为: O ( 1 ) O(1) O(1)。
const char * strchr ( const char * str, int character)
{
const char* p_begin = s;
while (*p_begin != character)
{
if (*p_begin == '\0')
return NULL;
p_begin++;
}
return p_begin;
}
strchr执行的基本操作次数:
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;
}
}
BubbleSort执行的基本操作次数:
则 T ( N ) = N ∗ ( N + 1 ) 2 T(N)=\frac{N*(N + 1)}{2} T(N)=2N∗(N+1) 。
BubbleSort的时间复杂度取最差情况为: O ( N 2 ) O(N^{2}) O(N2)。
void func5(int n)
{
int cnt = 1;
while (cnt < n)
{
cnt *= 2;
}
}
假设执行次数为x,则 2 x = n 2^{x}=n 2x=n,因此执行次数 x = l o g n x = log n x=logn,func5的时间复杂度取最差情况为: O ( l o g n ) O(log n) O(logn) .
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
递归算法的时间复杂度 = 单次递归的时间复杂度 * 递归次数
调用一次Fac函数的时间复杂度为 O ( 1 ) O(1) O(1),而在Fac函数中,存在n次递归调用Fac函数,因此阶乘递归的时间复杂度为: O ( n ) O(n) O(n)。
空间复杂度也是一个数学表达式,是对一个算法在运行过程中因为算法的需要额外临时开辟的空间。空间复杂度不是程序占用了多少bytes的空间,算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
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;
}
}
BubbleSort额外申请的空间有exchange等有限个局部变量,使用了常数个额外空间,因此空间复杂度为 O ( 1 ) O(1) O(1)。
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
Fac递归调用了N次,额外开辟了N个函数栈帧,每个栈帧使用了常数个空间,因此空间复杂度为: O ( N ) O(N) O(N)。
n n n | l o g 2 n log_{2} n log2n | n × l o g 2 n n ×log_{2} n n×log2n | n 2 n^{2} n2 | n 3 n^{3} n3 | 2 n 2^{n} 2n | n ! n! n! |
---|---|---|---|---|---|---|
4 4 4 | 2 2 2 | 8 8 8 | 16 16 16 | 64 64 64 | 16 16 16 | 24 24 24 |
8 8 8 | 3 3 3 | 24 24 24 | 64 64 64 | 512 | 256 | 80320 |
10 10 10 | 3.32 | 33.2 | 100 | 1000 | 1024 | 3628800 |
16 16 16 | 4 4 4 | 64 64 64 | 256 | 4096 | 65536 | 2.1 × 1 0 13 2.1×10^{13} 2.1×1013 |
32 32 32 | 5 5 5 | 160 | 1024 | 32768 | 4.3 × 1 0 9 4.3×10^{9} 4.3×109 | 2.6 × 1 0 35 2.6×10^{35} 2.6×1035 |
128 | 7 7 7 | 896 | 16384 | 2097152 | 3.4 × 1 0 38 3.4×10^{38} 3.4×1038 | ∞ \infty ∞ |
1024 | 10 10 10 | 10240 | 1048576 | 1.07 × 1 0 9 1.07×10^{9} 1.07×109 | ∞ \infty ∞ | |
10000 | 13.29 | 132877 | 108 | 1 0 12 10^{12} 1012 | ∞ \infty ∞ | ∞ \infty ∞ |
从图表可以看出,随着n的增长,不同复杂度的函数值变化差异很大, O ( 1 ) O(1) O(1)复杂度最低, O ( n ! ) O(n!) O(n!)复杂度增长最快。
题目链接:https://leetcode.cn/problems/rotate-array/description/
循环K次将数组所有元素向后移动一位(但时间复杂度超出限制)。
void rotate(int* nums, int numsSize, int k) {
while(k--)
{
int end = nums[numsSize-1];
for(int i = numsSize - 1;i > 0 ;i--)
{
nums[i] = nums[i-1];
}
nums[0] = end;
}
}
时间复杂度 O ( n 2 ) O(n^{2}) O(n2).
空间复杂度 O ( n ) O(n) O(n),申请新数组空间,先将后k个数据放到新数组中,再将剩下的数据挪到新数组中。
void rotate(int* nums, int numsSize, int k)
{
//创建新数组
int newArr[numsSize];
//向右轮转K次,将结果保存在临时数组中
for (int i = 0; i < numsSize; ++i)
{
newArr[(i + k) % numsSize] = nums[i];
}
//将临时数组中的数据导入到数组中
for (int i = 0; i < numsSize; ++i)
{
nums[i] = newArr[i];
}
}
空间复杂度 O ( 1 ) O(1) O(1),前n - k个逆置,后k个逆置,整体逆置。
void reverse(int* nums,int begin,int end)
{
while(begin<end){
int tmp = nums[begin];
nums[begin] = nums[end];
nums[end] = tmp;
begin++;
end--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k = k%numsSize;
reverse(nums,0,numsSize-k-1);
reverse(nums,numsSize-k,numsSize-1);
reverse(nums,0,numsSize-1);
}
这部分是数据结构的前置知识,但也很重要,如有错误还望指正。
数据结构从这篇开始了,明天更新顺序表,还望多多支持。
想起一段游戏台词(略改)作为结尾吧:
——敬我们的过去、现在、未来和稚子至死不渝的梦。
——敬坚忍的岁月、每个悲伤的夜晚,和终将到来的黎明。
——敬不在沉默的历史、热烈而勇敢的奔赴,和通往群星的旅途。
——敬不完美的明天。
安