一、什么是数据结构
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
二、什么是算法
算法(Algorithm)是定义良好的计算过程,它取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
三、算法的效率
我们会通过复杂度去衡量一个算法的好坏。算法在编写成可执行程序后,运行时需要耗费**时间资源和空间(内存)资源 **。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
四、时间复杂度
4.1 时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
void Func1(int N) { int count = 0; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { ++count; } } //N*N for (int k = 0; k < 2 * N; ++k) { ++count; } //2*N int M = 10; while (M--) { ++count; } //10 printf("%d\n", count); }
(1)时间复杂度表达式为:F(N)=N*N+2*N+10
(2)大O渐进表示法:O(N^2)
这个函数的基本操作次数是:F(N)=N*N+2*N+10,随着N的增大,后两项对整个结果的影响可以忽略不计
4.2 大O渐进表示法
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们来学习大O的渐进表示法
大O符号:是用于描述函数渐进行为的数学符号。
推导大O阶方法:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且系数不为1,则去除这个项的系数
另外有些算法的时间复杂度存在最好、平均和最坏情况:
- 最坏情况:
任意输入规模的最大运行次数(上界) - 平均情况:
任意输入规模的期望运行次数 - 最好情况:
任意输入规模的最小运行次数(下界)
说明:在实际中一般情况关注的是算法的最坏运行情况。
4.2 常见时间复杂度计算
冒泡排序:
// 计算BubbleSort的时间复杂度? void BubbleSort(int* a, int n) { assert(a); for (int end = n; end > 0; --end) { int flag = 0; for (int i = 1; i < end; ++i) { if (a[i - 1] > a[i]) { Swap(&a[i - 1], &a[i]); flag = 1; } } if (flag == 0) break; } }
最好情况O(N)
最好情况就是数组本身有序,虽然它有序,但是计算机最初并不知道它是有序的,仍需要遍历一遍数组才能知道它是有序的,所以就好情况就是O(N)。
最坏情况O(N^2)
最坏情况是数组完全逆序,则第一趟需要交换N − 1 次,第二趟需要交换N − 2次…直到最后一趟只交换一次,把所有的交换次数加起来就得到了冒泡排序最坏情况下的时间复杂度,其实也就是一个等差数列求和,所以最会情况下的时间复杂度是O(N^2)
二分查找:
// 计算BinarySearch的时间复杂度? int BinarySearch(int* a, int n, int x) { assert(a); int left = 0; int right = n - 1; while (left <= right) { int mid = left + ((right - left) >> 1); if (a[mid] < x) left = mid + 1; else if (a[mid] > x) right = mid - 1; else return mid; } return -1; }
最好情况O(1)
最好情况是第一次查找就找到目标值,此时时间复杂度就是O(1)。
最坏情况O(long2N)
二分查找每次可以排出一半的数据,就坏的情况就是排出到只剩下一个数据。当N/2/2/2/2……/2=1时,就找到了目标值。除去了几个2就是执行的次数,所以时间复杂度为O(log2N)。
O(N)和O(log2N)的对比:
N | 1000 | 100W | 10亿 |
O(N) | 1000 | 100W | 10亿 |
O(log2N) | 10 | 20 | 30 |
由此我们看到O(log2N)相对O(N)在效率上有很大的提升,但二分查找有一个限制条件就是数组必须有序,所以在实际中二分查找应用并不多。
递归阶乘:
// 计算阶乘递归Fac的时间复杂度? long long Fac(size_t N) { if (0 == N) return 1; return Fac(N - 1) * N; }
时间复杂度: O(N)
Fac一共被递归调用了N+1次,且每次Fac中执行1次,总共执行N+1次,所以时间复杂度是O(N)。
斐波那契数列:
// 计算斐波那契递归Fib的时间复杂度? long long Fib(size_t N) { if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); }
时间复杂度: O(2^N)
斐波那契数列,它的时间复杂度是等比数列求和,所以时间复杂度为O(2^N)。
4.3 例题:消失的数字
方法一:我们可以把0~N个数字全部加起来,减去数组中的元素,结果就是消失的数字。
时间复杂度为O(N)
int missingNumber(int* nums, int numsSize) { int i=0; int ret=(numsSize+1)*numsSize/2; for(i=0;i<numsSize;i++) { ret-=nums[i]; } return ret; }
方法二: 我们可以使用异或,异或的条件是相同为0,相异为1。两个相同的数异或为0,0和任何数异或都为原数,所以我们将0~N与数组中的所有异或,得到的结果就是消失的数。
int missingNumber(int* nums, int numsSize){ int i = 0,k = 0; for(i=0;i<numsSize;i++) { k^=nums[i]; } for(i=0;i<numsSize+1;i++) { k^=i; } return k; }
五、空间复杂度
空间复杂度的概念:
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。 空间复杂度不是程序占用了多少bytes的空间,而算的是变量的个数。 空间复杂度计算规则也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
一般常见的空间复杂度都是O(1)或者O(N)(额外开辟数组)。
5.1 空间复杂度计算
冒泡排序:
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),临时占用储存空间的变量有i、exchange、end,一共三个。
递归阶乘:
// 计算阶乘递归Fac的空间复杂度? long long Fac(size_t N) { if(N == 0) return 1; return Fac(N-1)*N; }
空间复杂度为O(N),函数的每一次调用都会开辟一个栈帧,每个栈帧开常数个空间,开辟N+1个栈帧,空间复杂度就为O(N)。
注意: 递归程序最大的问题就是深度太深,会有栈溢出的风险。
斐波那契数列:
long long Fib(size_t N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
空间复杂度为O(N)
空间是可以重复利用的,递归的调用是按一条线递归下去的,不会同时递归,当递归到最后一层返回时,创建的函数栈帧销毁,调用另一条仍可以使用这块空间,所以空间复杂度是O(N)。
5.2 例题:轮转数组
方法一:右旋K次(每次右旋一次)【时间复杂度为O(N*K);空间复杂度为O(1)】(时间复杂度的k最大为N-1),最坏的结果为O(N^2))
方法二:开设一个新的数组,把后k个放到新数组的前面,前面的依次放到后面【时间复杂度O(N),空间复杂度O(N)】 (这种方法比第一种,就是时间换取空间的算法)
方法三:三趟逆置【时间复杂度为O(N),空间复杂度为O(1)】
void reverse(int*nums,int* left,int* right) { int tmp=0; while(left<=right) { tmp=*left; *left=*right; *right=tmp; left++; right--; } } void rotate(int* nums, int numsSize, int k) { k%=numsSize; int* p=nums; reverse(nums,p,p+numsSize-k-1); reverse(nums,p+numsSize-k,p+numsSize-1); reverse(nums,p,p+numsSize-1); }
本次的内容到这里就结束啦。希望大家阅读完可以有所收获,同时也感谢各位铁汁们的支持。文章有任何问题可以在评论区留言,小羊一定认真修改,写出更好的文章~~