目录
一、算法效率
1.1 算法效率的类别
1.2 大O的渐进表示法
二、时间复杂度
2.1 概念
2.2 例题
三、空间复杂度
3.1 概念
3.2 例题
在了解时间复杂度与空间复杂度之前,让我们先了解一下什么是算法效率和表示方法。
算法效率分析分为两种: 第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的空间大小
我们计算时间复杂度时,并不一定要计算精确的执行次数,而只需要大概执行次数,使用大o的渐进表示法。
在大O的渐进表示法中,只保留最高阶项,且去除与这个项目相乘的常数
—个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数则为算法的时间复杂度。
(1):O(N^2)
void Func(int N) {
int count = 0;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < N; j++)
{
count++;
}
} 到这里总共执行了N*N次
for (int k = 0; k < 2 * N; k++)
{
count++;
} 这里执行了2*N次
int M = 10;
while (M--)
{
count++;
} 这里执行了10次
printf("%d\n", count);
}
准确的次数:N*N +2*N +10
随着N的增大,这个表达式中N ^2对结果的影响是最大的,则将后面的2*N和10全部舍去
根据大O的渐进表示法,则估算时间复杂度:O(N ^2)
(2):O(N+M)
void Func(int N, int M)
{
int count = 0;
for (int k = 0; k < N; k++)
{
count++;
}
执行N次
for (int k = 0; k < M ; k++)
{
count++;
}
执行M次
printf("%d\n", count);
}
准确的次数:O(N+M)
特殊情况:假设给了条件:M远大于N,则答案为O(M)
若M和N差不多大,则为O(M)或O(N)
(3):O(1)
void Func(int N)
{
int count = 0;
for (int k = 0; k < 100; k++)
{
count++;
}
printf("%d\n", count);
}
这里执行了100次
准确的次数:O(1)
确定的常数次,都是O(1)
(4):O(N)
const char * strchr ( const char * str, int character )
{
while(*str!='\0')
{
if(*str == character)
return *str;
++str;
}
return NULL;
}
这个函数是数组中用来查找指定数据的
在数组中搜索数据时,这里会出现两种极端情况:第一次就找到,或者最后一次才找到
不知道数组有多长,假设长度为N,这里就会出现不同时间复杂度的情况,究竟是O(1)还是O(N/2)还是O(N)呢?
当算法的时间复杂度存在最好、平均和最坏情况,当算法存在这三种情况的时候,看最坏!
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
(5):O(N^2)(冒泡排序)
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
第二趟冒泡:N-1
第三趟冒泡:N-2
......
第N趟:1
根据等差数列的性质可得:准确的次数:(N+1)*N/2
括号打开得:N*N/2+N/2
把1/2的系数和N/2舍去后可得答案为:O(N^2)
注意:不是说一层循环就是O(N)两层循环就是O (N^2),具体要看程序,通过算法过程去分析!!!!
(5):O(logN)(折半查找)(二分查找)
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;
}
二分查找
先来看二分查找的定义:
首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。
简而言之,就是将一段有序数列(前提是有序!)每次查找都从中间分开,在符合条件的那一半区域内再次划分成两半,不断重复直到找到目标
根据上文所说,用最坏情况来想,最后一次找到目标,假设有8个数,则最多查找3次,8/2/2/2=1
16个数,则最多查找4次,16/2/2/2/2=1。
发现规律了吗?8为2的3次方,16为2的4次方,则成对数函数的关系!
3=log 以2为底数,8为真数的对数函数,2一般不写出来,则写作log8
所以二分查找的时间复杂度为O(logN)
(6):O(N)(阶乘递归)
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
这里运用了函数的递归思想
Factorial(9)*10
Factorial(8)*9
......
Factorial(1)*2
函数总共递归调用了十次,也就是N次,为1*2*3*4*5*6*7*8*9*10
每一次递归调用了O(1)次
则答案为O(N)
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度,算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
(1):O(1)(冒泡排序)
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;
}
}
这里总共定义了五个参数,分别是两个形参int* a,int n,还有三个实参size_t end = n,int exchange = 0,size_t i = 1
虽然进行了for循环,是在循环内部重复定义了N次,但是这里重复使用的却都是同一块栈区的空间,循环结束一次会销毁一次,所以总共算是定义了五个变量,为常数。。
则答案为O(1)
(2):O(1)(N)(阶乘递归)
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N-1)*N;
}
这里递归调用了N层,每次调用建立一个栈帧,每个栈帧使用了常数个空间,为O(1)
调用时,建立栈帧,返回时,销毁栈帧,总共进行了N次,每次为O(1)
则答案为O(N)
(3):O(1)(N)(斐波那契数列)
long long* Fibonacci(size_t n)
{
if(n==0)
return NULL;
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 ] = fibArray[ i - 1] + fibArray [i - 2];
}
return fibArray ;
}
斐波那契数列,又称“兔子数列”,其数值为:1、1、2、3、5、8、13、21、34……,就是每个数等于前两个数之和
这里 long long * fibArray =(long long *)malloc((n+1) * sizeof(long long))开辟了N+1个变量,加上其他的5个就是N+6个变量,把6省略
则答案为O(N)