目录
java基础
1.常见算法和Lambda表达式
查找算法
基本查找(对数据顺序没有要求)
定义一个容器,像是集合,数组等,然后通过索引一个一个的查找
核心:从零索引开始,挨个往后查找
package suanfa;
public class chatOne {
public static void main(String[] args) {
}
public static boolean basicSearch(int []arr,int number){
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
System.out.println("元素已被找到");
return true;
}
}
return false;
}
}
- 练习
package suanfa;
import java.util.ArrayList;
public class chatOne {
public static void main(String[] args) {
int []arr={1,2,3,4,4,4,4,5};
int number=4;
ArrayList<Integer>a=basicSearch2(arr,number);
System.out.println(a);
}
//不需要考虑重复
public static int basicSearch1(int []arr,int number){
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
System.out.println("元素已被找到");
return i;
}
}
return -1;
}
//考虑重复
public static ArrayList<Integer> basicSearch2(int []arr , int number){
ArrayList<Integer> a=new ArrayList<>();
for (int i = 0; i < arr.length; i++) {
if(arr[i]==number){
a.add(i);
}
}
return a;
}
}
二分查找/插值查找/斐波那契(要求数据有一定顺序)
前提条件:数据必须是有序的,从大到小
如果数据是无序的先排序再查找是没有意义的,因为只能确定当前数字在数组中是否存在,不能确定数字实际的索引值
核心逻辑:每次排除一半的查找范围
核心点
这里的mid=(min+max)/2
如果所查找的元素的值在mid的左边,min不变,max=mid-1
如果所查找的元素的值在mid的右边,max不变,min=mid+1
min,max,mid都是索引值
- 练习
//我写的代码
package suanfa;
public class chatTwo {
public static void main(String[] args) {
int []arr={7,23,79,81,100,123,192};
int a=123;
System.out.println(getIndex(arr,a));
}
public static int getIndex(int[]arr,int number){
int min=0;
int max=arr.length-1;//这里注意-1
int mid=(min+max)/2;
while (true){
if(arr[mid]==number){
break;
}
if(arr[mid]>number){
max=mid-1;
mid=(min+max)/2;
}else{
min=mid+1;
mid=(min+max)/2;
}
}
return mid;
}
}
//教程写的代码,感觉比我的考虑的周到
package suanfa;
public class chatTwo {
public static void main(String[] args) {
int []arr={7,23,79,81,100,123,192};
int a=200;
System.out.println(getIndex(arr,a));
}
public static int getIndex(int[]arr,int number){
int min=0;
int max=arr.length-1;
int mid=(min+max)/2;
while (true){
if(min>max){
return -1;
}
if(arr[mid]>number){
max=mid-1;
mid=(min+max)/2;
}else if(arr[mid]<number){
min=mid+1;
mid=(min+max)/2;
}
else{
return mid;
}
}
}
}
小结
- 二分查找改进1(插值查找)
要求:数据分布比较均匀
用一个公式使mid更接近实际的数据
mid=(key-arr[min])/(arr[max]-arr[min])*(max-min)
这个公式的解释就是
用所求值减去开头除以总的长度得到所求值在数组中所占的比例,再用这个比值乘以索引的长度得到所求值索引的估值
就比如,在一个xy坐标系上,一条直线的长度是10,用3除以10得到十分之三,那么,十分之三就是3这个点在这台哦直线上的比值,即0到3的长度等于10乘以十分之三。
最后再加上min的原因是使查找范围进行一个偏移
因为查找范围可能不是从0开始 - 二分查找改进2(斐波那契查找)
mid公式
mid=min+黄金分隔点左半边长度-1
小结
分块查找
分块的原则:
- 前一块中的最大数据,小于后一块中的所有数据(块内无序,块间有序)
- 块的数量一般等于数字的个数开根号,例如:16个数字一般分四块左右
核心思路:先确定要查找的元素在哪个块内,然后再块内挨个查找
定义一个块类,将不同的块创建不同的块对象
用一个数组来存储这些块的对象,这个数组也叫做索引表
package suanfa;
public class BlockTest {
public static void main(String[] args) {
int []arr={16,5,9,12,21,18,
32,23,37,25,45,34,
50,48,61,52,73,66};
Block one=new Block(21,0,5);
Block two=new Block(45,6,11);
Block three=new Block(73,12,17);
Block[]arr1={one,two,three};
int number=32;
//调用方法,获取所求元素的索引
int index=getIndex(arr1,arr,number);
System.out.println(index);
}
private static int getIndex(Block[] arr1, int[] arr, int number) {
int index=findIndexBlock(arr1,number);
int strIndex=arr1[index].getStarIndex();
int endIndex=arr1[index].getEndIndex();
for (int i = strIndex; i <=endIndex ; i++) {
if(number==arr[i]){
return i;
}
}
return -1;
}
//定义方法,确定number在哪一块中
public static int findIndexBlock(Block[]arr1,int number){
for (int i = 0; i < arr1.length; i++) {
if(number<=arr1[i].getMax()){
return i;
}
}
return -1;
}
}
class Block{
private int max;
private int starIndex;
private int endIndex;
public Block() {
}
public Block(int max, int starIndex, int endIndex) {
this.max = max;
this.starIndex = starIndex;
this.endIndex = endIndex;
}
public int getMax() {
return max;
}
public void setMax(int max) {
this.max = max;
}
public int getStarIndex() {
return starIndex;
}
public void setStarIndex(int starIndex) {
this.starIndex = starIndex;
}
public int getEndIndex() {
return endIndex;
}
public void setEndIndex(int endIndex) {
this.endIndex = endIndex;
}
}
- 分块查找的扩展1(无规律的数据)
上图的数据不满足分块查找的规律,所以就需要扩展,在分块时,块与块之间不能有交集。
package suanfa;
public class BlockTest2 {
public static void main(String[] args) {
int []arr={27,22,30,40,26,
13,19,16,20,7,10,
43,50,48};
BlockTwo b1=new BlockTwo(22,40,0,4);
BlockTwo b2=new BlockTwo(7,20,5,10);
BlockTwo b3=new BlockTwo(43,50,11,13);
BlockTwo []arrBlock={b1,b2,b3};
int number=20;
int c = getIndex(arrBlock, arr, number);
System.out.println(c);
}
public static int getIndex(BlockTwo[]arrBlock,int []arr,int number){
int indexBlock=findIndexBlock(number,arrBlock);
if(indexBlock==-1){
return -1;
}
for (int i = arrBlock[indexBlock].getMinIndex(); i <= arrBlock[indexBlock].getMaxIndex(); i++) {
if(number==arr[i]){
return i;
}
}
return -1;
}
public static int findIndexBlock(int number,BlockTwo[]arrBlock){
for (int i = 0; i < arrBlock.length; i++) {
if(number>=arrBlock[i].getMin()&&number<=arrBlock[i].getMax()){
return i;
}
}
return -1;
}
}
class BlockTwo{
private int min;
private int max;
private int minIndex;
private int maxIndex;
public BlockTwo() {
}
public BlockTwo(int min, int max, int minIndex, int maxIndex) {
this.min = min;
this.max = max;
this.minIndex = minIndex;
this.maxIndex = maxIndex;
}
/**
* 获取
* @return min
*/
public int getMin() {
return min;
}
/**
* 设置
* @param min
*/
public void setMin(int min) {
this.min = min;
}
/**
* 获取
* @return max
*/
public int getMax() {
return max;
}
/**
* 设置
* @param max
*/
public void setMax(int max) {
this.max = max;
}
/**
* 获取
* @return minIndex
*/
public int getMinIndex() {
return minIndex;
}
/**
* 设置
* @param minIndex
*/
public void setMinIndex(int minIndex) {
this.minIndex = minIndex;
}
/**
* 获取
* @return maxIndex
*/
public int getMaxIndex() {
return maxIndex;
}
/**
* 设置
* @param maxIndex
*/
public void setMaxIndex(int maxIndex) {
this.maxIndex = maxIndex;
}
public String toString() {
return "BlockTwo{min = " + min + ", max = " + max + ", minIndex = " + minIndex + ", maxIndex = " + maxIndex + "}";
}
}
- 分块查找的扩展2(查找的过程中还需要添加额外的数据,哈希查找)
分块后把一个数存在数组里,其余在范围内的数挂
在后面。 - 小结
排序算法
冒泡排序
相邻的数据两两比较,小的放在前面,大的放在后面
第一轮结束,最大的数据就会放在最前面,即数组的最右面
第二轮循环就在剩余的元素里找最大的值就行了
一直循环到数组排列好为止
- 核心思想
相邻的数据两两比较,小的放在前面,大的放在后面
第一轮循环后,最大值就已被找到,第二轮可以少循环一次
如果数组里有n组数据,总共执行n-1轮代码就可以
package suanfa;
public class maoPao {
public static void main(String[] args) {
int []arr={2,4,3,1,5};
//外循环,表示循环的次数
for (int i = 0; i < arr.length-1; i++) {
for (int j = 0; j < arr.length-1-i; j++) {
if(arr[j]>arr[j+1]){
int temp=arr[j];
arr[j]=arr[j+1];
arr[j+1]=temp;
}
}
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
}
}
}
选择排序
从0索引开始,拿着每一个索引上的元素跟后面的元素依次比较,小的放前面,大的放后面,以此类推
第一轮循环后,最小的数据已被确定,放在0索引的位置,所以,第二轮循环时,从1所索引开始循环
代码与冒泡类似
插入排序
将0索引的元素到N索引的元素看作是有序的,把N+1索引的元素到最后一个当成是无序的。遍历无序的数据,将遍历到的元素插入到有序数列中适当的位置,如遇到相同数据,插在后面。
N的范围:0~最大索引
package suanfa;
public class insertTest {
public static void main(String[] args) {
int []arr={3,44,38,5,47,15,36,26,27,2,46,4,19,50,48};
//第一步,先获取无序部分的起始索引
int strIndex = 0;
for (int i = 0; i < arr.length; i++) {
if(arr[i]>arr[i+1]){
strIndex=i+1;
break;
}
}
//遍历无序部分,获得每一个无序部分的值
for (int i = strIndex; i < arr.length; i++) {
//先记录当前要插入数据的索引
int j=i;
while (j>0&&arr[j]<arr[j-1]){
int temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
j--;
}
}
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
}
快速排序(递归)
- 递归算法:
递归指的是方法中调用方法本身的现象
递归的注意点:递归一定要有出口(不再调用自己),否则就会出现内存溢出,即不能无限循环的调用方法 - 递归作用:把一个复杂问题层层转化为一个
与原问题相似的规模较小
的问题来求解 - 书写递归的两个
核心
:
找出口:什么时候不再调用方法
找规则:如何把大问题转化为规模较小的问题
练习1
//100+99+98+。。。+1
package suanfa;
public class diGui {
public static void main(String[] args) {
System.out.println(get(100));
}
public static int get(int a){
if(a==1){
return 1;
}
return a+get(a-1);
}
}
练习2
//5的阶乘
package suanfa;
public class diGui {
public static void main(String[] args) {
System.out.println(get(5));
}
public static int get(int a){
if(a==1){
return 1;
}
return a*get(a-1);
}
}
注意:方法内部再次调用方法时,参数必须要更加的
靠近出口
方法的返回值会返回到方法的调用处
这里递归到出口时,出口处的n=1返回给jc(1),jc(1)*2返回给jc(2),同理,一直返回到jc(5)即函数总体的返回值
- 快速排序
第一轮把0索引的数字作为基准数,确定基准数在数组中正确的位置。比基准数小的全部在左边,比基准数大的全部在右边
先移动end,再移动start
核心思想
package suanfa;
public class quickSort {
public static void main(String[] args) {
int []arr={6,1,2,7,9,3,4,5,10,8};
quick(arr,0,arr.length-1);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
}
/*定义三个参数
第一个参数:要排序的数组
第二个参数:要排序数组的起始索引(start)
第三个参数:要排序数组的结束索引(end)
* */
public static void quick(int []arr,int i,int j){
//记录两个变量要查找的范围
int start=i;
int end=j;
if(start>end){
return;
}
//记录基准数
int baseNumber=arr[i];
while (start!=end){
while (true){
if(end<=start||arr[end]<baseNumber){
break;
}
end--;
}
while (true){
if(end<=start||arr[start]>baseNumber){
break;
}
start++;
}
int temp=arr[start];
arr[start]=arr[end];
arr[end]=temp;
}
int temp=arr[start];
arr[start]=arr[i];
arr[i]=temp;
quick(arr,i,start-1);
quick(arr,start+1,j);
}
}
181集
- 小结
Arrays(api)
Arrays就是操作数组的工具类
里面的方法基本上都是用static修饰的,想要调用不需要创建对象,直接类名调用就行了
- 常用方法
方法名 | 说明 |
---|---|
public static String toString(数组) | 把数组拼接成一个字符串 |
public static int binarySearch(数组,查找的元素) | 二分查找法查找元素 |
public static int[] copyOf(原数组,新数组长度) | 拷贝数组 |
public static int[] copyOfRange(原数组,起始索引,结束索引) | 拷贝数组(指定范围) |
public static void fill(数组,元素) | 填充数组 |
public static void sort(数组) | 按照默认方式进行数组排序 |
public static void sort(数组,排序规则) | 按照指定的规则排序 |
package suanfa;
import java.util.Arrays;
import java.util.Comparator;
public class ArraysTest {
public static void main(String[] args) {
int []arr={1,2,3,4,5,3,1,7,8,4,8,9};
//toString
String a= Arrays.toString(arr);
System.out.println(a+1);
//binarySearch
//使用binarySearch数组的元素必须时有序的,二分查找
//如果查找的元素存在,返回索引,不存在,返回 -插入点-1(负的插入点减1)(插入点也按照大小顺序)
int b=Arrays.binarySearch(arr,7);
System.out.println(b);
//copyOf
//第一个参数是老数组,第二个是新数组的长度
//若新数组的长度小于老数组,部分拷贝,等于全部拷贝
//大于就全部拷贝的同时,补上数组默认初始化值
int []copyArr=Arrays.copyOf(arr,12);
System.out.println(Arrays.toString(copyArr));
//copyOfRange(指定范围)
//包头不包尾,包左不包右
int []arr3=Arrays.copyOfRange(arr,3,9);
System.out.println(Arrays.toString(arr3));
//fill,原来的数据全部被覆盖
Arrays.fill(arr,10);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+" ");
}
System.out.println(" ");
//sort,默认情况下。给基本数据类型进行升序排列,底层使用的是快速排序
int []arr2={3,2,4,5,8,1,9,3};
Arrays.sort(arr2);
System.out.println(Arrays.toString(arr2));
//sort(数组,指定的规则),可以指定排序的规则
//只能给引用数据类型的数据进行排序
//如果是基本数据类型,需要变成对应的包装类
Integer []arr5={3,2,4,5,8,1,9,8,7,6};
//第二个参数是一个接口,需要传递这个接口的实现类对象,作为排序的规则
//,但是这个实现类只需要执行一次就行,所以没有必要单独写一个类,直接用匿名内部类就行了
//返回值:o1-o2:升序排列,o2-o1:降序排列
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
System.out.println(Arrays.toString(arr5));
}
}
sort更改排序规则的底层原理
记住一句:o1-o2:升序排列,o2-o1:降序排列
Lambda表达式
用来简化内部类的书写
//不用Lambda表达式时
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
//用Lambda表达式时
Arrays.sort(arr5, new (Integer o1, Integer o2)-> {
return o1-o2;
}
);
- 函数式编程
函数式编程思想,忽略面向对象的复杂语法,强调做什么,而不是谁去做 - Lambda表达式
Lambda表达式是JDK8开始的一种新语法格式
()->{
}
(): 对应方法的形参
->:固定格式
{}:对应方法体
- 注意:
Lambda表达式只可以用来简化匿名内部类的书写
Lambda表达式只能简化函数式接口的匿名内部类的写法
函数式接口:
有且只有一个抽象方法的接口叫做函数式接口,接口上方可以加@Functionallnterface注解
package suanfa;
public class LambdaTest {
public static void main(String[] args) {
method( ()-> {
System.out.println("游泳");
}
);
}
public static void method(Swim s){
s.swimming();
}
interface Swim{
public abstract void swimming();
}
}
- 小结
- Lambda表达式的省略写法
省略核心:可推导,可省略(即可以推导出来的都可以省略)
Lambda表达式的省略规则
1.参数类型可以省略不写
2.如果只有一个参数, 参数类型可以省略,同时()
也可以省略
3.如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,但是需要这三个同时省略
//不用Lambda表达式时
Arrays.sort(arr5, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1-o2;
}
});
//Lambda表达式的完整格式
Arrays.sort(arr5, new (Integer o1, Integer o2)-> {
return o1-o2;
}
);
//Lambda表达式的省略写法
Arrays.sort(arr5, new (o1,o2)-> o1-o2);//最省略的写法
小结
- 练习
package suanfa;
import java.util.Arrays;
import java.util.Comparator;
public class LambdaTest2 {
public static void main(String[] args) {
String []arr={"a","aaa","aa","aaaa"};
//不用Lambda
Arrays.sort(arr, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.length()-o2.length();
}
});
//用Lambda
Arrays.sort(arr,(String o1, String o2)-> {
return o1.length()-o2.length();
}
);
//用Lambda简写
Arrays.sort(arr, (o1, o2) -> o1.length()-o2.length());
System.out.println(Arrays.toString(arr));
}
}
算法题
String的compareTo方法,可以比较ascll表值来进行字符串的判断
String a="a";
String b="b";
sout(a.compareTo(b));//-1,证明a的ascll值小于b的值
String c="abc";
String d="abd";
sout(c.compareTo(d));//-1,先比较ab,都相同,再将c和d进行比较
package suanfa;
import java.util.Arrays;
import java.util.Comparator;
public class Practice {
public static void main(String[] args) {
Griltest g1=new Griltest("abc",19,1.68);
Griltest g2=new Griltest("abd",18,1.72);
Griltest g3=new Griltest("abe",19,1.78);
Griltest g4=new Griltest("abf",19,1.78);
Griltest []arr={g1,g2,g3,g4};
Arrays.sort(arr, new Comparator<Griltest>() {
@Override
public int compare(Griltest o1, Griltest o2) {
double a=o1.getAge()- o2.getAge();
a = a == 0 ? o1.getHeight() - o2.getHeight() : a;
a = a == 0 ? o1.getName().compareTo(o2.getName()) : a;
if(a>0){
return 1;
} else if (a<0) {
return -1;
}else {
return 0;
}
}
});
System.out.println(Arrays.toString(arr));
}
}
数据的特点
从第三个月开始,每月的数量等于前两个月之和
package suanfa;
public class tuiziTest {
public static void main(String[] args) {
// int[]arr=new int[12];
// arr[0]=1;
// arr[1]=1;
// for (int i = 2; i < arr.length; i++) {
// arr[i]=arr[i-1]+arr[i-2];
// }
// System.out.println(arr[11]);
System.out.println(getMax(12));
}
/*
* 12=11+10
* 11=10+9
* ..
* 3=2+1
* */
public static int getMax(int mon){
if(mon==1||mon==2){
return 1;
}else{
return getMax(mon-1)+getMax(mon-2);
}
}
}
反向递归
package suanfa;
public class houziTest {
public static void main(String[] args) {
System.out.println(getDay(1));
}
public static int getDay(int a){
if(a==10){
return 1;
}else {
return (getDay(a+1)+1)*2;
}
}
}
20层台阶的爬法=19层台阶的爬法+18层台阶的爬法
还是斐波那契数列
public static int getMax(int mon){
if(mon==1){
return 1;
}else if(mon==2){
return 2;
}
return getMax(mon-1)+getMax(mon-2);
}
2.集合进阶
ArrayList直接集合的一个基础而已
单列集合的体系结构
在java中有很多集合,这些集合大体可以分为两类
1.单列集合(Collection),添加数据的时候每次只能添加一个元素
2.双列集合(Map),添加数据的时候每次添加一对数据
单列集合的体系结构
上面Collection,List,Set是接口,其余的都是这三个接口的实现类
-
List系列的集合的特点:
添加的元素是有序
,可重复
,有索引的
有序:存储和取出的顺序是一样的
可重复:集合中存储的元素是可以重复的
有索引:可以通过索引获取集合中的每一个元素 -
Set系列的集合的特点:
添加的元素是无序
,不重复
,无索引的
有序:存储和取出的顺序有可能是不一样的
可重复:集合中存储的元素是不可以重复的
有索引:不可以通过索引获取集合中的每一个元素
Collection
Collection是单列集合的祖宗接口,他的功能是全部单列集合都可以继承使用的
方法名 | 说明 |
---|---|
public boolean add(E e) | 把给定对象添加到当前集合中 |
public void clear() | 清空集合中的所有元素 |
public boolean remove(E e) | 把给定对象在当前集合中删除 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空,即判断集合的长度是否为0 |
public int size() | 判断集合中元素的个数/集合的长度 |
Collection是一个接口,不能创建他的对象,只能创建他的实现类对象
package suanfa;
import java.util.ArrayList;
import java.util.Collection;
public class CollectionTest {
public static void main(String[] args) {
//为了学习Collection的方法,先这样创建对象
Collection c=new ArrayList();
//add
//如果向List系列中添加元素,表达式会永远返回true,
//即c.add("aa")的返回值为true
//但是,向Set系列添加元素的话,
//如果当前要添加的元素在Set系列的集合不存在,结果返回true,
//如果当前要添加的元素在Set系列的集合存在,结果就会返回false
c.add("aaa");
c.add("bbb");
c.add("ccc");
//clear,清空元素
// c.clear();
//remove,因为Collection中定义的是共性的方法,
//所以不能通过索引删除,只能通过对象删除
//返回值为布尔类型,如果要删除的元素不存在,就会删除失败,返回false
c.remove("ccc");
System.out.println(c);
//contains
//contains底层是通过equals方法来判断是否存在的
//所以,如果集合中存储的是自定义对象,也想通过contains方法来判断是否包含
// 那么在自定义对象的JavaBean类中,一定要重写equals方法
//如果不重写,就会使用Object中的equals方法,对地址值进行判断
//
boolean b=c.contains("ccc");
System.out.println(b);
//isEmpty,集合为空返回true,不为空返回false
System.out.println(c.isEmpty());
//size获取集合的长度
System.out.println(c.size());
}
}
Collection的遍历方式
分为三类:
迭代器遍历
增强for遍历
Lambda表达式遍历
迭代器遍历
迭代器遍历时不依赖索引
迭代器在Java中的类是Iterator
,迭代器是集合专用的遍历方式
- Collection集合获取迭代器
方法名:Iterator< E (泛型)> iterator()
这个方法可以返回迭代器对象,默认指向当前集合的0索引处 - Iterator中的常用方法
方法名 | 说明 |
---|---|
boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
ArrayList<String>list=new ArrayList<>();
list.add("111");
list.add("222");
list.add("333");
Iterator<String> it=list.iteartor();//获取迭代器对象,创建指针
while(it.hasNext()){//判断指针指向的集合处是否有元素
String str=it.next();//获取元素,移动指针,将指针移动到下一个元素
}
图示的情况就是指针指向的位置没有元素,所以,这里的hasNext就会返回false
- 迭代器书写时需要注意的细节
1.当指针指空时,即上图所示,再执行next方法就会报错NoSuchElementException
2.迭代器遍历完毕后,指针不会复位,就会一直保留在最后的位置,如果想要再次遍历一遍,就需要再创建一个新的迭代器对象
3.循环中只能用一个next方法,next与hasNext是配套使用的
4.迭代器遍历的时候(遍历的过程中,这点很重要),不能用集合的方法增加或删除,如果实在要删除,可以用迭代器的remove进行删除(迭代器名.remove()),没有增加的办法
- 小结
增强for遍历
增强for遍历的底层就是迭代器,在jdk5之后出现,
只有单列集合和数组才能用增强for遍历,双列集合不能用
- 格式
for(元素的数据类型 变量名: 数组或集合){
}
for(String s:list){//这里的s就是一个第三方变量,
//在循环的过程中依次表示集合中的每一个数据
sout(s);
}
ieda快捷键:集合名+for
:快速创建增强for
- 增强for的细节
修改增强for中的变量(这个变量只是一个第三方变量而已),不会改变集合中原本的数据
Lambda表达式遍历(即forEach方法)
方法名 | 说明 |
---|---|
default void forEach(Consumer<? super T> action ): | 结合Lambda遍历集合 |
要想改成Lambda表达式的形式,方法里面的接口必须是一个函数式接口
package suanfa;
import java.util.ArrayList;
import java.util.function.Consumer;
public class CollectionTest2 {
public static void main(String[] args) {
ArrayList<String>list=new ArrayList<>();
list.add("yuying");
list.add("laoyang");
list.add("yyy");
//匿名内部类的形式
//forEach底层原理
//在底层,forEach会遍历集合,依次得到每一个元素
//把得到的每一个元素,传递给下面的accept方法
//所以s就依次表示集合中的每一个元素(数据)
list.forEach(new Consumer<String>() {
@Override
//这里的s就依次表示集合中的每一个数据
public void accept(String s) {
System.out.println(s);
}
});
//lambda表达式
//转换成lambda表达式的过程
//1.先把new 一直到方法名删除
//new Consumer<String>() {
//@Override
//这里的s就依次表示集合中的每一个数据
//public void accept
//2.然后删除下面多出来的大括号
//3.在形参与方法体之间加上->,可以省略形参类型
//(s)-> {System.out.println(s)}
//4.若只有一个形参,可以把括号省略
//s-> {System.out.println(s);}
//5.如果只有一行,可以省略大括号和分号
//s-> System.out.println(s)
list.forEach(s-> System.out.println(s));
}
}
Collection小结
List系列集合
List是一个接口
- List系列的集合的特点:
添加的元素是有序
,可重复
,有索引的
有序:存储和取出的顺序是一样的
可重复:集合中存储的元素是可以重复的
有索引:可以通过索引获取集合中的每一个元素
Collection的方法List都继承了
又因为List有了索引,所以多了很多索引操作的方法
方法名 | 说明 |
---|---|
void add(int index,E element) | 在此集合中指定位置插入指定的元素 |
E remove(int index) | 删除指定索引处的元素,返回被删除的元素 |
E set(int index,E element) | 修改指定索引处的元素,返回被修改的元素 |
E get(int index) | 返回指定索引处的元素 |
package newList;
import java.util.ArrayList;
import java.util.List;
public class one {
public static void main(String[] args) {
List<String> list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//List的add,向指定索引添加元素
list.add(1,"eee");
System.out.println(list);//把eee插入到了1索引的位置,
//原来索引上的元素会依次往后移
//List的remove
String s=list.remove(1);
System.out.println(list);
System.out.println(s);
//List的set
String s2=list.set(1,"fff");
System.out.println(list);
System.out.println(s2);
//List的get
String s3=list.get(1);
System.out.println(s3);
}
}
在调用方法时,如果方法出现重载现象,优先调用实参与形参类型一致的那个方法
List<Integer>list=new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.remove(1);//这里删除的是2,也就是索引为1的值
//因为在调用方法时,如果方法出现重载现象,优先调用实参与形参类型一致的那个方法
//如果想使用对象删除的话,就手动装箱
Integer i=Integer.valueOf(1);
list.remove(i);
List集合的遍历方式(列表迭代器)
List有5种遍历方式,
迭代器,列表迭代器遍历(List独有),增强for,Lambda,普通for循环
这几种都跟上面的方式差不多
主要是列表迭代器
列表迭代器的获取:
Listterator<数据类型>列表迭代器名=集合名.ListIterator()
元素的添加
列表迭代器名.add(元素)
这里判断与获取元素跟迭代器是一样的
方法名 | 说明 |
---|---|
boolean hasNext() | 判断当前位置是否有元素,有元素返回true,没有元素返回false |
E next() | 获取当前位置的元素,并将迭代器对象移向下一个位置 |
列表迭代器的使用与迭代器没有什么区别,唯一不同的点就是,列表迭代器可以在遍历的过程中添加元素
package newList;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class two {
public static void main(String[] args) {
List<String>list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
ListIterator<String> it=list.listIterator();//这里的list就是一个集合
while(it.hasNext()){
String str=it.next();
it.add("eee");
}
System.out.println(list);
}
}
- 五种遍历方式的对比
数据结构
数据结构就是计算机存储,组织数据的方式
不同的场景下选择不同的数据结构
-
数据结构的概述
数据结构就是计算机存储,组织数据的方式
是指数据相互之间是以什么方式排列在一起的
数据结构是为了更加方便的管理和使用数据,需要结合具体的业务场景来选择
一般情况下,好的数据结构可以带来更高的运行或储存效率 -
需要了解掌握的
栈
栈的特点:后进先出,先进后出
一端开口(栈顶),一端封闭(栈底)
数据进栈的过程称为进/压栈
数据出栈的过程称为出/弹栈
最先进去的元素就是栈底元素,最后进去的元素就是栈顶元素
就像手枪的弹匣,最先装入的子弹最后打出
java内部内存的栈内存也用的这个原理
队列
队列的特点:先进先出,后进后出
两端都开口,一端称为后端,一端称为前端
数据从后端进队列的过程就称作:入队列
数据从前端出队列的过程就称作:出队列
- 栈和队列小结
数组和链表
- 数组特点
1.
查询速度快:通过地址值(找到整个数组)和索引(找到单独的元素)定位,查询任意数据耗时相同(数据在内存中是连续存储的)
2.
删除效率低:要将原始数据删除,同时后面每个数据前移
3.
添加效率极低:添加位置后的每个数据后移,再添加元素
即数组是一种查询快,增删慢的数据模型
与之相对立的数据结构就是链表
- 链表特点
1.
查询速度慢:链表在查询的时候就会比较慢,无论查询哪个数据都要从头开始找
2.
链表的增删相对快(相对数组)
链表的每一个元素称之为结点,每一个结点都是独立的对象,结点里会存储具体的数据和下一个结点的地址值(单向链表)
第一个被创建出来的结点就是头结点
所以链表在查询的时候就会比较慢,无论查询哪个数据都要从头开始找
- 链表的扩展(双向链表)
即在原有的基础上再结点内添加了上一个结点的地址值
可以双向查找,提高了一点查询效率
小结
ArrayList集合底层原理(要多看两遍)
ArrayList底层是数组结构.
1.利用空参构造创建的集合,会在底层创建一个默认长度为0的数组,数组名elementData,同时会有一个成员变量size记录元素的个数
2.当添加第一个数据时,底层会创建一个新的长度为10的数组。所以这里的elementData是在添加第一个元素的时候创建的
size在记录元素的个数的同时,也代表着元素下一次存入的位置
3.底层的数组存满时,会自动扩容1.5倍
即当数组存满后,底层会创建一个新的数组,数组的长度是原来数组的1.5倍,然后把原来数组的元素全部拷贝到新数组中
4.如果一次添加多个元素,1.5倍的新数组放不下,则新创建的数组以实际长度为准
elementData是底层数组的名称
grow就是扩容
- ArrayList底层源码
add(E e)的源码
这里的参数1,2,3是指方法体里的add的参数
add(e,elementData,size)的源码
空参grow的源码
带参grow的源码
第一次执行添加元素后,会一直递归到带参grow的if-else的else方法体内,然后执行Math.max的判断,返回一个长度为10的数组elementData,这样之后,就算扩容完成了,然后接着执行add(e,elementData,size)下面的语句,把想要添加的值赋给elementData[0],size=size+1=1
第一次添加数据的过程
如果添加的数据超过了第一次数据的长度,即10,就会执行第二段
LinkedList集合源码
LinkedList底层数据结构是双链表,查询慢,增删快,但是如果操作的是首尾元素,速度也是极快的
双向链表
因为首尾元素操作起来比较快,所以LinkedList本身多了很多直接操作首尾元素的特有api
这些方法用的不多
特有方法 | 说明 |
---|---|
public void addFirst(E e) | 将指定元素插入此列表的开头 |
public void addLast(E e) | 将指定元素添加到此列表的结尾 |
public E getFirst() | 返回此列表的第一个元素 |
public E getLast() | 返回此列表的最后一个元素 |
public E removeFirst() | 移除并返回此列表的第一个元素 |
public E removeLast() | 移除并返回此列表的最后一个元素 |
-
LinkedList的底层源码
-
结点的源码,item是结点存储的数据,next是下一个结点的地址值,prev是上一个结点的地址值。
-
LinkedList的源码,
这三个成员变量分别是:
size,记录结点的个数
first,记录头结点
last,记录尾结点
当使用LinkedList的空参构造时,上面三个成员变量就会创建出来,只不过是初始化值。 -
add的源码,调用linkLast方法
-
linkLast方法的源码
-
add向集合中添加元素的过程
例如
LinkedList <String> list=new LinkedList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
集合list
第一次调用add方法添加元素“aaa”
时,会调用add方法中的linkLast
方法,同时把“aaa”
元素传递过去,此时在linkLast方法中创建一个变量l
用来记录LinkedList集合创建时的成员变量last(结点的地址)
的值,由于刚开始集合内没有元素(结点),所以此时l
的值为null,然后linkLast方法会在集合list
中创建一个新的结点newNode(结点1)
,在创建结点时把变量l
,“aaa”
和null
传递过去,创建list集合中第一个结点,然后把newNode
的地址值赋值给成员变量last
,再做一个判断,如果此时l
的值为null,就把newNode
的地址值赋值给成员变量first
反之把newNode
的地址值赋值给l.next
,然后成员变量size
+1.
等到集合list
第二次添加元素"bbb"
时,此时再调用linkLast
方法,还是把成员变量last
的值赋值给变量l
,但是,此时成员变量last
的值不再是null,而是上一次添加元素“aaa”
时所创建的结点(结点1)的地址值,同样,会再创建一个新结点(结点2)
,把这时候的变量l(结点1的地址值)
,“bbb”
和null
传递过去,此时if做判断的时候,l
不为null,所以把结点2的地址值赋值给了l.next(结点1内的成员变量next)
,然后size再+1.
此时,结点1就是首结点,有数据和结点2的地址值,结点2就是尾结点,有结点1的地址值和数据。
就如下图所示。
同理,再添加新的结点也是这样的步骤
迭代器的源码
191集
- 引例
ArrayList<String>list=new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
//创建迭代器
Iterator<String>it=list.iterator();
while(it.hasNext()){//判断迭代器指向的位置是否为空
String str=it.next();//获取指向的元素,移动指针到下一个位置
sout(str);
}
-
创建iterator对象时,即使用iterator()创建迭代器对象时,实际上就是创建了一个内部类Itr的对象。
-
内部类Itr
-
hasNext源码
会判断光标与迭代器内的长度(元素的个数)是否相等,相等就返回false,证明指针指到了空的位置,不等就返回true
-
next源码
checkForComodification()方法,判断并发修改异常
-
如何避免并发修改异常
泛型深入
泛型:jdk5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
格式:<数据类型>
注意:泛型中只能写引用数据类型(或包装类)
- 没有泛型时,集合存储数据的方式
package newList;
import java.util.ArrayList;
import java.util.Iterator;
public class three {
public static void main(String[] args) {
//没有泛型时,集合存储数据的方式
//先创建一个集合,没有泛型,可以加任意的数据类型
ArrayList list=new ArrayList();
list.add("111");
list.add(222);
list.add(new Student(11,"11"));
//遍历集合
Iterator it = list.iterator();
while(it.hasNext()){
Object obj=it.next();
//这里虽然可以访问,但是无法使用集合元素的特有功能,
//就像多态,不能调用子类的特有功能
System.out.println(obj);
}
//即如果不给集合限定类型,集合中所有的元素的默认类型都会是Object类型
//Object类型在获取出来的时候是不能使用子类的特殊行为的,
//所以java推出了泛型,数据统一
}
}
-
泛型带来的好处
1.统一数据类型
2.把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。 -
扩展
java中的泛型是伪泛型
即在java文件中,泛型定义的集合内是泛型所限制的数据类型,在编译文件(class)中,泛型内的文件还是Object类型,这个过程叫做泛型的擦除
泛型只需记住一句话,即泛型的出现是为了统一数据类型
-
泛型的细节
1.泛型中不能写基本数据类型
2.指定泛型的具体类型后,传递数据时,可以传入该类类型或子类类型(一般不会写子类类型)
3.如果不写泛型,类型默认时Object -
泛型可以在很多地方定义
即泛型可以写在类的后面
,就是泛型类
,写在方法的后面
,就是泛型方法
,写在接口后面
,就是泛型接口
。
泛型类
使用场景:
当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类
格式:
//格式
修饰符 class 类名<类型>{
}
//例如
public class ArrayList<E>{//只有创建ArrayList对象的时候,E才会确定类型,
}
上面的E可以理解为变量,但不是用来记录数据的,而是记录数据的类型,可以写成:T,E,K,V等
- 泛型类的练习
//新创建的泛型类
package newList;
import java.util.Arrays;
public class MyArrayList<E> {
Object []obj=new Object[10];
int size;
public boolean add(E e){//E是类型,e是形参
obj[size]=e;
size++;
return true;
}
public E get(int index){
return (E)obj[index];
}
@Override
public String toString() {
return Arrays.toString(obj);
}
}
//泛型类的使用
package newList;
public class MyText {
public static void main(String[] args) {
MyArrayList<String>list=new MyArrayList<>();
list.add("111");
list.add("222");
list.add("333");
System.out.println(list.get(2));
System.out.println(list);
}
}
泛型方法
方法中形参不确定时,可以使用类名后面定义的泛型< E >,也可以使用泛型方法
注意:
在类上定义的泛型,在本类上所有的方法都可以使用,
而方法定义的泛型只有本方法可以使用
格式:
//格式
修饰符 <类型> 返回值类型 方法名(数据类型 变量名){
}
//例子
public <T> void show(T t){//T就是数据类型
}
- 泛型方法的练习
//泛型方法
package newList;
import java.util.ArrayList;
public class ListUtil {
private ListUtil(){
}
public static<E> void addAll(ArrayList<E> list,E e1,E e2,E e3,E e4){
list.add(e1);
list.add(e2);
list.add(e3);
list.add(e4);
}
}
//使用工具类
package newList;
import java.util.ArrayList;
import java.util.Iterator;
public class MyText {
public static void main(String[] args) {
ArrayList<Integer>list=new ArrayList<>();
ListUtil.addAll(list,111,222,333,444);
Iterator<Integer> it = list.iterator();
while (it.hasNext()){
Integer inta = it.next();
System.out.println(inta);
}
}
}
泛型接口
//格式
修饰符 interface 接口名<类型>{
}
//例子
public interface list<E>{
}
重点:
泛型接口的两种使用方式:
1.实现类给出具体类型
2.实现类延续泛型,创建对象时再确定具体数据类型
//实现类有具体类型,后续使用时类的方法也要具体的数据类型
public class MyArrayList2 implements List<String> {}
//实现类延续泛型,创建对象时再确定具体数据类型
public class MyArrayList3<E> implements List<E> {}
MyArrayList3<String>list=new MyArrayList3<>();
泛型的继承与通配符
泛型本身不具备继承性,但是数据具备继承性
package newList;
import java.util.ArrayList;
public class Test3 {
public static void main(String[] args) {
ArrayList<Fu>list1=new ArrayList<>();
ArrayList<Zi>list2=new ArrayList<>();
ArrayList<Sun>list3=new ArrayList<>();
//这里可以看出,泛型不具有继承性,从而产生报错
method(list1);
// method(list2);
// method(list3);
//但是数据是有继承性的,可以将子类的对象传到父类泛型的集合中去
list1.add(new Fu());
list1.add(new Zi());
list1.add(new Sun());
}
public static void method(ArrayList<Fu> f){
}
}
class Fu{}
class Zi extends Fu{}
class Sun extends Fu{}
//可以用泛型方法来搞定上面的需求
package newList;
import java.util.ArrayList;
public class Test3 {
public static void main(String[] args) {
ArrayList<Fu>list1=new ArrayList<>();
ArrayList<Zi>list2=new ArrayList<>();
ArrayList<Sun>list3=new ArrayList<>();
ArrayList<Student>list4=new ArrayList<>();
method1(list1);
method1(list2);
method1(list3);
method1(list4);
method2(list1);
method2(list2);
method2(list3);
method2(list4);
}
//但是,使用泛型方法是有弊端的
//弊端:
//泛型可以接受任意的数据,
//但是有的时候需求是只传递某一系列类的对象(例如 Fu,Zi,Sun等)。
public static void <E>method1(ArrayList<E> e){
}
//此时就可以使用泛型的通配符:
// ? 也表示不确定的类型
// 但 ? 可以进行类型的限定
// ? extends E:表示可以传递E或E的所有子类类型
// ? super E:表示可以传递E或E的所有父类类型
public static void method2(ArrayList<? extends FU> list){
}
}
class Fu{}
class Zi extends Fu{}
class Sun extends Fu{}
class Student {}
- 泛型的通配符
? 表示不确定的类型,可以进行类型的限定
? extends E
:表示可以传递E或E的所有子类类型
? super E
:表示可以传递E或E的所有父类类型
注意,这里的E是一个明确的类型
使用格式
public static void getOne(ArrayList<? extents Fu> list){}
上述代码就表示getOne这个方法的参数可以传递Fu或Fu的所有子类类型
-
应用场景:
1.如果在定义类,方法,接口的时候,如果类型不确定,就可以定义泛型类,泛型方法,泛型接口
2.如果类型不确定,但是知道只能传递某个继承体系中的,就可以用泛型的通配符 -
泛型通配符的关键点
可以限定类型的范围
练习
package lainxi.one;
import java.util.ArrayList;
public class test1 {
public static void main(String[] args) {
ArrayList<Animal>list=new ArrayList<>();
Huskies a=new Huskies();
a.setName("1");
a.setAge(2);
a.eat();
Teddy b=new Teddy();
b.setName("2");
b.setAge(2);
b.eat();
PersianCat c=new PersianCat();
c.setName("3");
c.setAge(2);
c.eat();
TanukiCat d=new TanukiCat();
d.setName("4");
d.setAge(2);
d.eat();
list.add(a);
list.add(b);
list.add(c);
list.add(d);
// keepPet(list);
}
public static void keepPet1(ArrayList<? extends Cat>list){
for (Cat cat : list) {
cat.eat();
}
}
public static void keepPet2(ArrayList<? extends Dog>list){
for (Dog dog : list) {
dog.eat();
}
}
public static void keepPet3(ArrayList<? extends Animal>list){
for (Animal animal : list) {
animal.eat();
}
}
}
小结
数据结构(树)
树里的每一个元素都被叫做节点
- 树的一些名词
每一个节点都是独立的对象,里面存放着:父节点的地址值,数据(值),左子节点的地址值,右子节点的地址值
当没有父节点或子节点时,就会记为null,
每一个节点的子节点的数量就是度
22,18,26的度都是2
所以,在二叉树中,任意节点的度都要小于等于2
书的高度就是节点的,节数(层数),有几节(层)就是多少高度
在树中,最上面的节点就是根节点
蓝色虚线的部分就是根节点的左子树
同理,绿色虚线的部分就是根节点的右子树、
子节点也有左右子树
二叉树的遍历方式
四种:
前序遍历,中序遍历,后序遍历,层序遍历
-
前序遍历
即中(根节点)左(子树)右(子树)的方式遍历
根节点,左子树,右子树
例如
那么遍历从20开始,然后18,16,19,23,22,24
这就是前序遍历, -
中序遍历(重要)
即左(子树),中(根节点)右(子树)的方式遍历
遍历从16开始,然后18,19,20,22,23,24
从小到大的方式获取 -
后续遍历
即左(子树)右(子树)中(根节点)
从16开始,然后19,18,22,24,23,20 -
层序遍历
从上到下
20,18,23,16,19,22,24 -
小结
即前序遍历获取当前节点是从最前面获取,
中序就是中间的时候获取,后序就是最后获取
二叉查找树
二叉查找树,又称二叉排序树或二叉搜索树
特点:
1.每一个节点上最多有两个节点
2.任意节点左子树上的值都小于当前节点
3.任意节点右子树上的值都大于当前节点
-
二叉查找树添加节点
在二叉查找树添加节点时遵循一个原则
小的存左边,大的存右边,一样的不存
-
二叉查找树查找节点
也是遵循小的查左边,大的查右边,一样的就是结果
即会先将数据与根节点比较,若比根节点小,就会查找根节点的左边,反之查右边 -
二叉查找树的弊端
若数据只是单向增大或减小,就会变得跟链表一样
平衡二叉树
平衡二叉树在二叉查找树的基础上又多了一个规则:
任意
节点左右子树高度差不超过1.
这里的任意一定要注意
树从二叉树演变到二叉查找树,再演变到平衡二叉树
平衡二叉树的旋转机制
-
平衡二叉树通过旋转机制来保持二叉树的平衡
-
平衡二叉树有两种旋转机制:
左旋,右旋
只有当添加一个节点后,该树不是一个平衡二叉树的时候才会触发旋转机制。
旋转之后会重新保证树的平衡
但是如果添加一个节点后,该树还是一个平衡二叉树,那么就不会触发旋转机制
平衡二叉树的左旋
旋转的时候先确定支点。
-
确定支点:从添加的节点开始,不断的往父节点找不平衡的节点,把遇到的第一个不平衡的点当作支点,再通过支点进行旋转
-
非根节点旋转的步骤
1.以遇到的第一个不平衡的点作为支点(确定支点)
2.把支点左旋降级,变成左子节点
3.晋升原来的右子节点
由上图可知,10为第一个遇到的不平衡的节点,就把10作为支点,进行旋转
旋转之后就是这样,10节点左旋降级,11节点晋升
- 根节点旋转的步骤
1.以不平衡的点(根节点)作为支点(确定支点)
2.将根节点往左拉
3.原先的右子节点变成新的父节点,并把多余的左子节点出让,给已经降级的根节点当右子节点(???)
这里的7,即根节点是一个支点
然后先把10的左子节点9给忽略,按照非根节点旋转的步骤来
然后再把10的左子节点给7(之前的根节点),作为7的右子节点
平衡二叉树的右旋
-
确定支点:从添加的节点开始,不断的往父节点找不平衡的节点,把遇到的第一个不平衡的点当作支点,再通过支点进行旋转
-
非根节点旋转的步骤
1.以遇到的第一个不平衡的点作为支点(确定支点)
2.把支点右旋降级,变成右子节点
3.晋升原来的左子节点
-
根节点旋转的步骤
1.以不平衡的点(根节点)作为支点(确定支点)
2.将根节点往右拉
3.原先的左子节点变成新的父节点,并把多余的右子节点出让,给已经降级的根节点当左子节点(???)
与上面左旋是反过来的
平衡二叉树需要旋转的四种情况
1.左左
2.左右
3.右右
4.右左
- 左左
当根节点左子树的左子树有节点插入,导致二叉树不平衡的情况就是左左
如果是左左导致的不平衡的话,一次右旋就可以搞定了
7是根节点,7的左子树就是蓝色虚线部分,7的左子树的左子树就是红色部分,即7的左子树的左子树就是4
下图的两种情况就是在根节点的左子树的左子树上插入了一个节点导致不平衡
然后做一次右旋就可以了
-
左右
当根节点的左子树的右子树有节点插入,导致二叉树不平衡的情况就是左右
这种情况一次旋转就搞不定了,要先局部左旋,再整体右旋
需要先在局部进行一次左旋,使二叉树变成左左的情况,紫色就是局部左旋的部分
完成后变成左左,在进行右旋
-
右右
当根节点右子树的右子树有节点插入,导致二叉树不平衡的情况就是右右
跟左左相反,一次左旋就可以了 -
右左
当根节点的右子树的左子树有节点插入,导致二叉树不平衡的情况就是右左
跟左右相反,这种情况一次旋转就搞不定了,要先局部右旋旋,再整体左旋
小结
一些平衡二叉树的问题
1(第一问).平衡二叉树是一种特殊的二叉查找树,所以平衡二叉树节点的添加也遵循二叉查找树的方式,小的存左边,大的存右边,一样的不存
2(第二问).查找节点从根节点开始找,将节点值与根节点进行比较,若比根节点小,就再与根节点的左子节点比较,同理,若比根节点大,就与根节点的右子节点比较,如果不相等,就重复以上步骤
红黑树(有点抽象,多看两遍196)
-
红黑树是一种自平衡的二叉查找树,是计算机科学与技术中用到的一种数据结构,
-
它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,
-
每一个节点可以是红或黑
,红黑树不是高度平衡的,他的平衡是通过“红黑规则
”进行实现的
红黑树
这里Nil代表着空,即节点值为Nil是空节点,视为叶子节点,没有具体数据。
红黑树与平衡二叉树的区别
- 红黑规则
1.每一个节点或是红色,或是黑色,只能有这两种色
2.根节点必须是黑色
3.如果一个节点没有(没有左或右或左右都没有)子节点或者没有父节点,则该节点相应的指针属性值为Nil
,这些Nil
视为叶节点
,每个叶节点(Nil)都是黑色的,没有什么实际含义,主要就是在第五条的规则用来统计个数
4.如果某一个节点是红色,那么它的子节点必须是黑色(即不能出现两个红色节点相连的情况)
5.对每一个节点,从该节点到其所有后代叶节点的简单路径
上,均包含相同数目的黑色节点
简单路径:从某一节点开始,一直往子节点前进,不能后退的路径
二叉树节点的组成
红黑树节点的组成
-
红黑树添加节点的规则
默认添加颜色:添加的节点默认是红色的(效率高)
由于规则5,导致添加红色节点的效率高 -
添加节点的处理规则
这个表应该会很常用(在红黑树中)
这个后面的具体步骤就看上面的图
红黑树的增删改查的性能都很好
Set系列集合
Set也是一个接口
-
Set系列集合特点:
无序
,不重复
,无索引
无序:存取的顺序不一致
不重复:集合中的元素不可重复,可以利用这一特性去除重复
无索引:没有带索引的方法,不能用普通for循环遍历,也不能通过索引获取元素 -
Set系列集合的实现类
HashSet,无序,不重复,无索引
LinkedHash,有序,不重复,无索引
TreeSet,可排序,不重复,无索引
Set接口中的方法基本上与Collection的Api一致。
方法名 | 说明 |
---|---|
public boolean add(E e) | 把给定对象添加到当前集合中 |
public void clear() | 清空集合中的所有元素 |
public boolean remove(E e) | 把给定对象在当前集合中删除 |
public boolean contains(Object obj) | 判断当前集合中是否包含给定的对象 |
public boolean isEmpty() | 判断当前集合是否为空,即判断集合的长度是否为0 |
public int size() | 判断集合中元素的个数/集合的长度 |
- 练习
package lainxi.two;
import java.util.HashSet;
import java.util.Iterator;
public class SetTest {
public static void main(String[] args) {
HashSet<String>Set=new HashSet<>();
Set.add("111");
Set.add("222");
Set.add("333");
//增强for
for (String s : Set) {
System.out.println(s);
}
//迭代器
Iterator<String>it=Set.iterator();
while (it.hasNext()){
String s=it.next();
System.out.println(s);
}
//lambda表达式
Set.forEach(s-> System.out.println(s));
}
}
- 小结
HashSet
没有什么额外的方法,与上面集合的方法是一样的
-
HashSet底层原理
HashSet在底层采用哈希表存储数据。
哈希表是一种对于增删改查数据性能都比较好的结构 -
哈希表的组成
jdk8以前:是由数组+链表组成
jdk开始:是由数组+链表+红黑树组成 -
哈希值(哈希表中非常重要的值)
哈希值:对象的整数表现形式
根据int index=(数组长度-1)&哈希值;
来得出数据在数组中存储的位置 -
哈希值的具体定义
根据hashCode方法算出来的int类型的整数
该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
一般情况下,会重写hashCode方法,利用对象内部的属性值计算哈希值,因为不重写hashCode方法的话,hashCode是利用对象地址值计算哈希值,这就会导致属性值一样的两个对象有不同的j哈希值 -
对象的哈希值的特点
1.如果没有重写hashCode方法,不同对象计算出的哈希值是不同的(因为没有重写hashCode方法的话,哈希值是用对象的地址值来计算的
2.如果已经重写hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
3.在小部分情况下,不同的属性值或不同的地址值计算出来的哈希值也有可能一样(哈希碰撞)
//stu类
package lainxi.two;
import java.util.Objects;
public class stu {
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
}
//测试类
package lainxi.two;
public class HashTest {
public static void main(String[] args) {
String s1="111";
String s2="222";
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
//没有重写时,两个对象属性值相同,但是地址不同,所以hash不同
//重写后,两个对象属性值相同,hash值相同
stu t1=new stu(18,"111");
stu t2=new stu(18,"111");
System.out.println(t1.hashCode());
System.out.println(t2.hashCode());
}
}
- jdk8之前HashSet的底层存储原理
1.在底层,创建一个默认长度为16,默认加载因子为0.75
的数组,数组名为table
,默认初始化值为null
2.根据元素的哈希值和数组的长度利用公式计算出应该存入的位置
int index=(数组长度-1)&哈希值;
3.根据计算的值把数据存入相应位置,判断存入位置的值是否为null
,如果是null
,则直接存入
4.如果存入位置的值不是null,就表示存入位置有元素,然后调用equals
方法比较属性值。
5.如果属性值一样,就不会存入,不一样就存入数组,形成链表
jdk8之前,存入时是新元素存入数组,老的元素挂在新元素的下面
jdk8之后,新元素直接挂在老元素的下面
先用equals比较属性值
- jdk8之前
新元素存入数组,老的元素挂在新元素的下面
- jdk8之后
新元素直接挂在老元素的下面
- 加载因子的使用
当数组里存入了加载因子*数组长度
个元素后,数组就会扩容成原先的两倍
注意:
1.当链表长度大于8且数组长度大于64
时,此时HashSet的链表就会自动的转换成红黑树
2.如果集合中存储的是自定义对象,那么必须重写hashCode和equals方法
- HashSet的三个问题
1.HashSet为什么存和取的顺序不一样?
2.HashSet为什么没有索引
3.HashSet是利用什么机制保证数据去重的
1.HashSet为什么存和取的顺序不一样?
因为向HashSet存储元素的时候,是通过数组和链表或红黑树来存储的,用哈希值将数据在数组中存到链表或红黑树时,存储的顺序和取出的顺序是不同的
2.HashSet为什么没有索引
因为链表和红黑树,一个索引下挂着多种元素,所以取消了索引
3.HashSet是利用什么机制保证数据去重的
用hashCode和equals来保证数据的唯一
-
问题,可以解决一下
-
练习
//stu类
package lainxi.two;
import java.util.Objects;
public class stu {
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
}
//测试类
package lainxi.two;
import java.util.HashSet;
public class HashTest {
public static void main(String[] args) {
HashSet<stu>set=new HashSet<>();
set.add(new stu(18,"1111"));
set.add(new stu(19,"2222"));
set.add(new stu(19,"3333"));
set.add(new stu(19,"3333"));
//由于重写了equals和hashCode方法,所以set.add(new stu(19,"3333"));
//没有成功添加,因为重写方法之后比较的是属性值,而没有重写方法之前比较的
//是地址值
System.out.println(set);
for (stu stu : set) {
System.out.println(stu);
}
}
}
像java已经提供好的一些类(String Integer)就不用重写hashCode和equals方法,因为java源码已经重写了
LinkedHashSet
- LinkedHashSet的底层原理:
有序,不重复,无索引
这里的有序是指保证存储和取出的元素顺序一致
有序的原理:底层数据结构仍然是哈希表,只是每个元素又额外多了一个双链表的机制记录存储的顺序
即每个元素都会像双向链表的结点一样,除了存储着数据外,还存储着其他元素的地址值,存入的第一个元素就是头结点,最后一个元素就是尾结点,遍历时从头结点开始,按照添加的顺序遍历,到尾结点结束
在遍历的时候就会遵循添加进链表的数据进行遍历,存储和取出的元素顺序一致
//stu类
package lainxi.two;
import java.util.Objects;
public class stu {
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
}
//测试类
package lainxi.two;
import java.util.LinkedHashSet;
public class HashTest {
public static void main(String[] args) {
LinkedHashSet<stu> set=new LinkedHashSet<>();
set.add(new stu(19,"2222"));
set.add(new stu(19,"3333"));
set.add(new stu(18,"1111"));
set.add(new stu(19,"3333"));
System.out.println(set);
}
}
- 小结
TreeSet
- TreeSet的特点:
不重复,无索引,可排序
可排序:按照元素的默认规则(由小到大)排序
TreeSet集合底层是基于红黑树的数据结构实现排序的,增删改查性能都很好
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<Integer>ts=new TreeSet<>();
ts.add(1);
ts.add(6);
ts.add(3);
ts.add(2);
ts.add(4);
System.out.println(ts);
}
}
- TreeSet集合默认的规则
1.对于数值类型,Integer,Double,默认按照从小到大的顺序进行排序
2.对于字符,字符串类型,则是按照字符在ASCLL码表中的数字升序进行排序
3.如果字符串的字符比较多,比较时跟字符串的长度没有关系,而是会先比较第一个字符,如果第一个字符小,就排在前面,如果第一个字符相同,再比较第二个字符,以此类推
练习
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<stu>ts=new TreeSet<>();
ts.add(new stu(19,"aaa"));
ts.add(new stu(29,"aba"));
ts.add(new stu(20,"aac"));
ts.add(new stu(27,"bca"));
ts.add(new stu(19,"aaa"));
System.out.println(ts);
}
}
代码执行后会报错,因为这里用的是自己定义的stu类,没有给TreeSet指定比较规则,所以就会报错
TreeSet的两种比较方式(comparaTo,compara)
使用的原则:默认使用第一种方式,如果第一种不能满足当前需求,那就使用第二种方式,如果方式1和方式2同时存在,以方式2为准
- 方式一:
默认排序/自然排序:Javabean类实现Comparable接口指定比较规则
即让自己定义的JavaBean类实现Comparable接口并且重写里面的抽象方法,不需要重写hashCode和equals方法,因为hashCode是跟哈希表有关,而TreeSet的底层是红黑树
//stu类
package lainxi.two;
import java.util.Objects;
public class stu implements Comparable<stu>{
private int age;
private String name;
public stu() {
}
public stu(int age, String name) {
this.age = age;
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
stu stu = (stu) o;
return age == stu.age && Objects.equals(name, stu.name);
}
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "stu{age = " + age + ", name = " + name + "}";
}
//这个方法就是实现Comparable接口后重写的抽象方法
@Override
public int compareTo(stu o) {
//this:表示当前要添加的元素
//o:表示当前已经在红黑树存在的元素
System.out.println("this:"+this);
System.out.println("o:"+o);
//在这个方法中,可以指定排序的规则
//例如,指定年龄的规则,按照年龄的升序进行排列
/*返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
* */
return this.getAge()-o.getAge();
}
}
//测试类
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<stu>ts=new TreeSet<>();
ts.add(new stu(19,"aaa"));
ts.add(new stu(29,"aba"));
ts.add(new stu(20,"aac"));
ts.add(new stu(27,"bca"));
ts.add(new stu(19,"aaa"));
System.out.println(ts);
}
}
- 详解compareTo(stu o)方法
//这个方法就是实现Comparable接口后重写的抽象方法
@Override
public int compareTo(stu o) {
//在这个方法中,可以指定排序的规则
//例如,指定年龄的规则,按照年龄的升序进行排列
return this.getAge()-o.getAge();
}
在这个方法中,this
表示当前要添加的元素,o表示已经在红黑树中存在的元素,返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<stu>ts=new TreeSet<>();
ts.add(new stu(19,"aaa"));
ts.add(new stu(29,"aba"));
ts.add(new stu(20,"aac"));
ts.add(new stu(27,"bca"));
ts.add(new stu(19,"aaa"));
System.out.println(ts);
}
}
在上述代码中
第一次添加的是stu(19,"aaa")
,此时stu(19,"aaa")
作为红黑树的根节点,然后再添加stu(29,"aba")
,此时就要根据compareTo(stu o)
方法中指定的规则来比较红黑树结点中数据的大小,上面compareTo(stu o)
方法指定的规则是stu对象的age属性进行比较,即return this.getAge()-o.getAge();
,这里this就表示当前要添加的元素,即stu(29,"aba")
,this.getAge()就是29,然后减去o.getAge(),这里的o就表示已经在红黑树中存在的元素,即stu(19,"aaa")
,o.getAge()就是19,29-19>0,所以表示新添加的数据比原本的数据大,所以添加到原来数据的右边,添加多个元素时根据红黑树的规则进行添加
- 方式二
比较器排序:创建TreeSet对象的时候,传递比较器Comparator指定规则
TreeSet(Comparator<? super E> comparator)
构造一个新的空 TreeSet,它根据指定比较器进行排序。
package lainxi.two;
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
TreeSet<String>ts=new TreeSet<>(new Comparator<String>() {
@Override
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值规则与方法1一样
//返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
//返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
//返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
public int compare(String o1, String o2) {
//按照字符串的长度排序
int i=o1.length()-o2.length();
//由三目运算符计算,如果o1与o2的长度相同
//,再执行o1.compareTo(o2),返回给o1
i=i==0?o1.compareTo(o2):i;
return i;
}
});
}
}
- 练习
//stu类
package lainxi.two;
public class stu implements Comparable<stu>{
private String name;
private int age;
private int math;
private int chinese;
private int english;
public stu() {
}
public stu(String name, int age, int math, int chinese, int english) {
this.name = name;
this.age = age;
this.math = math;
this.chinese = chinese;
this.english = english;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
/**
* 获取
* @return math
*/
public int getMath() {
return math;
}
/**
* 设置
* @param math
*/
public void setMath(int math) {
this.math = math;
}
/**
* 获取
* @return chinese
*/
public int getChinese() {
return chinese;
}
/**
* 设置
* @param chinese
*/
public void setChinese(int chinese) {
this.chinese = chinese;
}
/**
* 获取
* @return english
*/
public int getEnglish() {
return english;
}
/**
* 设置
* @param english
*/
public void setEnglish(int english) {
this.english = english;
}
public String toString() {
return "stu{name = " + name + ", age = " + age + ", math = " + math + ", chinese = " + chinese + ", english = " + english + "}";
}
@Override
public int compareTo(stu o) {
int sum1=this.getChinese()+this.getMath()+this.getEnglish();
int sum2=o.getEnglish()+o.getChinese()+o.getMath();
//比较总分
int i=sum1-sum2;
//比较语文
i = i == 0 ? this.getChinese() - o.getChinese() : i;
//比数学
i = i == 0 ? this.getMath() - o.getMath() : i;
//比年龄
i=i==0?this.getAge()-o.getAge():i;
//这里的comparaTo是字符串的方法,比姓名
i=i==0?this.getName().compareTo(o.getName()):i;
return i;
}
}
//测试类
package lainxi.two;
import java.util.TreeSet;
public class TreeSetTest {
public static void main(String[] args) {
stu s1=new stu("zhangsan",19,50,50,60);
stu s2=new stu("lisan",17,60,50,60);
stu s3=new stu("wusan",21,50,60,60);
stu s4=new stu("zhangsi",24,55,50,60);
stu s5=new stu("zhangsan",19,50,50,60);
TreeSet<stu>ts=new TreeSet<>();
ts.add(s1);
ts.add(s2);
ts.add(s3);
ts.add(s4);
ts.add(s5);
for (stu t : ts) {
int sum=t.getChinese()+t.getMath()+t.getEnglish();
System.out.println(sum);
System.out.println(t);
}
}
}
小结
使用的原则:默认使用第一种方式,如果第一种不能满足当前需求,那就使用第二种方式,如果方式1和方式2同时存在,以方式2为准
集合的使用场景
实际开发使用的最多的有两种
ArrayList和HashSet
如果想要集合中的元素可以重复,就用ArrayList(基于数组),
如果想对集合中的元素去重的话,就使用HashSet(基于哈希表)
有一些特殊情况:
如果集合的增删操作明显多于查询的
就用LinkedList(基于链表)
如果想对集合的元素去重,而且保证存取顺序
就用LinkedHashSet集合(基于哈希表和双链表,效率低于HashSet)
如果想对集合中的元素进行排序
就用TreeSet集合(基于红黑树,后续也可以用List集合实现排序)
双列集合
双列集合的特点
-
单列集合与双列集合特点的对比
单列集合每次添加元素的时候,一次只能添加一个元素
双列集合每次添加元素的时候,一次需要添加两个元素,也可以说成是一对元素
在双列集合中,有两个关键的概念,键
和值
上图左边一列就是键,右边一列就是值
键是不可以重复的,值可以重复
键和值之间是一 一对应的关系,每一个键只能对应自己的值
一个键和值在java就被称为键值对,或者叫做键值对对象(Entry)
下图中就用三个键值对对象
-
双键集合特点的总结
双列集合的体系结构
Map集合中常见Api
双列集合的体系结构
Map是双列集合的顶层接口
,他的功能是全部双列集合都可以继承使用的
Map接口有两个泛型,一个是键的泛型,一个是值的泛型
方法名 | 说明 |
---|---|
V put(K key,V value) | 添加元素 |
V remove(Object key) | 根据键删除键值对元素 |
void clear() | 移除所有的键值对元素 |
boolean containsKey(Object key) | 判断集合是否包含指定的键,包含返回true,不包含返回false |
boolean containsValue(Object value) | 判断集合是否包含指定的值,包含返回true,不包含返回false |
boolean isEmpty() | 判断集合是否为空 |
int size() | 集合的长度,也就是集合中键值对的个数 |
V get(Object key) | 得到键所对应的值 |
package dayOne;
import java.util.HashMap;
import java.util.Map;
public class MapTest {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
//put添加元素,与add类似
//当用put对一个键添加两次键值时,
//后面的键值会覆盖掉前面的键值,
//并且返回值为前面的键值(自己的语言解释的还是不够好)
//在添加数据时,如果键不存在,直接把键值对对象添加到map集合中,方法返回null
//如果键存在,那么会把原有的键值覆盖,并把被覆盖的值进行返回(方法返回被覆盖的值)
//put方法的返回值是由创建Map类集合时的泛型决定
// V put(K key,V value)
//即当键不存在的时候时添加操作,键存在的时候时覆盖操作
map.put("one","1");
String s=map.put("one","2");
System.out.println(s);//1
map.put("two","2");
map.put("three","3");
map.put("four","4");
System.out.println(map);//{four=4, one=2, two=2, three=3}
//remove
//根据键删除键值对,并把值进行返回
String a= map.remove("three");
System.out.println(map);//{four=4, one=2, two=2}
System.out.println(a);//3
// //clear,清空所有的键值对
// map.clear();
// System.out.println(map);//{}
//containsKey containsValue,判断集合是否包含指定的键或值
boolean b= map.containsKey("one");
boolean b2= map.containsKey("five");
System.out.println(b);//true
System.out.println(b2);//false
boolean b1=map.containsValue("4");
boolean b3=map.containsValue("5");
System.out.println(b1);//true
System.out.println(b3);//false
//isEmpty,判断集合是否为空,若集合为空,返回true,不为空返回false
System.out.println(map.isEmpty());//false
//size,表示集合的长度,也就是键值对的个数
System.out.println(map.size());
}
}
Map集合的遍历方式
有三种遍历方式
1.键找值
2.键值对
3.Lambda表达式
键找值遍历(keySet,get)
先把所有的键提出到一个单列集合中,然后遍历单列集合,得到每一个键,然后通过get方法得到每个键所对应的值
Map的keySet
方法可以把Map集合的所有键存到一个Set集合中
格式:Map集合名.keySet();
,返回值是一个Set集合
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
Set<String> keys= map.keySet();
}
Map的get(Object key)
方法可以得到键所对应的值
格式Map集合名.get(键名)
,返回值是键所对应的值
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
sout(map.get("one"));//1
第一种遍历方式(利用增强for)
package dayOne;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
//这里使用keySet(),将map集合的所有键存储到st集合中
Set<String>st=map.keySet();
for (String s : st) {//这里对st使用增强for遍历,获取每一个键值
//然后利用get方法,获取键对应的值
System.out.println(s+"="+map.get(s));
}
}
}
- 练习(利用迭代器)
package dayOne;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
Set<String>st=map.keySet();
Iterator<String>it= st.iterator();
while (it.hasNext()){
String s=it.next();
System.out.println(s+"="+map.get(s));
}
}
}
- 练习(利用Lambda表达式)
package dayOne;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
Set<String>st=map.keySet();
st.forEach( s-> System.out.println(s+" "+map.get(s)));
}
}
键值对遍历(entrySet)
依次获取Map集合里的每一个键值对对象(Entry),然后用键值对对象(Entry)通过getKey方法获取键
,通过getValue方法获取值
。
上述的Entry实际上是Map接口的一个内部接口
Map的entrySet方法可以把所有的键值对存到一个Set集合中
格式:Map集合名.entry()
返回值是一个set集合
注意,这里返回的set集合的泛型是Entry类型,即Set< Map.Entry< E,E > >
//Entry实际上是Map接口的一个内部接口,所以表达Entry时要用Map.来调用一下
Set<Map.Entry<String, String>> entries = map.entrySet();
//当然。这里的Map.也可以不写,但是要在代码上面进行一个导包
import java.util.Map.Entry
Set<Entry<String, String>> entries = map.entrySet();
- 键值对遍历例子(增强for)
package dayOne;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
//通过entrySet方法来获取键值对的Set集合
Set<Map.Entry<String, String>> entries = map.entrySet();
//通过增强for来遍历每一个键值对
for (Map.Entry<String, String> entry : entries) {
//再通过getKey和getValue来获取每一个键和值
System.out.println(entry.getKey()+"="+entry.getValue());
}
}
}
- 迭代器
package dayOne;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
Set<Map.Entry<String, String>> entries = map.entrySet();
Iterator<Map.Entry<String,String>>it=entries.iterator();
while (it.hasNext()){
Map.Entry<String,String>en=it.next();
String key=en.getKey();
String value=en.getValue();
System.out.println(key+"="+value);
}
}
}
- lambda表达式
package dayOne;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
Set<Map.Entry<String, String>> entries = map.entrySet();
entries.forEach((Map.Entry<String, String> stringStringEntry)-> {
String key=stringStringEntry.getKey();
String value=stringStringEntry.getValue();
System.out.println(key+"="+value);
}
);
}
}
Lambda表达式遍历
Map的forEach方法
default void forEach(BiConsumer<? super K, ?super V>action)
可以结合lambda遍历Map集合
BiConsumer
是一个函数式接口
map.forEach(new BiConsumer<String, String>() {
@Override
//这里的key就表示键,value就表示值
public void accept(String key, String value) {
}
});
- Map的forEach方法的底层原理
使用键值对遍历的方式,利用增强for循环来遍历Map集合
package dayOne;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
public class MapDemo1 {
public static void main(String[] args) {
Map<String,String>map=new HashMap<>();
map.put("one","1");
map.put("two","2");
map.put("three","3");
map.put("four","4");
map.forEach(new BiConsumer<String, String>() {
@Override
public void accept(String key, String value) {
System.out.println(key+"="+value);
}
});
}
}
HashMap
注意:HashMap的键如果存储的是自定义对象,那么就需要重写hashCode和equals方法
- HashMap的特点
1.HashMap是Map的一个实现类
2.HashMap没有特殊方法,所有方法都是Map接口里的,可以直接使用
3.特点都是由键决定的:无序,不重复,无索引
:这三点指的都是键的特点
4.HashMap跟HashSet的底层原理是一样的,都是哈希表结构
在底层,创建一个默认长度为16,默认加载因子为0.75的数组,默认初始化值为null,利用put方法添加数据,put方法的底层首先会创建一个Entry对象
(Entry1),Entry对象里记录着键和值,
注意:
只利用键计算哈希值,跟值无关
,即要保证键的唯一
利用哈希值计算出在数组中应存入的索引,
如果索引处为null,就把数据存入,
如果存入位置的值不是null,就表示存入位置有元素,
然后调用equals方法比较键
的属性值(只比较键的属性值)。
如果键的属性值一样,那么新的Entry对象
(Entry2)就会覆盖原来的Entry对象
(Entry1),这就是put方法隐藏的覆盖功能
如果不一样,
jdk8之前,存入时是新元素存入数组,老的元素挂在新元素的下面
jdk8之后,新元素直接挂在老元素的下面
额外:jdk8开始,当链表的长度超过8而且数组的长度超过64,链表就会自动转成红黑树
- 小结
练习
由于他这里题目要求同姓名,同年龄是同一个学生,而学生对象在键的位置,HashMap的底层也是哈希表,所以要想使同姓名,同年龄是同一个学生的话就要在学生类重写hashCode方法,使hashCode方法判断由地址值转变为属性值
package dayTwo;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
public class HashMapTest {
public static void main(String[] args) {
HashMap<Student,String>hs=new HashMap<>();
Student s1=new Student(18,"111");
Student s2=new Student(19,"222");
Student s3=new Student(20,"333");
Student s4=new Student(20,"333");
hs.put(s1,"河南");
hs.put(s2,"河南");
hs.put(s3,"河南");
hs.put(s4,"商丘");
//遍历集合
//lambda,这里的匿名内部类没有删
hs.forEach(new BiConsumer<Student, String>() {
@Override
public void accept(Student student, String s) {
System.out.println(student+"籍贯是"+s);
}
});
System.out.println("_______________");
//键找值
Set<Student> students = hs.keySet();
for (Student student : students) {
String s = hs.get(student);
System.out.println(student+"籍贯是"+s);
}
System.out.println("__________________");
//键值对
Set<Map.Entry<Student, String>> entries = hs.entrySet();
for (Map.Entry<Student, String> entry : entries) {
System.out.println(entry.getKey()+"籍贯是"+entry.getValue());
}
/*Student{age = 18, name = 111}籍贯是河南
Student{age = 19, name = 222}籍贯是河南
Student{age = 20, name = 333}籍贯是商丘
可以看到,由于s4由s1的属性值相同,
所以被hashCode方法赋予了一样的哈希值,
然后又因为put的特殊性,后面的对象把前面给覆盖了
*/
}
}
核心:创建一个Map集合,键为:景点,值为:投票次数,然后判断集合中是否包含该景点,不包含表示第一次出现,包含表示已经出现过了
package dayTwo;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Random;
import java.util.Set;
public class tongJi {
public static void main(String[] args) {
//创建数组存储景点
String[]arr={"a","b","c","d"};
Random rd=new Random();
//创建集合记录每个人选择的景点
ArrayList<String>list=new ArrayList<>();
for (int i = 0; i < 80; i++) {
int index=rd.nextInt(arr.length);
list.add(arr[index]);
}
HashMap<String,Integer>map=new HashMap<>();
//list使用增强for,把每个同学选择景点的次数记录到HashMao中
for (String name : list) {
if(map.containsKey(name)){
//集合里已经有景点了
Integer count = map.get(name);
count++;
map.put(name,count);
}else{
//集合里没有景点时
map.put(name,1);
}
}
System.out.println(map);
//求同学选择景点的最大值
//先定义一个最小边界,由于景点可能没人去,所以最小值为0
//这个循环是求最大值
int x=0;
Set<String> strings = map.keySet();
for (String s : strings) {
int y = map.get(s);
if(y>x){
x=y;
}
}
//这个循环是判断哪个景点的值跟最大值一样,并把景点打印出来
for (String s : strings) {
int y = map.get(s);
if(y==x){
System.out.println(s);
}
}
}
}
LinkedHashMap
无特殊方法,直接使用Map的方法
- LinkedHashMap特点:
由键决定:有序,不重复,无索引
有序的底层原理:底层数据结构仍然是哈希表,只是每个元素又额外多了一个双链表的机制记录存储的顺序
即每个元素都会像双向链表的结点一样,除了存储着数据外,还存储着其他元素的地址值,存入的第一个元素就是头结点,最后一个元素就是尾结点,遍历时从头结点开始,按照添加的顺序遍历,到尾结点结束
package dayTwo;
import java.util.LinkedHashMap;
public class LinkedHashMapTest {
public static void main(String[] args) {
LinkedHashMap<String,Integer>ihm=new LinkedHashMap<>();
ihm.put("two",2);
ihm.put("one",1);
ihm.put("three",3);
ihm.put("four",4);
ihm.put("five",5);
System.out.println(ihm);//{two=2, one=1, three=3, four=4, five=5}
}
}
TreeMap
无特殊方法,直接使用Map的方法
TreeMap跟TreeSet底层原理一样,都是红黑树结构
由键决定特性:不重复,无索引,可排序
这里的可排序是指:对键进行排序
默认按照键的大小进行排序,也可以自己规定键的排序规则
TreeMap的两种比较方式(Comparable,comparator)
这里有不懂的地方可以看TreeSet
的笔记
1.在自定义的JavaBean类中实现Comparable接口,指定比较规则
2.创建集合时传递Comparator对象,指定比较规则
- 练习
- 需求1
package dayTwo;
import java.util.Comparator;
import java.util.TreeMap;
public class TreeMapTest2 {
public static void main(String[] args) {
//需求1的降序排列
TreeMap<Integer,String>tm1=new TreeMap<>(new Comparator<Integer>() {
@Override
//o1:表示当前要添加的元素
//o2:表示已经在红黑树存在的元素
//返回值如果是负数,则会认为要添加的元素是小的,存在红黑树的左边
//返回值如果是正数,则会认为要添加的元素是大的,存在红黑树的右边
//返回值如果是0,则会认为要添加的元素已经存在,不存入红黑树
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
});
tm1.put(1,"one");
tm1.put(4,"four");
tm1.put(2,"two");
tm1.put(3,"three");
tm1.put(5,"five");
System.out.println(tm1);
}
}
- 需求2
//Student类
package dayTwo;
import java.util.Objects;
public class Student implements Comparable<Student>{
上面省略
//equals的重写
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return age == student.age && Objects.equals(name, student.name);
}
//hashCode的重写
@Override
public int hashCode() {
return Objects.hash(age, name);
}
public String toString() {
return "Student{age = " + age + ", name = " + name + "}";
}
//实现Comparable接口的重写方法
@Override
public int compareTo(Student o) {
//this:表示当前要添加的元素
//o:表示当前已经在红黑树存在的元素
int count=this.getAge()-o.getAge();
count=count==0?this.getName().compareTo(o.getName()):count;
return count;
}
//测试类
package dayTwo;
import java.util.TreeMap;
public class TreeMapTest {
public static void main(String[] args) {
TreeMap<Student,String>tm2=new TreeMap<>();
tm2.put(new Student(18,"abc"),"河南1");
tm2.put(new Student(21,"bac"),"河南2");
tm2.put(new Student(19,"bca"),"河南3");
tm2.put(new Student(20,"acb"),"河南4");
tm2.put(new Student(18,"cba"),"河南5");
tm2.put(new Student(18,"abc"),"商丘");
System.out.println(tm2);
/*
* {Student{age = 18, name = abc}=商丘,
* Student{age = 18, name = cba}=河南5,
* Student{age = 19, name = bca}=河南3,
* Student{age = 20, name = acb}=河南4,
* Student{age = 21, name = bac}=河南2}*/
}
}
练习
只要看到统计,就要想到计数器思想,但是计数器有弊端:如果统计的东西较多,需要定义大量变量,比较麻烦
所以有了新的统计思想:利用map集合统计,(HashMap,TreeMap)
即:键:表示要统计的内容,值:表示被选择的次数
注意:如果不要求统计后对结果进行排序,就默认使用HashMap,如果有要求,就使用TreeMap,因为HashMap的效率高于TreeMap
package dayTwo;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
public class TreeMapTongJi {
public static void main(String[] args) {
//创建字符串
String s="ababcabcdabcdae";
TreeMap<Character,Integer>tm=new TreeMap<>();
//遍历字符串,得到每一个字符串的值
for (int i = 0; i < s.length(); i++) {
char a=s.charAt(i);
//然后对每一个字符进行判断,如果在Map集合中存在,就将
//值加1,如果不存在,就把字符加到集合中去
if(tm.containsKey(a)){
//存在
int b=tm.get(a);
b++;
tm.put(a,b);
}else{
//不存在
tm.put(a,1);
}
}
System.out.println(tm);
//然后,为了达到题目中的效果,
// 使用StringBuilder或StringJoiner进行拼串
StringBuilder sb=new StringBuilder();
Set<Map.Entry<Character, Integer>> entries = tm.entrySet();
for (Map.Entry<Character, Integer> entry : entries) {
char a=entry.getKey();
int b=entry.getValue();
sb.append(a);
sb.append("("+b+")"+" ");
}
String s2 = sb.toString();
System.out.println(s2);
}
}
小结
这里的排序方式有不懂的可以看TreeSet的笔记
HashMap源码(不理解了重新看 下16,17)
先了解ieda中源码的一些东西
在源码中ctrl+f12可以打开一个菜单
蓝色的圆圈c(class)
,代表类,红色的圆圈m(method)
代表着方法
上面四个方法名和类名一样的,就是构造方法,下面一堆就是成员方法
这里的afterNodeRemoval
就是方法名,(Node< K,V >)
就是参数,void就是返回值
⬆
就表示重写的父类或接口中的方法
➡
表示从父类中继承下来的方法
黄色的圆圈f(field)
就表示属性值,有可能是成员变量,也有可能是常量
这种就是内部类
-
Node
-
在HashMap中,每一个元素都是一个Node的对象,而且在源码中,Node是Map.Entry的实现类,所以,也称一个Node(键值对)为Entry对象
四个成员变量
hash:哈希值
key:键
value:值
next:记录链表里下一个元素(结点)的地址值 -
如果是红黑树,那么红黑树里的结点元素叫做TreeNode
TreeNode也是HashMap的内部类,有五个成员变量
parent:记录父节点的地址值
left:记录左子节点的地址值
right:记录右子节点的地址值
prev:暂时不了解
red:判断是红色还是黑色,true为红色,false为黑色
除此之外,红黑树的元素还有
hash:哈希值
key:键
value:值
next:记录链表里下一个元素(结点)的地址值
HashMap底层是由数组,链表和红黑树组成,
-
HashMap的成员变量
-
table,组成HashMap底层的数组
里面装的元素就是Node对象。
|
这个常量表达了数组的默认长度是16
这个常量就是默认的加载因子0.75
-
HashMap的构造方法
空参构造,将加载因子0.75赋值给loadFactor,此时,底层的数组还没有创建。即使用空参构造创建HashMap集合时,底层还没有数组
当使用put方法向集合中添加第一个元素时,底层就创建数组了
put方法中的putVal有五个参数,
hash(key):键的哈希值
key:键
value:值
onlyAbsent:默认为false,表示重复的键所对应的值会被覆盖,为true时表示不被覆盖。
evict:暂时不用了解 -
源码详解
1.看源码之前需要了解的一些内容
Node<K,V>[] table 哈希表结构中数组的名字
DEFAULT_INITIAL_CAPACITY: 数组默认长度16
DEFAULT_LOAD_FACTOR: 默认加载因子0.75
HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值
1.2 红黑树中的键值对对象
包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色
1.3 数组中的键值对对象
分情况讨论
如果数组里的元素下挂的是链表
那就包含:
int hash; //键的哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个节点的地址值
如果数组里的元素下挂的是红黑树
那就包含:
int hash; //键的哈希值
final K key; //键
V value; //值
TreeNode<K,V> parent; //父节点的地址值
TreeNode<K,V> left; //左子节点的地址值
TreeNode<K,V> right; //右子节点的地址值
boolean red; //节点的颜色
2.添加元素
//空参构造,只是给集合一个加载因子,连数组都没创建
HashMap<String,Integer> hm = new HashMap<>();
//使用put方法后,HashMap集合在底层创建数组
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);
添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖
//put源码
//参数一:键
//参数二:值
//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
// true,表示老元素的值保留,不会覆盖
// false,表示老元素的值不保留,会进行覆盖
//putVal是put方法里调用的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
//定义一个局部变量,用来记录哈希表中数组的地址值。
//因为成员变量定义在堆里,而方法运行在栈里,如果不定义数组记录元素的话
//会在调用数组里的Node结点时就会反复在堆和栈之间重复运行,
//影响效率
Node<K,V>[] tab;
//临时的第三方变量,用来记录键值对对象的地址值
Node<K,V> p;
//表示当前数组的长度
int n;
//表示索引
int i;
//把哈希表中数组的地址值,赋值给局部变量tab
tab = table;
if (tab == null || (n = tab.length) == 0){
//resize方法的作用
//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,
//加载因子为0.75的数组
//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
//如果没有达到扩容条件,底层不会做任何操作
//如果达到了扩容条件,底层会把数组扩容为原先的两倍,
//并把数据(链表或红黑树的数据)全部转移到新的哈希表中
tab = resize();
//表示把当前数组的长度赋值给n
n = tab.length;
}
//拿着数组的长度跟键的哈希值进行计算,
//计算出当前键值对对象,在数组中应存入的位置
//这里的i就是index,即元素应存入的索引
i = (n - 1) & hash;
//获取数组中对应元素的数据
p = tab[i];
if (p == null){
//添加第一个元素
//底层会创建一个键值对对象,直接放到数组当中
tab[i] = newNode(hash, key, value, null);
}else {
//添加其他元素
Node<K,V> e;
K k;
//等号的左边:数组中键值对的哈希值
//等号的右边:当前要添加键值对的哈希值
//如果键不一样,此时返回false
//如果键一样,返回true
boolean b1 = p.hash == hash;
if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
e = p;
} else if (p instanceof TreeNode){
//判断数组中获取出来的键值对是不是红黑树中的节点
//如果是,则调用方法putTreeVal,
//把当前的节点按照红黑树的规则添加到树当中。
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
//如果从数组中获取出来的键值对不是红黑树中的节点
//表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//此时就会创建一个新的节点,挂在下面形成链表
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否超过8,
//如果超过8,就会调用方法treeifyBin
//treeifyBin方法的底层还会继续判断
//判断数组的长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//e: 0x0044 ddd 444
//要添加的元素: 0x0055 ddd 555
//如果哈希值一样,
//就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash && ((k = e.key)
== key || (key != null && key.equals(k)))){
break;
}
p = e;
}
}
//如果e为null,表示当前不需要覆盖任何元素
//如果e不为null,表示当前的键是一样的,值会被覆盖
//e:0x0044 ddd 555
//要添加的元素: 0x0055 ddd 555
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null){
//等号的右边:当前要添加的值
//等号的左边:0x0044的值
//即覆盖的是老的键值对的值,而不是把整个键值对给覆盖
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
//threshold:记录的就是数组的长度 * 0.75,
//哈希表的扩容时机 即threshold:16 * 0.75 = 12
if (++size > threshold){
resize();
}
//表示当前没有覆盖任何元素,返回null
return null;
}
TreeMap源码(不理解了重新看 下18,19)
TreeMap底层为红黑树,
红黑树每一个元素内部的属性分别为:
key:键
value:值
parent:记录父节点的地址值
left:记录左子节点的地址值
right:记录右子节点的地址值
color:判断是红色还是黑色,true为黑色,false为红色,TreeMap红黑树结点的初始值为黑色,TreeMap还会进行默认的调整,默认调整为红色
-
TreeMap集合的一些成员变量
comparator:表示比较的规则
root:记录红黑树根结点的地址值
size:表示集合的长度,也表示红黑树中结点的个数 -
TreeMap的空参构造
就是把null赋值给了comparator,表示没有比较器对象 -
TreeMap的带参构造
就是创建TreeMap的时候要传递比较器对象,然后把传递过来的比较器对象传递给成员变量comparator
1.TreeMap中每一个节点的内部属性
K key; //键
V value; //值
Entry<K,V> left; //左子节点
Entry<K,V> right; //右子节点
Entry<K,V> parent; //父节点
boolean color; //节点的颜色
2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
//比较器对象
private final Comparator<? super K> comparator;
//根节点
private transient Entry<K,V> root;
//集合的长度
private transient int size = 0;
3.空参构造
//空参构造就是没有传递比较器对象
public TreeMap() {
comparator = null;
}
4.带参构造
//带参构造就是传递了比较器对象。
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
5.添加元素
public V put(K key, V value) {
return put(key, value, true);
}
//put(key, value, true);
参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
true:覆盖
false:不覆盖
private V put(K key, V value, boolean replaceOld) {
//获取根节点的地址值,赋值给局部变量t
Entry<K,V> t = root;
//判断根节点是否为null
//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
if (t == null) {
//方法的底层,会创建一个Entry对象,把他当做根节点
addEntryToEmptyMap(key, value);
//表示此时没有覆盖任何的元素
return null;
}
//表示两个元素的键比较之后的结果
int cmp;
//表示当前要添加节点的父节点
Entry<K,V> parent;
//表示当前的比较规则
//如果我们是采取默认的自然排序,
//那么此时comparator记录的是null,cpr记录的也是null
//如果我们是采取比较器排序方式,
//那么此时comparator记录的是就是比较器
Comparator<? super K> cpr = comparator;
//表示判断当前是否有比较器对象
//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else {
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
} else {
//把键进行强转,强转成Comparable类型的
//要求:键必须要实现Comparable接口,如果没有实现这个接口
//此时在强转的时候,就会报错。
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把根节点当做当前节点的父节点
parent = t;
//调用compareTo方法,比较根节点和当前要添加节点的大小关系
cmp = k.compareTo(t.key);
if (cmp < 0)
//如果比较的结果为负数
//那么继续到根节点的左边去找
t = t.left;
else if (cmp > 0)
//如果比较的结果为正数
//那么继续到根节点的右边去找
t = t.right;
else {
//如果比较的结果为0,会覆盖
V oldValue = t.value;
if (replaceOld || oldValue == null) {
t.value = value;
}
return oldValue;
}
} while (t != null);
}
//就会把当前节点按照指定的规则进行添加
addEntry(key, value, parent, cmp < 0);
return null;
}
private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
Entry<K,V> e = new Entry<>(key, value, parent);
if (addToLeft)
parent.left = e;
else
parent.right = e;
//添加完毕之后,需要按照红黑树的规则进行调整
fixAfterInsertion(e);
size++;
modCount++;
}
private void fixAfterInsertion(Entry<K,V> x) {
//因为红黑树的节点默认就是红色的
x.color = RED;
//按照红黑规则进行调整,可以看一下红黑规则的笔记
//parentOf:获取x的父节点
//parentOf(parentOf(x)):获取x的爷爷节点
//leftOf:获取左子节点
while (x != null && x != root && x.parent.color == RED) {
//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
//目的:为了获取当前节点的叔叔节点
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//表示当前节点的父节点是爷爷节点的左子节点
//那么下面就可以用rightOf获取到当前节点的叔叔节点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
//叔叔节点为红色的处理方案
//把父节点设置为黑色
setColor(parentOf(x), BLACK);
//把叔叔节点设置为黑色
setColor(y, BLACK);
//把爷爷节点设置为红色
setColor(parentOf(parentOf(x)), RED);
//把爷爷节点设置为当前节点
x = parentOf(parentOf(x));
} else {
//叔叔节点为黑色的处理方案
//表示判断当前节点是否为父节点的右子节点
if (x == rightOf(parentOf(x))) {
//表示当前节点是父节点的右子节点
x = parentOf(x);
//左旋
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
//表示当前节点的父节点是爷爷节点的右子节点
//那么下面就可以用leftOf获取到当前节点的叔叔节点
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//把根节点设置为黑色
root.color = BLACK;
}
课堂思考问题
-
TreeMap添加元素的时候,键是否需要重写hashCode和equals方法?
此时是不需要重写的,因为在TreeMap源码中添加元素时没有用到hashCOde和equals方法
-
HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成的。
既然有红黑树,HashMap的键是否需要实现Compareable接口或者传递比较器对象呢?不需要。
因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的 -
TreeMap和HashMap谁的效率更高?
如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap(底层红黑树)的效率要更高
但是这种情况出现的几率非常的少。
一般而言,还是HashMap(底层数组,链表,红黑树)的效率要更高。 -
你觉得在Map集合中,java会提供一个如果键重复了,不会覆盖的put方法呢?
有,putIfAbsent这个方法就表示键重复时不会覆盖,与put相反
此时putIfAbsent本身不重要。
传递一个思想:
代码中的逻辑都有两面性,如果我们只知道了其中的A面,
而且代码中还发现了有变量可以控制两面性的发生。
那么该逻辑一定会有B面。习惯:
boolean类型的变量控制,一般只有AB两面,因为boolean只有两个值
int类型的变量控制,一般至少有三面,因为int可以取多个值。 -
三种双列集合,以后如何选择?
HashMap LinkedHashMap TreeMap
默认:HashMap(效率最高)
如果要保证存取有序:LinkedHashMap
如果要进行排序:TreeMap
可变参数
jdk5以后出的概念,可变参数即方法形参的个数是可以发生变化的
格式:属性类型...名字
,例如:int...a
可变参数底层就是一个数组,java自动创建好的,所以,可变参数本质上就是一个数组
package dayThree;
public class changeNumber {
public static void main(String[] args) {
System.out.println(getSum(1,2,3,4,5,6));//21
System.out.println(getSum(1,2,3));//6
System.out.println(getSum(1,2,3,9,10,29));//54
}
//可变参数
public static int getSum(int...number){
int sum=0;
//number既是参数名,也是数组名
for (int i = 0; i < number.length; i++) {
int a=number[i];
sum=sum+a;
}
return sum;
}
}
-
可变参数的细节
1.在方法的形参中,最多只能写一个可变参数
错误的写法:public static int getSum(int...a,int...b)
2.在方法中,如果除了可变参数以外还有其他的形参,那么可变参数要写在最后
public static int getSum(int a,int b,int...number)
-
小结
Collections
Collections
是一个java.util包的集合工具类
注:Collections是工具类,不是集合
- Collections常用的Api
方法名 | 说明 |
---|---|
public static boolean addAll(Collection< T > c,T... elements ) | 批量添加元素(只能给单列集合批量添加) |
public static void shuffle(List<?> list) | 打乱list集合的顺序 |
public static < T > void sort(List< T > list) | 排序 |
public static < T > void sort(List< T > list,Comparator< T > c) | 根据指定的规则进行排序 |
public static < T > int binarySearch(List< T > list,T key) | 以二分查找法查找元素 |
public static < T > void copy(List< T > dest,List< T > src) | 拷贝集合中的元素 |
public static < T > void fill(List< T > list,T obj) | 使用指定的元素填充集合 |
public static < T > void max/min(Collection< T > coll) | 根据默认的自然排序获取最大/小值 |
public static void swap(List<?> list, int i, int j) | 交换集合中指定位置的元素 |
package dayThree;
import java.util.ArrayList;
import java.util.Collections;
public class CollectionsTest {
public static void main(String[] args) {
ArrayList<String>list=new ArrayList<>();
//addAll,批量添加
Collections.addAll(list,"1","2","3","4");
Collections.addAll(list,"5","6");
System.out.println(list);//[1, 2, 3, 4, 5, 2]
//shuffle,打乱顺序
Collections.shuffle(list);
//sort,分两种,一种是默认排序,一种是指定方式排序
Collections.sort(list);
System.out.println(list);//[1, 2, 2, 3, 4, 5]
// Collections.sort(list, new Comparator<String>() {
// @Override
// //o1:表示当前要添加的元素
// //o2:表示已经在集合中存在的元素
// //返回值如果是负数,则会认为要添加的元素是小的,存在集合的左边
// //返回值如果是正数,则会认为要添加的元素是大的,存在集合的右边
// public int compare(String o1, String o2) {
// for (int i = 0; i < o2.length(); i++) {
// //此时为逆序排列
// if(o1.charAt(i)>o2.charAt(i)){
// return -1;
// }else{
// return 1;
// }
// }
// return 0;
// }
// });
System.out.println(list);
//binarySearch,以二分查找法查找元素,
int index=Collections.binarySearch(list,"5");
System.out.println(index);
//copy,复制元素,
//Collections.copy(list2,list1); 把list1中的元素拷贝到list2中
//会覆盖原来的元素
//注意:如果list1的长度大于list2的长度,就会报错
ArrayList<Integer>list1=new ArrayList<>();
ArrayList<Integer>list2=new ArrayList<>();
Collections.addAll(list1,1,2,4,5,3,5,2,34,5,23,42,3);
Collections.addAll(list2,10,0,0,51,32,51,22,4,25,3,2,31);
Collections.copy(list2,list1);
System.out.println(list1);//[1, 2, 4, 5, 3, 5, 2, 34, 5, 23, 42, 3]
System.out.println(list2);//[1, 2, 4, 5, 3, 5, 2, 34, 5, 23, 42, 3]
//fill,使用指定的数据填充集合
//把集合中的所有数据都改成指定数据
// Collections.fill(list,"1");
// System.out.println(list);//[1, 1, 1, 1, 1, 1]
//max/min,获取最大/小值
System.out.println(Collections.max(list));//6
System.out.println(Collections.min(list));//1
//swap,交换集合中指定位置的元素
Collections.swap(list,0,5);
System.out.println(list);//[6, 2, 3, 4, 5, 1]
}
}
综合练习
//第一种随机方式
package dayFour;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
public class practiceOne {
public static void main(String[] args) {
ArrayList<String>list=new ArrayList<>();
Collections.addAll(list,"111","121","131","141","161","151","171");
Random rd=new Random();
for (int i = 0; i < 7; i++) {
int x=rd.nextInt(list.size());
System.out.println(list.get(x));
}
}
}
//第二种随机方式
package dayFour;
import java.util.ArrayList;
import java.util.Collections;
public class practiceOne {
public static void main(String[] args) {
ArrayList<String>list=new ArrayList<>();
Collections.addAll(list,"111","121","131","141","161","151","171");
Collections.shuffle(list);
System.out.println(list.get(0));
}
}
package dayFour;
import java.util.ArrayList;
import java.util.Collections;
public class practiceOne {
public static void main(String[] args) {
ArrayList<Integer>list1=new ArrayList<>();
//list1集合是一个百分比容器,1代表男生,0代表女生
Collections.addAll(list1,1,1,1,1,1,1,1,0,0,0);
ArrayList<String>man=new ArrayList<>();
ArrayList<String>women=new ArrayList<>();
Collections.addAll(man,"n1","n5","n2","n4","n3");
Collections.addAll(women,"w1","w5","w2","w4","w3");
//每次点名都随机一次集合排序
Collections.shuffle(list1);
Collections.shuffle(man);
Collections.shuffle(women);
if(list1.get(0)==1){
System.out.println(man.get(0));
}else{
System.out.println(women.get(0));
}
}
}
package dayFour;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Random;
public class practiceOne {
public static void main(String[] args) {
ArrayList<String>man=new ArrayList<>();
Collections.addAll(man,"n1","n5","n2","n4","n3","w1","w5","w2","w4","w3");
ArrayList<String>listCopy=new ArrayList<>();
Random rd=new Random();
int count= man.size();
for (int j = 1; j <= 10; j++) {//外层循环,控制循环轮数
System.out.println("____________这是第"+j+"轮__________");
for (int i = 0; i < count; i++) {//内层循环,使随机到的元素不重复
int index=rd.nextInt(man.size());//每次循环获取一个随机数
String x = man.remove(index);//把获取到的随机数对应的元素赋值给x,同时在集合中删除,防止重复
listCopy.add(x);//把x赋值给空集合,为下一轮循环做准备
System.out.println(x);
}
man.addAll(listCopy);//把带有man集合所有元素的空集合的所有元素赋值给man
listCopy.clear();//清空空集合
}
}
}
集合的嵌套
这里用双列集合,但是一个键对应多个值,所以这里的值的元素类型要用单列集合,几个单列集合存放多个元素充当值
package dayFour;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
public class practiceTwo {
public static void main(String[] args) {
HashMap<String, ArrayList<String>>hm=new HashMap<>();
ArrayList<String>list1=new ArrayList<>();
ArrayList<String>list2=new ArrayList<>();
ArrayList<String>list3=new ArrayList<>();
Collections.addAll(list1,"南京","苏州","扬州","巴拉巴拉");
Collections.addAll(list2,"郑州","开封","洛阳","商丘","巴拉巴拉");
Collections.addAll(list3,"青岛","济南","日照","巴拉巴拉");
hm.put("江苏",list1);
hm.put("河南",list2);
hm.put("山东",list3);
//lambda表达式遍历,这里应该还有拼串
hm.forEach((s, strings) ->System.out.println(s+"="+strings));
}
}