目录
1.什么是数据结构
2.怎么学好数据结构
3.算法效率(复杂度)
4.时间复杂度
4.1时间复杂度的定义:
4.2时间复杂度的表示方法和规则——(大O的渐进表示法)
4.3常见的时间复杂度计算
5.空间复杂度
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的 数据元素的集合。
换言之:数据结构就是,在内存中,以某种方式对数据进行增删查改
1.说白了数据结构就是一种算法,是要解决问题的,并且这个算法,越高效越好,运行时间越短越
好。
2.在敲经典的算法过程中,需要注意思考和画图
对于一个算法而言,怎样去衡阳好坏呢?是代码行数短吗?还是其他的原因?
其实衡量代码的好坏,只有两个考察因素,运行的时间够不够短,占用的内存够不够小,
这两点就对应了时间复杂度和空间复杂度,
由于计算机硬件的告诉发展,计算机的运行内存已经非常大了,所以在考虑复杂度方面,我们更多
是考虑时间复杂度,毕竟越快的运行出来的代码,大概率是最好的。
在计算机科学中,算法的时间复杂度是一个函数(指的是数学上的函数),它定量描述了该
算法的运行时间。
一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起
来,才能知道。
但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间
复杂度这个分析方式。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为
算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执
行次数,那么这里我们使用大O的渐进表示法。
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
规则1、用常数1取代运行时间中的所有加法常数。
规则2、在修改后的运行次数函数中,只保留最高阶项。
规则3、如果最高阶项存在且系数不是1,则把最高阶项的系数置为1。得到的结果就是大O阶。
大O阶的渐进表示的总体思想就是:
极限思想,当x--->+无穷大的时候,对表达式f(x)影响最大的那一项叫做大O阶。
例子:
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < 2 * N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
假设时间复杂度函数为F(N);
那么,F(N) = 2 * N * N + 2 * N + 10
化简:
根据规则1;F(N) = 2 * N * N + 2 * N
根据规则2;F(N) = 2 * N * N
根据规则3;F(N) = N * N
那么可得时间复杂度为:O(N*N)
特殊情况:
有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
也就是说,实际情况中,要做到兜底的作用,要把所有的情况都考虑到的那种时间复杂度才是我们想要的时间复杂度,或者说时间复杂度至少要包含我们能够考虑到的所有情况。
对于自己想实现的功能的代码的时间复杂度,我们要精进到这种地步,就是在脑子里想到如何实现
这个算法的时候,就能够大致估算出自己想写代码的时间复杂度。
实例1:
// 计算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
//根据规则1,规则2,规则3
//时间复杂度:O(N)
实例2:
// 计算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);
}
//时间复杂度:O(M+N)
实例3:
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
//F(N) = 100;
//根据规则1
//时间复杂度为:O(1)
//表示只要进行常数次的执行
实例4:
// 计算strchr的时间复杂度?
const char * strchr ( const char * str, int character );
分析:
函数strchr表示的是在某一个字符串里面找某一个字符,如果找到了就返回这个字符的地址。
最好情况:一次找到
最坏情况:找遍了这个字符串都找不到
假设字符串长度是N,
那么最坏情况下对应的时间复杂度是:O(N)
总结:
最终考虑的时间复杂度是:O(N)
实例5:
// 计算冒泡排序的时间复杂度?
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;
}
}
答案:O(N*N)
分析:
对于一个元素为N个的数组
第一趟冒泡排序要排N-1次
第二趟冒泡排序要排N-2次
第三趟冒泡排序要排N-3次
............
第N-1趟冒泡排序要排1次
一趟可以排好一个数,最后一趟可以排好两个数,因此只要排N-1趟
所以:F(N) = 1 + 2 + 3 + ...... + N - 1 = N * N / 2
所以,时间复杂度为;O(N*N)
实例6:
// 计算二分查找的时间复杂度?
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;
}
N/2/2/2/2/2/..../2 = 1 每除一次2就代表找了一次。假设找了X次后,找到了
N =1*2*2*2*......2 = 2^X
X=Log(N)
也就是说时间复杂度为:O(logN)
ps:logN在算法分析中表示是底数为2,对数为N。有些地方会写成lgN。
实例7:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
分析;
时间复杂度为:O(N)
实例7变式:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if (0 == N)
return 1;
for (int i = 0; i < N; i++)
{
//...
}
return Fac(N - 1) * N;
}
时间复杂度是:O(N*N)
实例8:(难点)
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
时间复杂度:O(2^N)
空间复杂度:O(N)
从Fib(N)到Fib(2),每一次调用都会开辟一次新的空间,由于C语言是顺序执行的程序,会沿着时间线一直单向走下去的。因此Fib(N)到Fib(2)的空间复杂度的量级是N次
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,
所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定
好了,
因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
空间复杂度指的变量是,为了实现算法,而额外开辟的变量,并不是保持函数基本运行使用的变量。
换句话说,要计算空间复杂度的变量,是我为了实现我想要的功能而创建的变量。我们只针对这些变量去计算空间复杂度。
实例1:
// 计算Func4的空间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
//k和count是我为了实现要求创建的变量
//并且他们的次数都是常数次
//因此
//空间复杂度是:O(1)
实例2:
// 计算BubbleSort的空间复杂度?
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;
}
}
空间复杂度:O(1)
总共创建了3个变量,分别是 end exchange i
对于重复创建的exchange是算一个
实例3
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前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;
}
malloc开辟了n+1元素,也就是说开辟n+1个变量
因此空间复杂度是O(N)
空间复杂度要么是O(1)
要么是O(N)
O(N)的情况一般是对应于动态开辟N个量级的元素而言
一般来说,空间复杂度除了上述的两个状态(O(1)和O(N)),很好会出现其他的取值
实例4;
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
//空间复杂度是O(N)