文章目录
基本概念和术语
1.数据 :所有能输入到计算机中去的描述客观事物的符号。对于计算机科学而言,数据的含义极为广泛,如图像、声音等都可以通过编码归之于数据的范围。
2.数据元素 :是数据的基本单位,也称节点,记录。
3. 数据项 :是数据的不可分割的最小单位,也称域
4. 数据对象 :相同特性数据元素的集合,是数据的一个子集。
5. 数据结构 :是相互之间存在一种或者多种待定关系的数据元素的集合。也就是计算机存储,组织数据的方式。
6. 结构 :数据元素相互之间存在的关系
称为结构。
7. 算法 :就是一系列的计算步骤,用来将输入数据转化成想要的输出结果。
前三个之间的关系如下:
数据 > 数据元素 > 数据项
例如:学生表 > 个人记录 > 姓名
复杂度
算法在编写成可执行程序后,运行时需要消耗时间资源和空间内存资源,因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
1.时间复杂度
1.1时间复杂度的概念
在计算机科学中,算法的时间复杂度是一个函数F(N)。它定量描述了改算法的运行时间。一个算法的运行时间,是算不出来的,因为计算机的处理器处理这些算法是非常非常迅速的,只有把程序跑起来才能知道它所运行的时间。如果把每个程序都跑一遍看运行时间是很麻烦的。因此 有了时间复杂度这个分析方式。一个算法所花费的时间与其语句中的执行次数成正比,算法中基本操作的次数,就为算法的时间复杂度。
也就是找到某条基本语句与问题规模N之间的数学表达式, 就是算出了该算法的时间复杂度。
1.2大O渐进表示法
在实际进行计算时间复杂度时,不需要计算精确的执行次数,因为当N执行的次数无限大时,其余的项或者系数也就无所谓了,因此我们要使用大O渐进表示法求出大概执行次数也就是时间复杂度了。
大O渐进法的规则如下:当一个时间复杂度F(N)既有高次项,又有低次项,又有常数项时,只保留最高此项,若高次项含有系数,则同样把系数去除;当F(N)只有常数项时,直接用1来代替。
例如:
F(N) = 5 * N2 +8 * N+100
去除掉系数和低次项之后,所得到的就是时间复杂度O(N2)
F(N) = 10000
它只有常数项,用1来代替,则时间复杂度为O(1)
1.3常见的时间复杂度计算举例
O(1)
void fun1(int n)
{
int a = 0;
for(int i = 0; i < 100; i ++)
a ++;
printf("%d\n",a);
}
在这个程序中,for语句循环执行了100次,即F(N)=100,由大O渐进法的规则我们可以得出它的时间复杂度为O(1)。
O(N)
long long fac(int n)
{
if(n == 0)
return 1;
return fac(n - 1) * n;
}
这是一个阶层递归的程序,想要计算它的时间复杂度,我们就要知道它每递归调用一次,执行了程序几次。即递归的时间复杂度就是所有递归调用次数累加。
总共调用n+1次,则总共执行程序次数为F(N) = N+1,则时间复杂度为O(N)。
O(M+N)
void fun2(int n, int m)
{
int a = 0;
for(int i = 0; i < n; i ++)
a ++;
for(int i = 0; i < m; i ++)
a ++;
printf("%d\n",a);
}
在这个程序中,m和n作为未知数,则它循环的次数就是m+n次,时间复杂度也保留这两个未知数即可,即为O(m+n)。
O(N2)
void BubbleSort(int* a, int n)
{
assert(a);
for(int i = 0; i < n; i++)
{
int exchange = 0; //定义一个exchange
for(int j = 0; j < n - i - 1; j++)
{
if(a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
exchange = 1; //如果进行了排序则变为1
}
}
if(exchange == 0)//如果这一趟没有进行排序,则exchange的值还是0 就证明数组已经有序,跳出循环。
break;
}
}
这个程序就是很经典的冒泡排序。
对于排序,我们要分情况进行讨论,当数组本身就是有序时,我们不需要排序,但是我们只有从头遍历了一遍才知道它是有序的,所以最好情况它的时间复杂度就是O(N)。
当数组是倒序时,则是排序的最坏情况了,它需要不断遍历排序,第一趟排序需要交换n-1次,第二趟需要交换n-2次,依次类推,到最后交换一次。
此时计算程序运行次数则是将每一趟排序的次数加起来即可:(n-1) + (n-2) + ……+ 2 + 1 = ((n-1)+1)*(n-1)/2=(n2-n)/2。即F(N) = (n2-n)/2,则时间复杂度为O(N2)。
这里我们可以看到冒泡排序有两个时间复杂度,一个是O(N),一个是O(N2),在实际中一般关注的是算法的最坏运行情况
,所以冒泡排序的时间复杂度应该为O(N2)。
这里补充几点关于算法的时间复杂度的最好、最坏以及平均情况:
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
O(2N)
long long Fib(int n)
{
if(n < 3)
return 1;
return Fib(n-1) + Fib(n-2);
}
这个程序也是经典的斐波那契数列问题,我们要记得,计算递归的时间复杂度,需要将所有递归调用次数累加。
所以累计调用次数则为:20+21+22+……+2(n-2),用大O渐进表示法表示时间复杂度则为O(2N),这个的时间复杂度非常的慢,不建议使用。
O(log N)
int BinarySearch(int* a, int n, int x)
{
int left = 0;
int right = n-1;
while(left <= right)
{
int mid = (left + right)/2;
if(a[mid] > x)
right = mid - 1;
else if(a[mid] < x)
left = mid + 1;
else
return mid;
}
return -1;
}
这是经典的二分查找算法,当我们在查找时,被查找区间会一半一半的缩小。我们算时间复杂度一般要算最坏的情况,在二分查找中最坏情况就是查找区间只剩一个数且是要找的数字,或者说根本找不到,这就是最坏的情况。
即查找区间的变化是:
n -> n/2 -> n/2/2 -> n/2/2/2 ->…-> 1
所以说,我们查找了多少次,就是除了多少个2.
即N/2/2…/2 = 1
假设查找了x次,就是2x=N;
所以x=log2N,为了书写方便,一般将下标2省略,即时间复杂度为O(logN)。
二分查找的查找效率非常高,当你想要在10亿个数字中查找一个数字,则只需要查找30次即可,虽然它的效率非常高,但是它在实际中不太实用,因为它不方便插入删除等操作。
时间复杂度按数量级递增顺序为:
2.空间复杂度
2.1空间复杂度的概念
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间的量度。
空间复杂度算的是变量个数,它的计算与时间复杂度相似,也是用大O渐进表示法。
注意:
函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了。因此空间复杂度主要通过函数在运行时候显示申请的额外空间来确定。
如今计算机行业发展迅速,计算机的存储容量已经达到了很高的一个程度,所以有时我们也不太关注空间复杂度。
2.2常见的空间复杂度举例
O(1)
void BubbleSort(int* a, int n)
{
assert(a);
for(int i = 0; i < n; i++)
{
int exchange = 0; //定义一个exchange
for(int j = 0; j < n - i - 1; j++)
{
if(a[j] > a[j + 1])
{
int tmp = a[j];
a[j] = a[j + 1];
a[j + 1] = tmp;
exchange = 1; //如果进行了排序则变为1
}
}
if(exchange == 0)//如果这一趟没有进行排序,则exchange的值还是0 就证明数组已经有序,跳出循环。
break;
}
}
用冒泡排序举例,它临时创建了3个变量,即exchange,i,tmp,因此空间复杂度为O(1)。
O(N)
long long fac(int n)
{
if(n == 0)
return 1;
return fac(n - 1) * n;
}
阶层递归的空间复杂度,函数递归调用一次就会开辟一个空间,该函数递归调用了N+1次,用大O渐进表示法则空间复杂度为O(N)。
long long Fib(int n)
{
if(n < 3)
return 1;
return Fib(n-1) + Fib(n-2);
}
Fib(n)在递归调用时一直调用的是原来的空间,从左边的Fib(n)到Fib(1)会一直调用n个栈帧,后面的函数调用会一直使用原来的空间,所以它的空间复杂度为O(N)。
今天的内容到此结束啦,感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。