1.集合框架
1.1 什么是集合框架
Java集合框架,又被称为容器,是定义在java.util包下的一组接口和接口实现的一些类.其主要的表现就是把一些数据放入这些容器中,对数据进行便捷的存储,检索,管理.集合框架底层实现原理其实就是各种数据结构的实现方法,所以在以后的学习中,我们会采用首先掌握这些集合底层的数据结构是如何实现的,学会用代码自己实现,最后我们在来掌握这些集合类的具体方法等内容.下面是集合框架的总览:
集合类在面试和笔试中也非常常见,所以这部分内容相当重要.
2. 如何学好数据结构和集合类
- 数据结构中涉及到的代码非常多,90%全是代码,所以死磕代码,磕成这样就可以了
- 多画图,数据结构非常抽象,有些题不画图根本解不出来.
- 学完基础知识之后,多刷题,在以后的笔试时候经常考到数据结构,包括在面试的时候,面试官很可能让你手撕数据结构.剑指offer和leetcode还有牛客网均可.
leetcode链接
牛客网链接
3. 时间复杂度和空间复杂度
时间复杂度和空间复杂度,经常用来衡量一个算法的好坏.第一种是时间效率,第二种是空间效率。时间效率被称为时间复杂度,而空间效率被称作空间复杂度。 时间复杂度主要衡量的是一个算法的运行速度,而空间复杂度主要衡量一个算法所需要的额外空间,在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
3.1 时间复杂度
3.1.1 时间复杂度的定义
在计算机科学中,算法的时间复杂度是一个数学函数,它定量描述了该算法的运行时间。一个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个分析方式。一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
3.1.2 大O的渐进表示法
计算下面的基本操作执行了几次
void func1(int N){
int count = 0;
for (int i = 0; i < N ; i++) {for (int j = 0; j < N ; j++) {//执行n^2次count++;}
}
for (int k = 0; k < 2 * N ; k++) {//执行2n次count++;
}
int M = 10;
while ((M--) > 0) {//执行10次count++;
}System.out.println(count);
}
由上述代码可知,F(N) = N^2+2N+10 ,但是在我们在实际计算时间复杂度的时候,我们并不一定要计算精确的次数,而只需要大概执行的次数,那么我们就用大O渐进法来表示.
3.1.3 推导大O的方法
- 用常数1取代运行时间中所有的加法常数
- 在修改后的运行次数函数中,只保留最高阶项
- 如果最高项存在且系数不为1,则把最高项的系数改为1即可
通过上面的分析,我们会发现,大O渐进法去除了那些对结果影响不大的项,简洁明了的表示出了执行次数.
另外有些算法的时间复杂度存在最好,平均和最坏的情况.
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
在实际情况中,我们在衡量时间复杂度的时候,一般都说的是最坏的情况,所以上述数组搜索的时间复杂度是O(N).
3.1.4 常见时间复杂度举例
[实例1]
void func2(int N) {int count = 0;for (int k = 0; k < 2 * N ; k++) {//执行2N次count++;}int M = 10;while ((M--) > 0) {//执行10次count++;}System.out.println(count);
}
//F(N) = 2N+10,通过大O的渐进表示法的化简,我们得到了时间复杂度为O(N).
[实例2]
void func3(int N, int M) {int count = 0;for (int k = 0; k < M; k++) {//执行M次count++;}for (int k = 0; k < N ; k++) {//执行N次count++;}System.out.println(count);
}
//F(N)=M+N,根据化简后得到时间复杂度O(N+M).
[实例3]
void func4(int N) {int count = 0;for (int k = 0; k < 100; k++) {//执行100次count++;}System.out.println(count);
}
//F(N)为常数,所以直接把常数改为1,不存在最高次项,所以时间复杂度为O(1).
[实例4] (冒泡排序)
void bubbleSort(int[] array) {
for (int end = array.length; end > 0; end--) {//执行N次boolean sorted = true;for (int i = 1; i < end; i++) {//执行N-1次if (array[i - 1] > array[i]) {Swap(array, i - 1, i);sorted = false;}}if (sorted == true) {break;}}
}
//F(N) = N*(N-1),化简后为O(N^2).
看完上述实例之后,有的同学就会问了,好像时间复杂度只看for语句的循环条件就可以了,其实这种说法不准确,我们要结合代码的具体逻辑来分析.下面我们举一个例子来推翻这种不准确的说法.
[实例5] (二分查找法)
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;elsereturn mid;}return -1;
}
我们下面通过画图来说明问题
从上述分析我们可以知道,时间复杂度不可以只单单看for或while的循环条件,我们还是需要明白其中的逻辑来分析.上面我们就是通过画图来分析,也印证了数据结构的学习需要"多画图".
[实例6] (阶乘递归)
long factorial(int N) {return N < 2 ? N : factorial(N-1) * N;
}
递归的时间复杂度 = 递归的次数*每次递归后执行的次数
阶乘从N开始,返回N*(N-1),令返回的值为x,之后算x*(N-2),以此类推…算到最后,一共算了N-1次,时间复杂度就是O(N).
[实例7] (斐波那契数列)
int fibonacci(int N) {return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
这里的斐波那契数列的递归有点类似于二叉树,关于二叉树的问题,我们后边讨论.
由上图和递归时间复杂度的运算法则,我们可以算出时间复杂度为O(2^n).
3.2 空间复杂度
3.2.1 空间复杂度的概念
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度 .空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
3.2.2 空间度复杂度举例
[实例1]
void bubbleSort(int[] array) {for (int end = array.length; end > 0; end--) {boolean sorted = true;//sorted为额外申请的空间for (int i = 1; i < end; i++) {if (array[i - 1] > array[i]) {Swap(array, i - 1, i);//进行交换的时候,temp为额外申请的空间sorted = false;}}if (sorted == true) {break;}}
}
上述代码我们可知,额外申请的空间为2,所以空间复杂度为O(1).
[实例2]
int[] fibonacci(int n) {long[] fibArray = new long[n + 1];//在这里开辟了一个新的数组,空间和n有关fibArray[0] = 0;fibArray[1] = 1;for (int i = 2; i <= n ; i++) {fibArray[i] = fibArray[i - 1] + fibArray [i - 2];}return fibArray;
}
额外申请的空间为N+1个,所以空间复杂度为O(N).
[实例3]
long factorial(int N) {return N < 2 ? N : factorial(N-1)*N;
}
每次递归都在栈中创建了一个栈帧,每个栈帧都占用的是常数个额外空间,所以时间复杂度为O(N).