数据结构与算法
什么是数据结构?
简单来说,在内存中存储管理数据的结构,即数据结构。
数据结构与数据库的区别?
本质上都是存储管理数据.
但数据结构 —— 在内存中存储管理数据.
数据库 —— 在磁盘中存储管理数据.
什么是算法?
按要求对数据进行某种处理的过程.如查找、排序,不同算法实现过程不同.
数据结构和算法是不分家的.
复杂度
如何衡量算法的好坏?
某个算法在运行时,需要消耗时间和占用空间;
因此用时间复杂度(描述算法运行时间) 和 空间复杂度(描述算法额外开辟的空间大小)来衡量.
时间复杂度
功能:衡量一个算法的运行快慢.
定义:时间复杂度实际是一个函数,描述运行时间,同时也是算法中基本操作的执行次数.
问题:如何描述运行时间?
误区:用机器上的执行时间描述?
同一个算法,在不同的机器上运行耗费的时间一般不相同.
例:
cpu:i7,16G内存的机器1
cpu:i3,2G内存的机器2
显然机器1运行速度更快
结论:同一个算法的运行时间和硬件配置有关,无法算出准确时间.
正确方式:
一个算法花费的时间与语句的执行次数成正比例,
即语句的执行次数越多,算法花费的时间自然越多.例:
int func(int N)
{
int row = 0;
int col = 0;
int ret = 0;
for (row = 0; row < N; row++)
{
for (col = 0; col < N; col++)
{
ret++; // 该语句将执行N*N次
}
}
for (row = 0; row < 3 * N; row++)
{
ret++; // 该语句将执行3*N次
}
return ret;
}
通过计算可以算出较准确的时间复杂度函数式:F(N) = N*N+3×N+4;
但是准确的时间复杂度函数式,不方便在算法之间进行比较,于是引出了大O的渐进表示法.
大O的渐进表示法
例:F(N) = N*N+3×N+4.
随着N的增大,后两项对结果影响几乎可以忽略不计。
所以用大O渐进法表示的时间复杂度是O(N^2).
如何表示大O的渐进表示法?
用 1 取代【准确的函数式中】所有的常数;
在修改后的运行次数函数中,只保留最高次项.
例题一:
void func2(int N, int M)
{
//较准确的时间复杂度函数式为2*N+M+100
int i = 0;//用于遍历循环
for (i = 0; i < 2*N; i++)
{
printf("xxx\n"); //2*N次
}
for (i = 0; i < M; i++)
{
printf("kkk\n");//M次
}
for (i = 0; i < 100; i++)
{
printf("lll\n");//100次
}
}
使用大O的渐进表示法为O(N+M),有2个未知数。
准确说:
N远大于M时,O(N);
M远大于N,O(M);
M近似N,O(M)和O(N)都行
注意1:O(1)并不是表示一次,而是表示常数次。例:
//这里的时间复杂度是O(1)
void func(void)
{
int i = 0;
for (i = 0; i < 100; i++)
{
printf("kkk\n");//100次
}
}
注意2:算时间复杂度不能去数循环层次,不一定准确,一定要看它的算法思想!!!例:
void bubbleSort(int* arr, int num)
{
int i = 0;//冒泡排序的趟数,每一趟都能成功排序好一个数,所以只需排num-1次
int k = 0;//一趟冒泡排序需要比较的次数
int flag = 1;//假设一趟下来没有交换任何数
for (i = 0; i < num - 1; i++)
{
for (k = 0; k < num - 1 - i; k++)//一趟冒泡排序的比较次数与i有关
{
if (arr[k] > arr[k + 1])//交换
{
int tem = arr[k];
arr[k] = arr[k + 1];
arr[k + 1] = tem;
flag = 0;
}
}
if (flag == 1)//一趟下来没有交换任何数,说明数组已经有序
return;
}
}
首先外层循环执行num - 1次,内层循环执行的次数与i有关.
内层循环执行的总次数的函数式,就是该算法的时间复杂度.
| i | 内层循环执行次数 |
|---|---|
| 0 | num-1 |
| 1 | num-2 |
| 2 | num-3 |
| 3 | num-4 |
| ... | ..... |
| num-2 | 1 |
那总次数其实就是 1 + 2 + ... + num-1,根据等差公式,总次数是:
所以冒泡排序的时间复杂度是O(N^2).
注意3:有些算法的时间复杂度存在最好、平均和最坏三种情况。
以冒泡排序为例,
最好情况:第一趟冒泡排序下来没有交换任何数,说明已经有序;
最坏情况:要排序的数组元素是倒序的,要进行num-1趟才能排成有序.
在实际中,关注的是最坏的运行情况,所以时间复杂度是O(N^2).
注意4:时间复杂度是计算算法执行次数。
一个执行次数,不一定是一条语句,可能是多条语句,即常数条语句。例:
for (i = 0; i < N; i++)
{
printf("kkk\n");
printf("kkk\n");//该循环中的语句是常数条,时间复杂度的执行次数仍为N
printf("kkk\n");
}
实例演示1:计算阶乘的时间复杂度
//时间复杂度是O(N),因为每次递归内部的执行次数都是常数次,
//要执行N次递归
int fac(int N)
{
if (N == 1)
return 1;
return N * fac(N - 1);
}
实例演示2:计算斐波那契递归的时间复杂度
//时间复杂度是O(2^n)
long long fib(int 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) - 1;
时间复杂度是O(2^n).
空间复杂度
空间复杂度也是一个数学函数式,表示一个算法临时占用存储空间的大小.
注意1:空间复杂度不是程序占用了多少byte的空间,而是算变量的个数,
也使用大O的渐进表示法。 例:
int max(int a, int b)
{
int re = a > b ? a : b;//这里额外创建的变量只有常数个,所以空间复杂度是O(1)
return re;
}
注意2:空间复杂度是算因为算法过程额外创建的变量数。例:
void print(int* a, int num)
{
//空间复杂度是O(1),因为外面提供的数组大小不算
int i = 0;
for (i = 0; i < num; i++)
{
printf("%d ", a[i]);//打印数组中的所有数
}
}
注意3:时间是累计的,但空间不会累积,是可以重复利用的。例:
void test(void)
{
int i = 0;
for (i = 0; i < 4; i++)
{
int k = 0;//在循环中定义一个变量
printf("%p\n", &k);
}
}
运行结果:
在循环中定义的变量,每次创建的地址都是相同的,利用的都是同一块空间,
不会在其它地方再开辟一块空间,因此空间复杂度仍为O(1).
实例演示1:计算阶乘递归的空间复杂度:
int fac(int N)
{
if (N == 1)
return 1;
return N * fac(N - 1);
}
每次递归都要建立栈帧,这些栈帧都是因为该算法额外开的空间。
因为要递归N次,每次递归内部额外创建的变量为常数,所以空间复杂度是O(N).
实例演示2:计算斐波那契递归的空间复杂度:
long long fib(int n)
{
if (n < 3)
return 1;
return fib(n - 1) + fib(n - 2);
}
它需要进行很多趟递归。但是,时间是累积的,而空间是不累计的,可以重复利用.
对于该算法,fib(N)不会同时调用fib(N-1)和fib(N-2),它是先调用左边进行递归。
同理,fib(N-1)也会先调用fib(N-2)递归……
到后面,fib(3)会先调用fib(2)创建栈帧,fib(2)返回1栈帧销毁,
然后fib(3)再调用fib(1)创建栈帧,但这块栈帧和fib(2)的栈帧共用一块空间。
因此它的空间复杂度是O(N).