数据结构
是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
算法
定义良好的计算过程,取一个或一组的值为输入,产生一个或一组值作为输出。简单来说就是一系列的计算步骤,用来将输入数据转化成输出结果。
算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
时间复杂度
定义:
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
实际中我们计算时间复杂度时,并不一定要计算精确的执行次数,只需要大概执行次数,这里使用大O的渐进表示法。
- 抓大头
- 取具有决定性结果的那一项
大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
推导大O阶方法:
- 用常数1取代运行时间中的所有加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数
时间复杂度分类
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
冒泡排序的时间复杂度
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-3次
等差数列
1
+
⋯
+
(
n
−
3
)
+
(
n
−
2
)
+
(
n
−
1
)
1+\dots+(n-3)+(n-2)+(n-1)
1+⋯+(n−3)+(n−2)+(n−1)
(
首项
+
尾项
)
∗
项数
2
=
(
1
+
n
−
1
)
∗
n
2
=
n
2
2
=
O
(
n
2
)
\begin{array}{} \frac{(首项+尾项)*项数}{2} \\ =\frac{(1+n-1)*n}{2} \\ =\frac{n^{2}}{2} \\ =O(n^{2}) \end{array}
2(首项+尾项)∗项数=2(1+n−1)∗n=2n2=O(n2)
时间复杂度,最好
O
(
n
2
)
O(n^{2})
O(n2),最差
O
(
n
)
O(n)
O(n)
二分查找的时间复杂度
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-1;
else
return mid;
}
return -1;
}
最坏情况下,除了多少次2,就查找了多少次
假设查找了x次
2
x
=
N
2^{x}=N
2x=N
x
=
log
2
N
x=\log_{2}N
x=log2N
最好时间复杂度
O
(
1
)
O(1)
O(1),最坏
O
(
log
N
)
O(\log N)
O(logN)
阶乘递归Fac的时间复杂度
long long Fac(size_t N)
{
if(0 == N)
return 1;
return Fac(N-1)*N;
}
有多次调用,0才返回的话,有n+1次调用
每次调用是
O
(
1
)
O(1)
O(1)
合计是累加起来,是
O
(
n
)
O(n)
O(n)
- 递归算法时间复杂度是多次调用的次数累加
斐波那契递归Fib的时间复杂度
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
是两路递归
2^0 Fib(N)
2^1 Fib(N-1) Fib(N-2)
2^2 Fib(N-2) Fib(N-3) Fib(N-3) Fib(N-4)
2^3 ...
... ...
Fib(3)
2^(N-1) Fib(2) Fib(1)
加起来,时间复杂度是 O ( 2 n ) O(2^{n}) O(2n)
空间复杂度
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度
空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大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;
}
}
传过来的指针,指向一个数组的空间,不算在内
要算的是额外开辟的空间
有end,exchang,i,3个
所以空间复杂度
O
(
1
)
O(1)
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;
}
malloc开辟了n+1个空间
空间复杂度就是
O
(
n
)
O(n)
O(n)
阶乘递归Fac的空间复杂度
long long Fac(size_t N)
{
if(N == 0)
return 1;
return Fac(N-1)*N;
}
要一直递归调用,到Fac(0)截止
Fac(N)
Fac(N-1)
Fac(N-2)
Fac(N-3)
...
Fac(1)
Fac(0)
递归调用会在栈帧开空间
每个栈帧是
O
(
1
)
O(1)
O(1)
n个栈帧,空间复杂度就是
O
(
n
)
O(n)
O(n)
斐波那契递归Fib的空间复杂度
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}
空间复杂度是
O
(
N
)
O(N)
O(N)
虽然是两路递归,时间是累计的,空间是可以重复利用的
从Fib(N)一直调用到Fib(1),累计建立了N层栈帧
之后栈帧就销毁了,之前的空间还可以重复利用,可以共用空间