目录
1. 初识集合框架
1.1 集合框架的初识
1.2 什么是数据结构?
2. 时间与空间复杂度
2.1 时间复杂度
2.2 大O的渐进表示法
2.3 常见时间复杂度计算举例
2.4 空间复杂度
1. 初识集合框架
1.1 集合框架的初识
什么叫集合?什么叫框架?什么又叫集合框架?这几个字该怎么理解?下来我们一起来看。
首先,在java中集合框架也被叫做容器(container),是在java.util包底下的一组接口和其实现类.
注意: 列举了常用的接口或者抽象类, 展示了集合之间的关系,这个每个集合类放在一起就叫做集合框架;这些集合类是Java官方已经写好的类,我们需要学习的就是这一个个类或者接口背后的结构是一个什么样的东西。所以我们要学习集合,就需要学习它们背后的数据结构。
1.2 什么是数据结构?
什么是数据结构?通俗来说就是数据+结构。
举个例子:我们平时日常生活中的数据:做核酸,停车场的车位号等等,那么我们就用数据结构组织这样一堆堆的数据.
数据结构有许多:单链表、二叉树、数组等等
数据结构多种多样的原因,就是面对不同的数据我们描述和组织数据的方式不同。
所以当我们把数据结构学明白了,就可以了解了整个常用集合框架背后的知识。
于是我们接下来就会学习到:顺序表、链表、二叉树、栈、队列、堆、哈希表、AVL树、红黑树、B树……
2. 时间与空间复杂度
如何衡量一个算法的好坏?
由于我们看代码好坏的角度不一样,也就是衡量标准不一样,我们得出的结果就不一样,还有,可能由于其他的一些原因也会导致每个人的结果是不一样的,比如说硬件(内存等等)、计算标准(时间长短)等等。
那么我们在这里就统一使用如下标准:使用时间和空间两个角度去衡量算法的好坏。
那么衡量一个算法好坏是时间重要还是空间重要呢?
实际上两者都是重要的,但是平时我们通常追求的是时间的快慢,其次才是空间,因为空间随着时代的发展内存的空间可使用空间越来越大,所以我们会尽可能的浪费一点空间来取换取我们的时间.
算法效率
算法效率分析分为两种:第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
2.1 时间复杂度
定义:在计算机科学中,算法的时间复杂度是一个数学函数,用于描述算法执行所耗费的时间的快慢。
理论上我们是无法计算出一个算法所运行的时间的,因为这和硬件等各种因素是有关系的,硬件不同,执行的时间也不同。
所以我们这样分析:一个算法的时间复杂度和算法当中语句所执行的次数是成正比的,也就是说,语句越多,执行的次数就越多,所浪费的时间也是成正比的。
那么我们既然不能定量的去计算出实际上执行次数的时间,但是我们可以通过函数去描述它的时间,那这个函数就跟语句执行次数有关系,所以当我们看到代码之后我们可以看一下这个代码基本语句的执行次数有多少次。
算法中的基本操作的执行次数,为算法的时间复杂度。
有了这个概念之后我们通过以下几个例子来介绍一下:
// 请计算一下func1基本操作执行了多少次?
void func1(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--) > 0) {
count++;
}
System.out.println(count);
}
可以看出, count++
执行的次数是最多的;
先看第一个嵌套的 for 循环,都是0~N,那么count++
执行了多少次?
这里有n个 i ,每个 i 都走 n 次,那么所有的 i 就一共走了n^2次,即在这个 for 循环里面就是执行了 n^2 次;
第二个for:2n次;
第三个for:10次;
最终相对比较准确的是n^2 + 2n + 10.
但是我们发现,n^2 + 2n + 10只是我们语句的一个执行次数,在程序员角度,并不是以这样一个多项的表达式作为一个复杂度的,当我们的n越来越大的时候,最终算出来的值可以忽略掉这个10,那么也就是说,当n非常非常大的时候,有些东西是可以忽略掉的,于是就有了我们这样一些规则,我们把它叫做大O的渐进表示法.
2.2 大O的渐进表示法
实际中我们计算时间复杂度时,我们其实并不需要计算精确的执行次数,而只需要大概执行次数,即使用 大O的渐进表示法计算复杂度.
大O的渐进表示法
1. 用 常数1取代运行时间中的所有加法常数;
2. 在修改后的运行次数函数中,只 保留最高阶项;
3. 如果最高阶项存在且不是1,则 去除与这个项目相乘的 常数。得到的结果就是大O阶。
注意: 在这里我们在计算大O阶的时候并不是看着代码求,而是需要结合数学思想求解。
补充概念:
最坏情况复杂度;最好情况复杂度;平均复杂度。
我们直接举例说明:在数组中找一个数字的时间复杂度最坏是O(n),最好是O(1),平均是n/2。
我们所接触到的最多的情况就是最坏情况时间复杂度。
2.3 常见时间复杂度计算举例
// 计算func2的时间复杂度?
void func2(int N) {
int count = 0;
for (int k = 0; k < 2 * N; k++) {
count++;
} // 2n
int M = 10;
while ((M--) > 0) {
count++;
} // 10
System.out.println(count);
}
func2的时间复杂度为:O(n);
// 计算func3的时间复杂度?
void func3(int N, int M) {
int count = 0;
for (int k = 0; k < M; k++) {
count++;
} // M
for (int k = 0; k < N; k++) {
count++;
} // N
System.out.println(count);
}
func3的时间复杂度为:O(M+N);
// 计算func4的时间复杂度?
void func4(int N) {
int count = 0;
for (int k = 0; k < 100; k++) {
count++;
} // 100
System.out.println(count);
}
func4的时间复杂度为:O(1);
注:100是常数,常数看作1(第一条规则)
// 计算bubbleSort的时间复杂度? 最坏是时间复杂度?最好复杂度?
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) { //【0,n】
boolean sorted = true;
for (int i = 1; i < end; i++) { //n-1 n-2 …… 1(n在变导致i也在变)
if (array[i - 1] > array[i]) {
swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
}
注:算一下 end 的取值: [ n, n-1, n-2, ...... , n-(n-1) ]
i 的取值-> [ n-1, n-2, n-3, ... , 1, 0 ]
所以对 i 进行求和(等差数列求和),结果为:(最坏情况下if (array[i - 1] > array[i])
每次都被执行)n*(n-1) / 2, 所以最坏情况下时间复杂度为O(n^2),最好情况下就是数组有序了,复杂度为O(n)。
// 计算binarySearch的时间复杂度?
int binarySearch(int[] array, int value) {
int begin = 0;
int end = array.length - 1;
while (begin <= end) {
int mid = begin + ((end - begin) / 2);
if (array[mid] < value)
begin = mid + 1;
else if (array[mid] > value)
end = mid - 1;
else return mid;
}
return -1;
}
注:二分查找,每次“砍”一半:N -> N/2 -> N/4...;
于是:N/2^x = 1 => x = logN(以2为底N的对数)
// 计算阶乘递归factorial的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N - 1) * N;
}
注:递归的时间复杂度 = 递归的次数 * 每次递归执行的次数
本题中,每次递归相当于是一个三目运算符在走,所以每次递归执行的次数是常数次1,所以对于本题来说只需要找到递归的次数就是它的时间复杂度。本题递归的次数是n-1次。
factorial的时间复杂度为:O(n);
// 计算斐波那契递归fibonacci的时间复杂度?
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N - 1) + fibonacci(N - 2);
}
注:每一次递归,都会有两次“子递归”,所以递归次数就为:
2^0 + 2^1 + 2^2 + ... + 2^(n-1) = 2^n - 1次
斐波那契递归fibonacci的时间复杂度为O(2^n)
总结:
复杂度的计算需要通过结合代码的思想来做的。不能光靠代码来给出复杂度。
2.4 空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 。空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
// 计算bubbleSort的空间复杂度?完成这个算法临时空间的大小
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {
boolean sorted = true;
for (int i = 1; i < end; i++) {
if (array[i - 1] > array[i]) {
swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
}
bubbleSort的空间复杂度为O(1),因为没有让临时空间出现增加的情况。
如果修改以上代码:
// 计算bubbleSort的空间复杂度?完成这个算法临时空间的大小
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {
int tmp = new int[array.length];//->拷贝array这个数组里面的数组
boolean sorted = true;
for (int i = 1; i < end; i++) {
if (array[i - 1] > array[i]) {
Swap(array, i - 1, i);
sorted = false;
}
}
if (sorted == true) {
break;
}
}
}
复杂度就变为O(n);