数据结构是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。简单来说,数据结构就是对数据进行管理(增删查改)的一系列操作。数据结构分为线性数据结构和非线性数据结构。
算法 (Algorithm) 就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
数据结构和算法是相辅相成的,二者是我中有你、你中有我的关系:在一个数据结构中可能会用到算法来优化,一个算法中也可能用到数据结构来组织数据。
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。
时间复杂度主要衡量的是一个算法的运行速度,通常由循环来决定的,而空间复杂度主要衡量一个算法所需要的额外空间,通常由空间的开辟大小决定的
在计算机发展的早期,计算机的存储容量很小,所以对空间复杂度很是在乎;但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度;所以我们如今已经不需要再特别关注一个算法的空间复杂度,而更注重于时间复杂度。
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。(这里的函数指的是数学中的函数,而不是我们C语言中的函数)
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
通俗来说,
算法中基本操作的执行次数(循环部分)
,就是代表了该算法的时间复杂度。
我们是用大O渐进表示法的,什么是大O渐进表示法呢:
算法的时间复杂度通常用大O符号表述,定义为T[n] = O(f(n))。称函数T(n)以f(n)为界或者称T(n)受限于f(n)。 如果一个问题的规模是n,解这一问题的某一算法所需要的时间为T(n)。T(n)称为这一算法的“时间复杂度”。当输入量n逐渐加大时,时间复杂度的极限情形称为算法的“渐近时间复杂度”。
大O符号:是用来描述函数渐进行为的数学符号,这个符号有点像数学中取极限
我们计算时间复杂度时不是计算算法运行的具体次数,而是用大O的渐进表示法来计算,其具体计算方法如下:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。
算法复杂度分为三种情况
- 最坏情况:任意输入规模的最大运行次数(上界)
- 平均情况:任意输入规模的期望运行次数
- 最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
平均情况:N/2次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)。
void Func1(int N)
{
int count = 0;
for (int k = 0; k < 100; ++ k)
{
++count;
}
printf("%d\n", count);
}
答案:O(1)
上面程序具体执行的次数:100
用大O的渐进表示法得出时间复杂度:O(1) (用常数1取代运行时间中的所有加法常数。)
void Func2(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(N + M)
因为这里有两次循环,并且N
和M
都是未知数,无法区分出谁是最高阶项,因此两个都取出,都没有带常数项,不做去除操作。综上Func1
的时间复杂度就是O(N + M)
void Func3(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
答案:O(N^2)
上面程序具体执行的次数:N * N + 2*N + 10
用大O的渐进表示法得出时间复杂度:O(N^2) (只保留最高阶项)
//计算strchr的时间复杂度?
const char* strchr(const char* str, int character);
答案: O(N)
说明:strchr 是一个字符串寻找函数,作用是在字符串str中查找目标字符character
有三种情况:最好的情况,只找一次,此时的时间复杂度为 O(1)
最坏的情况,没有目标字符,需要把整个字符串找一遍,时间复杂度为 O(N)
平均的情况,在中间就找到了,时间复杂度是 O(N / 2)
面对这种多分支情况,我们要做预期管理,用最悲观的态度来判断程序,这样做的好处是预期值低,结果出来时不会有很大落差,生活中也可以像这样,做好准备。言归正传,这里选择最坏的情况,即 O(N),当然这种情况比较特殊,值得注意一下
void Func5(int N)
{
int count = 0;
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
答案:O(N)
上面程序具体执行的次数:2*N + 10
用大O的渐进表示法得出时间复杂度:O(N) (如果最高阶项存在且不是1,则去除与这个项目相乘的常数
void BubbleSort(int arr[], int n)
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
答案: O(N ^ 2)
冒泡排序是一个神奇的算法,每次冒泡比较的趟数都不同,可以这样推导第一趟:比较 N - 1 次
第二趟:比较 N - 2 次
第三趟:比较 N - 3 次
…………
最后一趟: 比较 1 次
总共比较的次数,就是时间复杂度,即 (N - 1) + (N - 2) + …… + 1,显然这是一个首项为 N - 1,尾项为 1 的等差数列,并且共有 N - 1 项,把高中学的知识用起来,N - 1 项和为 (N - 1) * N / 2 ,通过 大O渐进表示法 进行计算,最终结果为 O(N ^ 2)冒泡排序的时间复杂度需要重点记忆:O(N^2)
int BinarySearch(int arr[], int n, int x) //n元素个数 x:要查找的数
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mid = (left + right) / 2;
if (arr[mid] > x)
{
right = mid - 1; //中间元素比x大就挪动right下标
}
else if (arr[mid] < x)
{
left = mid + 1; //中间元素比x小就挪动left下标
}
else
return mid; //找到就返回该元素所在的下标
}
return 0; //找不到就返回0
}
答案:O(log2)
假设需要查找的次数为 k 次,那么可以这样写
N / 2 -> N / 2 ^ 1
N / 4 -> N / 2 ^ 2
N / 8 -> N / 2 ^ 3
…………
其中,左边的序号就是查找的次数 k ,可得出式子 N = 2 ^ k ,稍微变换下,得到 k = logN,其中第二个式子就是二分查找的时间复杂度,注:因为在键盘上无法表示出log的底数,所有在时间复杂度中把log的底数2省略掉了,直接用logN表示log2N的时间复杂度。
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
答案为:O(2^N)
我们可以将递归斐波那契数列水平展开,即
1+2+4+8+16+32+……+2^N
去除影响小的常数项,最终结果为O(2 ^ N)
算法的空间复杂度是指临时占用储存空间大小的量度,空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,空间复杂度算的是变量的个数。 空间复杂度计算规则基本跟时间复杂度类似,也使用大O的渐进表示法。
跟时间复杂度计算基本相似
void BubbleSort(int arr[], int n)
{
int i = 0;
int j = 0;
for (i = 0; i < n - 1; i++)
{
for (j = 0; j < n - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
答案: O(1)
这里我们在循环外部定义了两个变量,然后在循环内部又定义了一个变量;可能有的同学会认为temp变量因为在循环内部,每次进入循环都会被重新定义,所以空间复杂度为N^2,其实不是的;
我们知道虽然时间是累积的,一去不复返,但是空间是不累积的,我们可以重复使用;对于我们的temp变量来说,每次进入if这个局部范围时开辟空间,离开这个局部范围时空间销毁,下一次在进入时又重新开辟空间,出去又再次销毁;所以其实从始至终temp都只占用了一个空间;
所以上面一共一共定义了三个变量,用大O的渐进表示法得到空间复杂度为O(1)。
int BinarySearch(int arr[], int n, int x) //n元素个数 x:要查找的数
{
int left = 0;
int right = n - 1;
while (left < right)
{
int mid = (left + right) / 2;
if (arr[mid] > x)
{
right = mid - 1; //中间元素比x大就挪动right下标
}
else if (arr[mid] < x)
{
left = mid + 1; //中间元素比x小就挪动left下标
}
else
return mid; //找到就返回该元素所在的下标
}
return 0; //找不到就返回0
}
答案:O(1)
和冒泡排序的空间复杂度一样,这里只定义了三个(常数个)变量,所以空间复杂度是O(1)。
long long Fibonacci(size_t N)
{
return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}
答案:O(N)
递归的规则是先递出,再回归,如果中途遇到递归,继续递出,因此在计算递归的空间复杂度时,计算的是每次递归调用的变量个数相加(所开辟的空间),也可以看作递归的深度显然这里的递归深度是 N,开辟了N个栈帧,每个栈帧使用了常数个空间,空间复杂度自然就是 O(N) 了
时间复杂度和空间复杂度都是用大O的渐进表示法来表示。
时间复杂度看运算执行的次数,空间复杂度看变量定义的个数。
在递归中,时间复杂度看调用的次数,空间复杂度看调用的深度。
时间是累积的,一去不复返;空间是不累积的,可以重复利用。
冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1)。
二分查找的时间复杂度为O(logN),空间复杂度为O(1)。
斐波那契递归的时间复杂度为O(2^N),空间复杂度为O(N)。