一、ArrayList扩容规则:
1.使用add();方法进行元素添加:
(1)初始数组长度为0;无参构造数组长度为0;
(2)添加数据,先扩容10,之后扩容为原基础的1.5倍(准确说是位数转换为二进制数,右移一位再加上原位数);
(3)[0,10,15,22,33,49,73,109,163,244,366....]。
2.使用addAll();方法进行元素添加:
(1)初始数组长度为0;
(2)若添加元素个数小于基础扩容个数,则同add()方法扩容规则一致。若添加元素个数大于基础扩容个数,则在元素个数和相近的扩容个数里选较大值作为扩容个数。
二、复杂度分析:
1.时间复杂度:
(1)估算方法:
- 事后分析法(简单,但不可取);
- 事前分析法(用)
- 随着输入规模的增大,算法的常数操作可以忽略不记;
- 随着输入规模的增大,与最高次项相乘的常数可以忽略;
- 最高次项的指数大的,随着n的增长,结果也会变得增长特别快;
- 算法函数中n的最高次幂越小,算法效率越高;
(2)大O记法
- 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随着n变化情况并确定T(n)的量级,就是算法的时间量度,记作T(n)=Of(n),他表示随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称时间复杂度。
- 执行次数=执行时间。
- 随着规模n的增大,T(n)增长最慢的算法就是最优算法。
- 大O记法规则:
- 用常数1取代运行时间中的所有加法常数;
- 在修改后的运行次数中,只保留高阶项;
- 如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数。
- 例如:3次=O(1); n+3=O(n); n^2+2=O(n^2);
- 复杂度排序:O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)
2.空间复杂度:
三、排序
1.Comparable接口:
2.冒泡排序:
(1)比较原理:
- 比较相邻的元素,如果前一个元素比后一个元素大,就交换这两个元素的位置;
- 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大值。
图片来源:黑马程序员网络课程。
- 冒泡排序最优化代码实现:每轮冒泡时,最后一次交换索引可作为下一轮冒泡的比较次数,如果这个值为0,表示整个数组有序,直接退出外层循环即可。
public class Joe {
public static void main(String[] args){
int[] a = {5,9,7,4,1,3,2,8};
bubble(a);
}
public static void bubble(int[] a){
int n = a.length-1;
while (true){
int last = 0; //表示最后一次交换索引位置
for (int i = 0; i < n; i++) {
System.out.println("比较次数"+i);
if (a[i] > a[i+1]){
swap(a,i,i+1);
last = i;
}
}
n=last;
System.out.println(Arrays.toString(a));
if (n == 0){
break;
}
}
}
public static void swap(int[] a, int i ,int j){
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
3.选择排序
(1)排序原理:
- 将要排序的数组视作两个部分,已排序和未排序部分,从索引0的位置开始,作为最小值,一次与未排序部分的数组元素进行比较,如果有比原最小值更小的元素,则将最小元素交换,将新的最小值是做最小值继续向后比较,最终确定出数组中的最小值,将它与索引0处的元素位置互换,此时将最小值所在区域视为已排序部分,后续元素依次实现上述排序方法,知道整个数组已排序。
(2)选择排序和冒泡排序的比较:
- 二者时间复杂度都是O(n^2);
- 选择排序一般要快于冒泡,因为交换次数少;
- 但如果集合有序度高,冒泡优于选择;
- 冒泡属于稳定排序算法,而选择属于不稳定排序(选择排序如果同一组数据经历多次排序,后面的排序方法可能会打乱之前的排序结果。)
4.插入排序
(1)排序原理:
- 同选择排序一样,分为有序区和无序区两部分;
- 找到未排序区中的第一个元素,向已排序的组中进行插入;
- 倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待插入元素放到这个位置,其他的元素向后移动一位。
- 代码实现:
public static void main(String[] args){ int[] a = {5,9,7,4,1,3,2,8}; insert(a); } public static void insert(int[] a){ //i代表待插入的元素索引 for (int i = 0; i < a.length; i++) { int t = a[i]; //代表待插入的元素值 int j = i-1; //代表已排序区的元素索引 while (j >= 0){ if (t < a[j]){ a[j+1] = a[j]; }else { break; //退出循环,减少比较次数 } j--; } a[j+1] = t; System.out.println(Arrays.toString(a));; } }
(2)与选择排序比较:
- 二者时间复杂度都是O(n^2);
- 大部分情况下,插入优于选择;
- 有序集合插入的时间复杂度为O(n);
- 插入属于稳定算法。
5.希尔排序
(1)排序原理:
- 选定一个增长量h,按照增长量h作为分组依据,对数据分组;
- 对分好的每一组数据完成插入排序;
- 减小增长量,最小减为1,重复上述操作
6.快速排序
(1)排序原理:
- 每一轮选一个基准点进行分区
- 让小于基准点的元素进入一个分区,大于基准点的元素进行另一个分区;
- 当分区完成时,基准点元素的位置就是其最终位置;
- 在子分区内重复以上过程,直至子分区元素个数少于等于1。
(2)实现方式:
- 单边排序快排(lomuto罗穆托分区方案)
- 选择最右元素作为基准点;
- j指针负责找到比基准点小的元素,一旦找到则与i进行交换;
- i指针维护小于基准点元素的边界,也是每次交换的目标索引;
- 最后基准点与i交换,i即为分区位置。
- 代码实现:
public static void main(String[] args){
int[] a = {5,3,7,2,9,8,1,4};
quick(a,0,a.length-1);
}
public static void quick(int[] a,int l,int h){
if (l >= h){
return;
}
int p = partition(a, l, h); // p索引值
quick(a,l,p-1);
quick(a,p+1,h);
}
public static int partition(int[] a,int l,int h){
int pv = a[h];
int i = l;
for (int j = l; j < h; j++) {
if (a[j] < pv){
if (i != j) {
swap(a, i, j);
}
i++;
}
}
if (i != h) {
swap(a, h, i);
}
System.out.println(Arrays.toString(a)+"i"+i);
// 返回值代表基准点元素返回的正确索引,用它确定下一轮分区边界
return i;
}
- 双边排序:
- 选择最左边元素作为基准点元素;
- j指针负责从右向左找比基准点小的元素,i指针负责从左向右找比基准点大的元素,一旦找到二者交换,直至i,j相交。
- 最后基准点与i(此时的i与j相等)交换,i即为分区位置
- 实现:
@SuppressWarnings("all")
public class Joe {
public static void main(String[] args){
int[] a = {5,3,7,2,9,8,1,4};
quick(a,0,a.length -1 );
}
private static void quick(int[] a, int l, int h){
if (l >= h){
return;
}
int p = partition(a, l, h); // p索引值
quick(a,l,p-1);
quick(a,p+1,h);
}
private static int partition(int[] a, int l, int h){
int pv = a[l];
int i = l;
int j = h;
while (i < j){
//j 从右找小的
while (i< j && a[j] > pv){
j--;
}
//i 从左找大的
while (i < j && a[i] <= pv){
i++;
}
swap(a, i ,j);
}
swap(a,l,j);
System.out.println(Arrays.toString(a)+"j="+j);
return j;
}
(3) 特点:
- 平均时间复杂度是O(nlog2n),最坏的时间复杂度是O(n^2);
- 数据量较大时,优势非常明显;
- 属于不稳定排序;
7.递归
(1)定义:
- 程序调用自身的编程技巧称为递归
- 递归的典型例子是数字的阶乘。数字 N 的阶乘是 1 到 N 之间所有整数的乘积。
@SuppressWarnings("all")
public class Joe {
public static void main(String[] args){
long result = factorial(10);
System.out.println(result);
}
public static long factorial(int n){
if (n == 1){
return 1;
}
return n*factorial(n-1);
}
}
- 练习:使用递归列出windows目录下的所有文件夹及所有文件
getAllFile(new File("D:\\WeddingPics")); } public static void getAllFile(File dir) { File[] files = dir.listFiles(); if (files != null) { for (File f : files) { // 如果获取的File类型是目录,则进行递归调用 if (f.isDirectory()) { System.out.println("目录:" + f); getAllFile(f); } else { // 如果获取的File类型是文件,则直接打印输出 System.out.println("文件:" + f); } } } } }
四、二叉树
1.二叉树入门
(1)树的基本定义:
- 树是由n(n>=1)个有限节点组成的一个具有层次关系的集合。
- 数具有以下特点:
- 每个结点有零个或者多个子结点;
- 没有父节点的结点为根结点;
- 每一个非根结点只有一个父结点;
- 每个结点及其后代结点整体上可以看作是一棵树,称为当前结点的父结点的一个子树;
- 树的相关术语:
- 结点的度:一个结点含有的子树的个数称为该结点的度;
- 叶结点:度为0的节点称为叶结点,也可以叫做终端结点;
- 分支结点:度不为0的节点称为分支结点,也可以叫做非终端结点;
- 结点的层次:从根结点开始,根节点的层次为1,根的后继层次为2,以此类推;
- 结点的层序编号:从上到下,从左到右排成线性序列,编成连续的自然数;
- 树的度:树中所有结点的度的最大值;
- 树的高度(深度):树中结点的最大层次;
- 森林:m(m>=0)个互不相交的树的集合,将一颗非空树的根结点删去,树就变成一个森林;给森林增加一个统一的根结点,森林就变成一棵树;
- 孩子结点:一个结点的直接后继;
- 双亲结点(父结点):一个结点的直接前驱;
- 兄弟结点:统一双亲结点的孩子结点间。
(2)二叉树的基本定义:
- 度不超过2的树(每个节点最多有两个子结点);
- 满二叉树:一个二叉树,如果每一层的结点数都达到最大值,则这个二叉树就是满二叉树;
- 完全二叉树:叶结点只能出现在最下层和此下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
(3)二叉查找树的创建:
- 二叉树的结点类