目录
1.集合
集合进阶-06-ArrayList源码分析_哔哩哔哩_bilibili
08、Java面向对象应用API:ArrayList集合的概述_哔哩哔哩_bilibili
分为双列与单列
单列集合体系结构顶层接口:Collection
上图中:
- 有序与无序的概念并非之前数组的那种,可以通过首字母排序等等,仅仅是表示存与取时的顺序,比如存的时候是 "1" "2" "3",取的时候也是 "1" "2" "3"
- Vector在JDK1.2已过时/弃用,所以说项目中是见不到Vector的,如果见到了可以去找黑马阿伟老师,他直播倒立洗头
Collection常用方法
public class MyTest {
public static void main(String[] args) {
Collection arraylist = new ArrayList();
System.out.println(arraylist);
arraylist.add("哈哈哈");
arraylist.add("嘻嘻嘻");
arraylist.add("嘿嘿嘿");
System.out.println(arraylist);
arraylist.clear();
System.out.println(arraylist);
arraylist.add("哈哈哈");
arraylist.add("嘻嘻嘻");
arraylist.add("嘿嘿嘿");
arraylist.add(1);
arraylist.add(1);
arraylist.add(3);
System.out.println(arraylist);
arraylist.remove("嘻嘻嘻");
arraylist.remove(1);
System.out.println(arraylist);
System.out.println(arraylist.contains("嘿嘿嘿"));
System.out.println(arraylist.isEmpty());
System.out.println(arraylist.size());
}
}
add()
clear()
contains()(重点)
- 集合中存储自定义对象,使用contains()判断时,为什么要重写equals()?
- 看contains()源码,collection是接口,所以去找实现类
既然contains()的底层源码是用equals()比较元素地址从而确定集合是否包含元素,那么自定义对象的地址肯定总是不同的,就不能比较对象地址,而应该比较属性值从而区分,所以要重写equals()
- 代码实现
public class Student {
private String name;
private Integer age;
public Student() {
}
public Student(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName(){
return this.name;
}
public Integer getAge(){
return this.age;
}
public void setName(String name){
this.name = name;
}
public void setAge(Integer age){
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name) && Objects.equals(age, student.age);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
public class MyTest {
public static void main(String[] args) {
Collection arrlist = new ArrayList();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
System.out.println(s1 == s2);// false
Student s4 = new Student("冰糖", 25);
System.out.println(arrlist.contains(s4));// true
}
}
Collection的遍历方式(三种)
- 迭代器遍历
- 增强for遍历
- Lambda表达式遍历
由于Collection下的Set集合体系结构都是无索引的,所以不能使用之前数组用的fori循环,不过List集合还是能用的
Iterator (迭代器遍历)
迭代就是遍历,一个一个的找
迭代器不依赖索引
易错点
- 迭代器遍历完毕,指针不会复位,想要重新遍历只能再new一个迭代器
- 循环中只能使用一次next(),或者说不能在next(),继续next()会越界
- 迭代器遍历时,不能用集合的方法进行增加或者删除,删除应该使用Iterator的remove(),如果要添加,暂时没有办法
常用方法
注意,next()做了两件事:
- 获取当前位置元素
- 指针指向下一元素位置
public class MyTest {
public static void main(String[] args) {
Collection<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
Iterator<Student> it = arrlist.iterator();
while (it.hasNext()) {
Student stu = it.next();
System.out.println(stu);
}
}
}
增强for遍历
- 增强for的底层就是迭代器,为了简化迭代器的书写而生
- 涉及到for,所以双列集合Set不能使用,单列集合和数组能直接使用
public class MyTest {
public static void main(String[] args) {
Collection<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
for (Student stu : arrlist) {
System.out.println(stu);
}
}
}
注意点
Lambda表达式遍历
JDK8开始提供
先用匿名内部类实现
public class MyTest {
public static void main(String[] args) {
Collection<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.forEach(new Consumer<Student>() {
@Override
public void accept(Student s){
System.out.println(s);
}
});
// Collection<String> arrList = new ArrayList<>();
// arrList.add("12");
// arrList.add("23");
// arrList.add("34");
//
// arrList.forEach(new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// });
}
}
使用Lambda表达式简化
public class MyTest {
public static void main(String[] args) {
Collection<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.forEach( s -> System.out.println(s) );
// Collection<String> arrList = new ArrayList<>();
// arrList.add("12");
// arrList.add("23");
// arrList.add("34");
//
// arrList.forEach(new Consumer<String>() {
// @Override
// public void accept(String s) {
// System.out.println(s);
// }
// });
}
}
List
List集合特点
List常用方法
- Collection的方法List都继承了
- List集合因为有索引,所以多了一些索引操作的方法
代码实现
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
System.out.println(arrlist);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(3, s4);
System.out.println(arrlist);
arrlist.remove(2);
System.out.println(arrlist);
arrlist.set(0, new Student("黄小桃", 20));
System.out.println(arrlist);
System.out.println(arrlist.get(1));
}
}
remove()
- 提问:remove(1),是删除索引1上的元素,还是删除值为1的元素?
答:删除索引1上的元素。
List的遍历方式(五种)
- 迭代器遍历
- 列表迭代器遍历(List独有)
- 增强for遍历
- Lambda表达式遍历
- 普通for循环(因为List集合存在索引)
迭代器遍历
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.add(3, s4);
System.out.println(arrlist);
Iterator<Student> it = arrlist.iterator();
while (it.hasNext()) {
Student stu = it.next();
System.out.println(stu);
}
}
}
列表迭代器(ListIterator)遍历(List独有)
ListIterator()接口
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.add(3, s4);
System.out.println(arrlist);
ListIterator<Student> lslt = arrlist.listIterator();
while (lslt.hasNext()) {
Student stu = lslt.next();
System.out.println(stu);
}
}
}
增强for遍历
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.add(3, s4);
System.out.println(arrlist);
for (Student s : arrlist) {
System.out.println(s);
}
}
}
Lambda表达式遍历
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.add(3, s4);
System.out.println(arrlist);
// 匿名内部类实现
arrlist.forEach(new Consumer<Student>(){
@Override
public void accept(Student s){
System.out.println(s);
}
});
// Lambda表达式简化
arrlist.forEach( s -> System.out.println(s));
}
}
普通for循环(因为List集合存在索引)
public class MyTest {
public static void main(String[] args) {
List<Student> arrlist = new ArrayList<>();
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("冰糖", 25);
Student s4 = new Student("宁尚菱", 39);
arrlist.add(s1);
arrlist.add(s2);
arrlist.add(s3);
arrlist.add(3, s4);
System.out.println(arrlist);
for (int i = 0; i < arrlist.size(); i++) {
System.out.println(arrlist.get(i));
}
}
}
Set
Set接口中方法与Collection的基本一致,所以不需要单独学习了,如果有需要自己查文档即可
Set集合的实现类
HashSet
底层原理
- 默认创建一个名为table的数组
此数组默认长度为16,默认加载因子为0.75
- 计算得出一个整数,表示元素应存入的位置
注意下方公式的运算符是位运算符&
有的教程教的是(数组长度 - 1) % 哈希值,这个要看源码怎么写的
- 存入元素的逻辑
补充一句,如果链表是多个,新加入的元素会与链表中所有元素进行equals比较,但凡有一个相同的就会舍弃
加载因子
如上一小节,加载因子0.75
意思是:默认数组长度16,当元素个数超过 16 x 0.75 = 12时,就会成倍扩容 16 x 2 = 32
链表转红黑树模型
当某个链表长度大于8,且数组长度大于64时,此链表转成红黑树
何时重写hashcode和equals方法?
hashcode() 和 equals()默认都是用对象地址值进行计算比较,没有意义,需要重写,利用属性值进行比较
HashSet为什么存和取的顺序不一样?
再重申一遍,只是大概率不一样,还是有小概率是一样的
如下图是HashSet遍历顺序
HashSet为什么没有索引?
还是不够纯粹,JDK8之后,由 数组 + 链表 + 红黑树共同组成,下标索引意义不大了,所以没有设计索引
HashSet是利用什么机制保证数据去重的?
- 先通过HashCode()计算出哈希值,通过哈希值计算出数组元素位置
- 在通过equals()方法,将数组某位置中挂的链表的每个元素进行比较属性值是否相同
LinkedHashSet
就用Collection顶级父接口的方法够用了
特点
与HashSet的区别就只是存储和取出的元素顺序一致了
保证存取顺序
- 遍历集合的时候并不会从table数组的头开始,而是从双链表的头结点开始
TreeSet
一样,用Collection接口的方法就够用
特点
默认排序规则
排序方式(两种)
自然排序
自然排序是TreeSet的默认排序方式。它要求存储在TreeSet中的元素实现Comparable接口,并覆写compareTo(Object o)方法。当元素被添加到TreeSet中时,它们会根据compareTo方法定义的逻辑被自动排序。对于标准的Java类(如String、Integer等),它们已经实现了Comparable接口并定义了自己的自然顺序。
import java.util.TreeSet;
public class NaturalOrderingExample {
public static void main(String[] args) {
TreeSet<Integer> numbers = new TreeSet<>();
numbers.add(10);
numbers.add(2);
numbers.add(15);
numbers.add(5);
System.out.println("Numbers in natural order: " + numbers); // 输出将会是排序后的:[2, 5, 10, 15]
}
}
在上面的例子中,Integer
类已经实现了Comparable
接口,因此TreeSet
会根据整数的自然顺序进行排序。
我再来个例子
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
TreeSet<Student> ts = new TreeSet<>();
ts.add(s3);
ts.add(s2);
ts.add(s1);
System.out.println(ts);
}
}
public class Student implements Comparable<Student>{
private String name;
private int age;
public Student(){};
public Student(String name, int age){
this.name = name;
this.age = age;
};
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
public String getName(){
return name;
}
public int getAge(){
return age;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int compareTo(Student o) {
return this.getAge() - o.getAge();
}
}
定制排序(比较器排序)
定制排序允许你定义自己的排序规则,而不是依赖元素的自然顺序。这可以通过在TreeSet
的构造函数中传递一个Comparator
对象来实现。这个Comparator
对象需要覆写compare(Object o1, Object o2)
方法,以定义排序的逻辑。
import java.util.Comparator;
import java.util.TreeSet;
public class CustomOrderingExample {
public static void main(String[] args) {
Comparator<String> customComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length(); // 按字符串长度排序
}
};
TreeSet<String> names = new TreeSet<>(customComparator);
names.add("Anna");
names.add("John");
names.add("Christopher");
names.add("Mike");
System.out.println("Names in custom order: " + names); // 输出将会按字符串长度排序:[John, Mike, Anna, Christopher]
}
}
在这个例子中,我们创建了一个自定义的Comparator
来按照字符串的长度对TreeSet
中的元素进行排序,而不是按照字符串的自然顺序(字典顺序)。
小结
TreeSet提供了灵活的排序机制,既可以利用Java对象的自然顺序,也可以通过提供自定义的Comparator来实现特定的排序逻辑。选择哪种排序方式取决于你的具体需求。自然排序是对实现了Comparable接口的对象的一种直观排序方式,而定制排序则提供了更多的灵活性,允许定义更加复杂的排序规则。
自然排序用的多,默认使用自然排序能解决大部分情况,实在不行再用定制排序
如果两种都写了,是以定制排序为准
Map
双列集合的特点
键与值
键与值一 一对应
一个老公只能对应一个老婆
键值对 /键值对对象 /Entry对象
在Java中,特有名词叫做Entry
常用API
将双列集合体系的顶层接口Map的方法学会,下面实现类都不需要再学新的方法了,够用
遍历方式(五种)
- 键找值
- 增强for
- 迭代器
- Collection.forEach(匿名内部类/Lambda表达式)
- 键值对
- Map.forEach(匿名内部类/Lambda表达式)
键找值:增强for遍历
我也是醉了,B站1080p的视频,pixpin的截图软件,然后粘贴到csdn,结果糊成这个样子
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
Map<String,Integer> map = new HashMap<>();
map.put(s1.getName(), s1.getAge());
map.put(s2.getName(), s2.getAge());
map.put(s3.getName(), s3.getAge());
// 键找值
// 获取集合中所有键,将所有键存入一个单列集合
Set<String> keys = map.keySet();
for (String key : keys) {
Integer ageValue = map.get(key);
System.out.println(key + "遇到他时才" + ageValue + "岁。");
}
}
}
键找值:迭代器遍历
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
Map<String,Integer> map = new HashMap<>();
map.put(s1.getName(), s1.getAge());
map.put(s2.getName(), s2.getAge());
map.put(s3.getName(), s3.getAge());
// 键找值
// 获取集合中所有键,将所有键存入一个单列集合
Set<String> keys = map.keySet();
Iterator<String> it = keys.iterator();
while (it.hasNext()) {
String key = it.next();
Integer ageValue = map.get(key);
System.out.println(key + "遇到他时才" + ageValue + "岁。");
}
}
}
键找值:Collection.forEach()遍历
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
Map<String,Integer> map = new HashMap<>();
map.put(s1.getName(), s1.getAge());
map.put(s2.getName(), s2.getAge());
map.put(s3.getName(), s3.getAge());
// 键找值
// 获取集合中所有键,将所有键存入一个单列集合
Set<String> keys = map.keySet();
keys.forEach(new Consumer<String>() {
@Override
public void accept(String key) {
Integer ageValue = map.get(key);
System.out.println(key + "遇到他时才" + ageValue + "岁。");
}
});
}
}
键值对遍历 (entrySet)
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
Map<String,Integer> map = new HashMap<>();
map.put(s1.getName(), s1.getAge());
map.put(s2.getName(), s2.getAge());
map.put(s3.getName(), s3.getAge());
// 键值对
// 获取集合中所有Entry对象,存入一个单列集合中
Set<Map.Entry<String,Integer>> entries = map.entrySet();
// 遍历entries集合
for (Map.Entry<String, Integer> entry : entries) {
// 利用entry调用getter()获取键和值
String key = entry.getKey();
Integer ageValue = entry.getValue();
System.out.println(key + "遇到他时才" + ageValue + "岁。");
}
}
}
Map.forEach()遍历
public class MyTest {
public static void main(String[] args) {
Student s1 = new Student("黄小桃", 19);
Student s2 = new Student("王心妍", 21);
Student s3 = new Student("天婵", 23);
Map<String,Integer> map = new HashMap<>();
map.put(s1.getName(), s1.getAge());
map.put(s2.getName(), s2.getAge());
map.put(s3.getName(), s3.getAge());
// Map.forEach()
// map.forEach(new BiConsumer<String, Integer>() {
// @Override
// public void accept(String key, Integer ageValue) {
// System.out.println(key + "遇到他时才" + ageValue + "岁。");
// }
// });
map.forEach((String key, Integer ageValue) ->
System.out.println(key + "遇到他时才" + ageValue + "岁。"));
}
}
HashMap
特点
跟HashSet是一样的,就是多了个键
练习
LinkedHashMap
特点
跟linkedHashSet是一样的,就是多了个键
TreeMap
特点
跟TreeSet一样,就是多了个键,然后是对键进行排序而不是对值进行排序
Collections工具类
2.数据结构小结
数据结构:计算机存储、组织数据的方式,是指数据相互之间是以什么方式排列在一起的
每种集合的底层数据结构不同,为了更加方便的管理和使用数据,开发人员需要学习
一般情况下,精心选择的数据结构可以带来更高的运行或存储效率
常见的数据结构
- 栈
- 队列
- 数组
- 链表
- 二叉树
- 二叉查找树
- 平衡二叉树
- 红黑树
- B树
- 哈希表
栈
栈的特点:先进后出,后进先出
- 可以看成一个杯子状,一端开口(栈顶),一端封闭(栈底)
- 数据入栈的过程叫做:压栈 / 进栈
- 数据出栈的过程叫做:弹栈 / 出栈
- 栈中最上 / 下方的数据叫做:栈顶元素 / 栈底元素
队列
栈的特点:先进先出,后进后出
- 一端开口(后端) 一端开头(前端)
- 数据从后端进入队列的过程叫做:入队列
- 数据从前端离开队列的过程叫做:出队列
数组
链表
与数组刚好对立,相对于数组而言增删快,查询慢的数据模型
结点
- 创建一个链表,第一个结点叫做:头结点
此时就可以通过头结点找到11结点
单向链表与双向链表
双向链表对单向链表的查询进行了优化,如果是根据第N个查找,会先做判断,是距离首近还是距离尾近,如果是距离尾近那就从尾开始一个一个查找
二叉树
全是节点(跟链表的结点是一个意思,英文都叫做Node)
- 每个节点下都有两个节点(除了最底层)
- 每个节点包含1个值和三个地址值
- 没有父节点了或者没有子节点了地址值就为null
- 度:每一个节点的子节点数量
- 树高:树的总层数
- 根节点:最顶层的结点
- 根节点的左子树:蓝色虚线
- 当然,除了最后两层,每个节点都有自己的左右子树
二叉树遍历方法(四种)
是所有二叉树的通用遍历方法
- 前序遍历
- 中序遍历(最常用)
- 后序遍历
- 层序遍历
前序遍历
前序遍历:先从根节点开始,然后无时无刻都在从左到右遍历,优先左
如下图:
遍历结果应该是:20、18、16、19、23、22、24
中序遍历(最常用)
中序遍历:从最左边的子节点开始,然后每次按照左中右的顺序遍历
如下图:
遍历结果因该是:16、18、19、20、22、23、24
发现:居然是从小到大的顺序(正序)遍历的,果然很重要
后序遍历
后序遍历:从最左边的子节点开始,然后每次按照左右中的顺序遍历
如下图:
遍历结果因该是:16、19、18、22、24、23、20
层序遍历
层序遍历:层数从上到下,每层从左到右
如下图:
二叉查找树
特点
添加节点
规则:小的存左边,大的存右边,一样的不存
查找节点
规则:先跟根节点比较,大于根节点则向右边找,小于根节点向左边找
弊端
二叉查找树的弊端:查询效率低
平衡二叉树
在二叉查找树的基础上又多了一个规则:
任意节点的左右子树高度差 <= 1
这两个是平衡二叉树
这两个不是
旋转机制
- 有没有想过,平衡二叉树是如何保持平衡的?
旋转机制分为 左旋 和 右旋
触发时机:当添加一个节点之后,该树不再是一颗平衡二叉树,将会进行左旋或者右旋
确定支点(易错点)
如下图,节点10的左子树高度0,右子树高度2, 2 - 0 > 1,所以不平衡
此时达到了平衡二叉树的旋转机制的要求
旋转需要找到支点,支点选择的逻辑新手很容易错?
大部分人错的逻辑:"左边少,右边多,所以应该给左边补一个,7下去,10上来当根节点"。
结果虽然平衡了,但是逻辑错了,这种逻辑无法应对以后更复杂的场景
正确逻辑:"最后一个加入的节点是12,从12往上层找第一个不平衡的节点,嗯...11节点是平衡的,10节点不平衡啊,那么10节点就是支点了,既然10节点的左子树的0少于右子树的2,所以左旋,给11左旋到10的左边,12向上提升一层。"
再来一题
找支点的逻辑:从12开始往上找11,11左右差是1,往上找10,10 是平衡的,往上找7,不平衡,那么7为支点,7的左子树高度1,右子树3,左旋,原根节点7,变成了现在的根节点10,现根节点10的左子节点9出让给旧根节点7当做右子节点
需要旋转的情况(四种)
- 左左:当根节点的左子树的左子树有节点插入,导致二叉树不平衡
- 左右:当根节点的左子树的右子树有节点插入,导致二叉树不平衡
- 右右:当根节点的右子树的右子树有节点插入,导致二叉树不平衡
- 右左:当根节点的右子树的左子树有节点插入,导致二叉树不平衡
- 左左(一次右旋能解决)
给下面的平衡二叉树添加一个1或者3
变成下面两种不平衡的情况
从1 / 3往上找,最终找到7不平衡,以7作为支点右旋一次
- 左右(先局部左旋,变成左左,再整体右旋一次)
给下图添加一个6
变成这样不平衡了
如果从支点7右旋一次,并不能解决问题
应该先局部左旋,将左右变成左左的情况,再右旋一次
- 右右(一次左旋)
- 右左(先局部右旋,变成右右,再整体左旋一次)
三棵树的演变
普通二叉树只能依靠遍历查询 -> 二叉查找树单链查询效率低 -> 平衡二叉树
红黑树
误区:并不是平衡二叉树
与平衡二叉树比较
平衡二叉树:
- 通过高度与旋转控制平衡
- 增加节点效率低,因为动不动就旋转,甚至是多次旋转
红黑树:
- 是一个自平衡的二叉查找树
- 平衡规则没有平衡二叉树那么严格,有一套自己的红黑规则
红黑规则(重点)
- 每一个节点要么黑色要么红色
想比之前的二叉树,多了个颜色属性
- 根节点必须是黑色
- 如果一个节点没有子节点或者没有父节点,则该节点相应的指针属性值为Nil,这些Nil视为叶节点,每个叶节点(Nil)是黑色的
比如下图中的根节点是没有父节点的,那么父节点地址为空,专业名词叫Nil
- 叶子节点是没有数据的,查找和遍历红黑树的时候也不会考虑叶子节点,几乎毫无意义,唯一的作用是用于第五条判断红黑树是否满足规则
- 如果一个节点是红色,那么它的子节点必须是黑色(不能出现两个红色节点相连的情况)
两个黑的是可以的,两个红的不能相连
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
简单路径:从根节点走到最下面的Nil节点,不能回头走路线
如下图的四条路线都是简单路径,当然不止这四条
添加节点默认是红色则效率高
先说结论,红黑树添加红色节点效率 高于 添加黑色节点
下面我将添加三个黑色节点和添加三个红色节点进行对比。
- 添加三个黑色节点,需要调整两次
- 添加三个红色节点,需要调整一次
- 添加三个黑色节点(需要调整两次)
首先添加一个黑色20节点
再添加黑色的18
违背了第五条,节点的到所有Nil节点的简单路径上黑色节点数量不同了
此时要将黑18修改成红18
再添加一个黑23
同样也违背了第五条
将黑23改成红23
- 添加三个红色节点(只需要调整一次)
首先添加一个红色20
违背了结论2,根节点必须是黑色,将红20修改成黑20
再添加一个红色18
再添加一个红色23
添加节点规则(难点)
上一小结已经讲解了一条添加节点的规则:红黑树添加红色节点效率 高于 添加黑色节点
所以,红黑树的设计者规定,添加的节点默认是红色
黑马阿伟老师讲的还是很妙的
集合进阶-13-数据结构(红黑树、红黑规则、添加节点处理方案详解)_哔哩哔哩_bilibili
不方便做笔记,有需要的可以去看一下,总时长13分钟左右 23:00 - 36:00
为什么红黑树的增删改查效率都高?
哈希表(难点)
Java中哈希表的组成
哈希值(重点)
哈希值:对象的整数表现形式
哈希表结构添加数据的时候,会先生成一个数组,但并不是从数组的头元素位置添加,具体添加到哪个位置还是需要计算的
对象的哈希值特点
哈希碰撞
小概率时间,比如下图,就有8亿多个对象的哈希值出现相同现象
代码测试
3.泛型
JDK5中引入的特性,可以在编译阶段约束操作的数据类型,并进行检查
泛型的格式<数据类型>
注意:泛型只能支持引用数据类型,如果是int这种基本数据类型,需要先装箱成包装类
可以把泛型理解成看门的大爷,比如下图,给集合加一个<String>,此集合被约束元素只能是String类型
泛型的好处
泛型的擦除(重点)
Java中的泛型是伪泛型
Java中的泛型只是像保安一样在编译阶段检查一下,比如上面的ArrayList<String> list;
将所有要进入集合的元素限制为String类型,编译期过了之后,进入运行期,集合依旧会将元素当做Object看待,只不过在操作的时候,会将Object强转回String
如下图,编译成.class文件后,泛型就无了
泛型的细节
泛型类
使用场景:当一个类中,某个变量的数据类型不确定时,就可以使用带有泛型的类
如下图,创建类的时候不确定,创建对象的时候再确定
TEKV
这四个可以理解为变量,但是不是用来记录数据的,而是记录数据的类型,可以写成:T、E、K、V等
- 模拟ArrayList部分源码
泛型方法
泛型接口
泛型通配符?
super与extends
可以限定类型的范围
可变参数
JDK5出现
方法形参的个数是可以改变的
格式:属性类型...属性名
int...args
先看一个业务场景:计算n个数的和
以前的时候是这样子写比较麻烦
现在是这样子写