001 数据结构_时间复杂度与空间复杂度——“C”
一、时间复杂度
(1) 什么是时间复杂度
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
引入
大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。
原理:去除对结果影响不大的项
1.用常数1取代运行时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶
时间复杂度的最好、平均、最坏情况
在实际中一般情况关注的是算法的最坏运行情况最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
所以数组中搜索数据时间复杂度为O(N)
二、时间复杂度例题
实例1
时间复杂度为O(N)
void Fanc2(int N)
{
int count = 0;
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
解析:程序一共被执行了2*N+10次,如果N很大,那么系数和加一个常数则是对结果影响不大的数
去掉系数和加法常数,剩余N
实例2
时间复杂度为O(1)
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
解析:k<一个常数,这个常数有多大,程序就进行多少次,但这个常数影响不大,代表常数次,可忽略不计
在程序里写出来的常数在CPU看来都一样
实例3
时间复杂度为O(N)
long long Fac(size_t N)
{
if (0 == N)
{
return 1;
}
return Fac(N - 1) * N;
}
解析:一共有N次递归,程序走了N次
实例4
时间复杂度为O(N^2)次
long long Fac(size_t N)
{
if (0 == N);
{
return 1;
}
for (size_t i = 0; i < N; ++i)
{
;
}
return Fac(N - 1) * N;
}
解析:每一次递归,一次递归有N次循环,第二次递归有N-1次循环....
全部加起来
1*N+1*(N-1)+1*(N-2)+1*(N-3)+...;去掉系数与常数
实例5:冒泡排序法
时间复杂度为O(N^2)
void Bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
int temp;
for (i = 0; i < sz - 1; i++) //i<sz-1 ->从小到大排列,每进行一次冒泡排序法就会先把最大值排到最后,i表示一共进行多少次冒泡排序法
{
for (j = 0; j < sz - 1 - i; j++) //一次冒泡排序法的交换次数
{
if (arr[j + 1] < arr[j]) //比较前后元素的大小,反序则交换,有序不变
{
temp = arr[j]; //利用赋值达到从小到大排列的效果
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return 0;
}
解析:假设有N个元素,一次冒泡排序法交换次数为sz-1-i 一共要进行sz-1次冒泡排序法
即程序要进行(sz-1-i)(sz-1)次
实例6:二分查找法
时间复杂度为logN
int mid_search(int arr[], int k, int sz) //定义二分查找函数
{
int left = 0; //定义数组元素最左侧下标为0
int right = sz - 1; //定义数组元素最右侧下标为总数-1
while (left <= right)
{
int mid = left + (right - left) / 2; //此表达式是防止int型溢出哦,也是求平均哈
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid; //找到了,返回下标
}
}
return -1; //过程全部走完了,没找到,返回-1;
}
解析:
时间复杂度要看最坏的情况,N个值,每找一次缩小一半, 最坏的情况
为N /2/2/2/2..../2=1 即N个数一直二分,二分查找找到最后
剩余两个数,想要查找的值为两个其中的一个数,只剩最后1个数即1
【/2/2/2/2..../2】=2^x
假设找了x次,每找一次除一个2,那么除了x个2 , 2^x*1=N
x=log N 即时间复杂度为log N 底数为2
常见的时间复杂度对比O(1) < O(logN) < O(N) < O(N^2)
二、空间复杂度
(1) 什么是空间复杂度
空间复杂度
1.空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时额外占用存储空间大小的量度
2.空间复杂度不是程序占用了多少bytes的空间,空间复杂度算的是变量的个数
注意:函数运行时所需要的栈空间(存储参数,局部变量,一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定
空间复杂度与时间复杂度的计算方法相同:
去除对结果影响不大的数
1.用常数1取代运行时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不是1,则去除与这个项目相乘的常数,得到的结果就是大O阶
(2) 引入
函数栈帧是什么
函数栈帧由以下几部分组成:
函数参数:函数在调用时传递的参数值,这些参数通常是存放在栈帧的开始处。
返回地址:指向调用该函数的指令的地址,用于在函数执行完毕后返回到调用者处继续执行。
帧指针(frame pointer):指向当前函数的栈帧的底部,用于在函数内部访问局部变量和参数。
局部变量:函数内部定义的变量,存放在栈帧中,并随着函数的执行而动态分配和释放。
临时变量:函数执行过程中产生的临时变量,通常也存储在栈帧中
在函数调用结束后,栈帧所占用的空间会被销毁,即栈指针会回到之前的位置。这个过程实际上就是将栈顶指针移动到先前的位置,从而释放掉该函数所占用的栈空间。这样做的目的是为了使下一次函数调用可以使用先前被释放的栈空间,从而提高栈空间的利用率。
因此,栈帧空间的销毁本质上就是回收并释放该函数所占用的栈空间
(3) 空间复杂度例题
实例1
斐波那契数列
long long Fib(size_t N)
{
if (N > 3)
{
return 1;
}
return Fib(N - 1) + Fib(N - 2);
}
解析如下
原理:
用等比数列求和计算个数约为2N个,时间复杂度是为O(2N)
。
先沿着左边一条竖直不断地走,左边Fib2的回归,随之栈帧销毁,再调用右边的Fib1,用同一个栈帧,这样观来,最多会建立0到N-2个栈帧,即N-1个
因此空间复杂度为O(N)
void Func1()
{
int a = 0;
printf("%p\n", &a);
}
void Func2()
{
int b = 0;
printf("%p\n", &b); //a,b两个变量开到同一个空间,地址是一样的
}
int main()
{
int a = 0;
printf("%p\n", &a); //两个a所在的栈帧不同
Func1();//调用func1和2相当于公用了同一块空间
Func2();
}
提出疑问:为什么Func1与Func2的地址相同呢
实例2
空间复杂度为O(1)
冒泡排序法
void Bubble_sort(int arr[], int sz)
{
int i = 0;
int j = 0;
int temp;
for (i = 0; i < sz - 1; i++) //i<sz-1 ->从小到大排列,每进行一次冒泡排序法就会先把最大值排到最后,i表示一共进行多少次冒泡排序法
{
for (j = 0; j < sz - 1 - i; j++) //一次冒泡排序法的交换次数
{
if (arr[j + 1] < arr[j]) //比较前后元素的大小,反序则交换,有序不变
{
temp = arr[j]; //利用赋值达到从小到大排列的效果
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
return 0;
}
解析:固定数量的额外空间,i,j等变量的数量都是常数个,不是n个,所以空间复杂度为O(1)个