前言
什么是数据结构?
数据结构(Data Structure):是计算机储存组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合
什么是算法?算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一共或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化为输出结果(算法=你让计算机所做的操作)
1.时间复杂度的定义
在计算机科学中,算法的时间复杂度是一个函数,它定量描述了算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比列,算法中的基本操作的执行次数,为算法的时间复杂度
例1:
计算Func1中++count执行的次数
void Func1(int N)
{
int count = 0;
for(int i = 0; i < N; ++i)
{
for(int j = 0; j < N; ++j)
{
++count;
}
}
for(int i = 0; i < 2 * N; ++i)
{
++count;
}
int M = 10;
while(M--)
{
++count;
}
printf("%d\n", count);
}
Func1的基本操作次数:F(N) = N^2 + 2 * N + 10来分析一下是为什么?
首先可以看到这段代码有三个循环
第一个是由两个for内外嵌套组成:每次循环N次,执行了N次,即N + N + N.....=N * N = N^2 次
第二个循环执行了 2*N 次
第三个循环执行了 10 次
如果每个时间复杂度都要这么表示的话那太复杂了,所以我们只取最大量级来表示这段代码的时间复杂度
当N = 10时:F(N) = 130
当N = 20时:F(N) = 10210
当N = 30时:F(N) = 1002010
当我们的N取无穷大时 2 * N + 10这两个项对结果的影响已经不大了可以忽略不计,所以说只需要取N^2来表示它的时间复杂度就可以了
所以这段代码Func1的时间复杂度为: O(N ^ 2)
2.大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号
推导大O阶方法:
(1).用常数1来取代运行时间中的所有加法常数
(2).在修改后的运行次数的函数中,只保留最高阶项
(3).如果最高阶存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶
通过上面一个例子我们可以发现大O渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数
我们来计算几道代码的时间复杂度
例1:
void Func2(int N)
{
int count = 0;
for(int i = 0; i < 2 * N; i++)
{
++count;
}
int M = 10;
while(M--)
{
++count;
}
printf("%d", count);
}
F(N) = 2 * N +10
去掉与最高阶相乘的常熟和10使用大O渐进法表示法该段代码的时间复杂度为:O(N)
例2:
void Func3(int M, int N)
{
int count = 0;
for(int i = 0; i < M; i++)
{
++count;
}
for(int j = 0; j < N; j++)
{
++count;
}
printf("%d\n", count);
}
使用大O渐进法表示法该段代码的时间复杂度为:O(N + M)
因为M和N是未知的所以不能去掉它们两个任意一个
如果N大于M,则可以去掉M,反之可以去掉N,相等可任取M和N中任何一个
例3:
void Func4(int N)
{
int count = 0;
for(i = 0; i < 100: i++)
{
++count;
}
printf("%d\n", count);
}
F(N) = 100
执行了100次,但是我们用1来表示
使用大O渐进法表示法该段代码的时间复杂度为:O(1)
注:这里的1表示代表1次,而是常数次
3.时间复杂度的最好,最坏和平均情况
另外有些算法的时间复杂度存在最好,平均,最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模最小运行次数(下界)
例4:
char* strchr(const char * str, int character)
{
while(*Str)
{
if(*str == character)
{
return str;
}
str++;
}
return NULL;
}
例如:在一个长度为N的数组中找一个数据x
最好情况:1次找到
平均情况:N/2次找到
最坏情况:N次找到
在实际情况中一般关注的是算法的最坏运行情况,所以该段代码的时间复杂度为:O(N)
例5:
void BubbleSort(int *a, int n)
{
assert(a);
for(int end = n; end > 1; --end)
{
for(int i = 1; i < end; i++)
{
if(a[i - 1] > a[i])
{
int tmp = a[i];
a[i] = a[i + 1];
a[i + 1] = tmp;
}
}
}
}
最好情况:O(N)
最坏情况将两个for循环跑满
外循环为n时,内循环循环n - 1次 然后按顺序n - 2, n-3, ....., 3, 2, 1通过判断可以知道这是一个等差数列,所以它的总和就为:n(n - 1 + 1)/2 = n^2*1/2 即最坏情况:O(N^2)
使用大O渐进法表示法去掉常数该段代码的时间复杂度为:O(N^2)
例6:
在数组有序的情况下:可以使用二分法(折半查找)
int binarysearch(int *a,int n, int x)
{
int begin = 0;
int end = n - 1;
while(begin <= end)
{
int mid = begin + ((end - begin)>>1);
if(a[mid] > x)
{
end = a[mid] - 1;
}
else if(a[mid] < x)
{
begin = a[mid] + 1;
}
else
{
return mid;
}
}
return -1;
}
最好情况:O(1)
最坏情况:区间缩放到一个值,要么找到,要么找不到,假设N为数组个数,x是最坏查找次数N每次除2就等于查找一次,折半查找多少次就除多少个2
N/2/2/2..../2 = 1, 因为n为int所以最小二分到1,2^x = N 即:x = logN(log在时间复杂度中表示以2为底)所以最坏情况:O(logN)
例7:
long long fac(size_t N)
{
if(N == 0)
return 1;
else
return fac(N - 1) * N;
}
使用大O渐进法表示法该段代码的时间复杂度为:O(N)
例8:
long long Fib(int n)
{
if(n < 3)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
最好情况:O(1)
可以观察到该递归的方式为等差数列我们用求和公式可以得出:2^(N-1)-1
最坏情况用大O渐进表示法:O(2^N)
总结以上时间复杂度:O(1)>O(logN)>O(N)>O(N^2)>O(N^3)>O(2*N)
4.空间复杂度的定义
算法在临时占用储存空间大小的量度(就是完成这个算法所额外开辟的空间),空间复杂度也使用大O渐进表示法来表示
注: 函数在运行时所需要的栈空间(储存参数,局部变量,一些寄存器信息等)在编译期间就已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
例1:
int* fib(int n)
{
int* fibarray = (int*)malloc(sizeof(int) * (n));
if (n == 0)
{
return NULL;
}
fibarray[0] = 1;
fibarray[1] = 1;
for (int i = 2; i < n; i++)
{
fibarray[i] = fibarray[i - 1] + fibarray[i - 2];
}
return fibarray;
}
怎么查看这段代码的空间复杂度:可以观察到该段代码为了完成这个算法额外申请了n个空间,即空间复杂度:O(n)
例2:
void Bubble_Sort(int a[], int n)
{
assert(a);
int exchange = 0;
for (int end = n; end > 1; --end)
{
for (int begin = 1; begin < end; ++begin)
{
if (a[begin] < a[begin - 1])
{
Swap(&a[begin], &a[begin - 1]);
exchange = 1;
}
}
if (!exchange)
{
break;
}
}
}
可以观察到这段代码完成这个算法额外开辟了三个空间 即空间复杂度:O(1)
例3:
int fac(int N)
{
if(N == 0)
{
return 1;
}
return fac(N-1)*N;
}
这段递归的空间复杂度为多少了
注:每次递归都是开辟新的空间来进行操作
这段代码从Fac(N) 开始 Fac(N - 1),Fac(N - 2).....Fac(1),Fac(0),所以递归了N次
该算法完成递归额外开辟了N个空间:O(N)
我们来看一段代码
void F1()
{
int b = 0;
printf("%p ", &b);
}
void F2()
{
int a = 0;
printf("%p ", &a);
}
int main()
{
F1();
F2();
return 0;
}
F1和F2是否使用的同一个空间
运行结果:
可以看到这两个不同函数中的变量居然是用的同一个地址
原因:因为mian函数中是先调用的F1,在F1执行完之后,F1所在的空间被系统收回了,即在执行F2时,F2也是用的这个地址,因此F2和F1所用的空间相同
例4:
long long Fib(int N)
{
if (N < 3)
{
return 1;
}
return Fib(N - 1) + Fib(N - 2);
}
这段代码的时间复杂度为:O(2^N)
递归时Fib(N) ——> Fib(N - 1) 和 Fib(N - 2),然后编译器是先将Fib(N - 1)先递归完再进行Fib(N - 2)
递归到最后Fib(2) Fib(1)时先执行的是Fib(2),随后就是Fib(1),所以可以推断Fib(1)所用的空间是Fib(2)所释放的空间,所以Fib(1)和Fib(2)所用的空间是一样的所以跟着这个思路往上推,假如N = 5 Fib(5)就额外开辟了4个空间
如图:
一共额外开辟了4个空间
这段代码的时间复杂度;O(2^N) 时间一去不复返,这次不可能执行上一次的操作,不可重复利用
空间复杂度:O(N) 空间用了之后归还,可以重复利用