【JavaSE】基础知识复习(四)

目录

1.常用类和基础API

API初识

为什么要学别人写好的程序?

什么是包?

注意事项

字符串相关类之不可变字符序列:String

String创建对象方式(两种)

具体代码

String的常用方法

String的注意事项

String是不可变的

以" "方式创建的字符串只存在一份

new或者运算出来的字符串对象不只存在一份

2.集合

单列集合体系结构顶层接口:Collection

Collection常用方法

add()

clear()

contains()(重点)

Collection的遍历方式(三种)

Iterator (迭代器遍历)

易错点

常用方法

增强for遍历

注意点

Lambda表达式遍历

先用匿名内部类实现

使用Lambda表达式简化

List

List集合特点

List常用方法

代码实现

remove()

List的遍历方式(五种)

迭代器遍历

列表迭代器(ListIterator)遍历(List独有)

增强for遍历

Lambda表达式遍历

普通for循环(因为List集合存在索引)

Set

Set集合的实现类

HashSet

底层原理

加载因子

链表转红黑树模型

何时重写hashcode和equals方法?

HashSet为什么存和取的顺序不一样?

HashSet为什么没有索引?

HashSet是利用什么机制保证数据去重的?

LinkedHashSet

特点

保证存取顺序

TreeSet

特点

默认排序规则

排序方式(两种)

自然排序

定制排序(比较器排序)

小结

Map

双列集合的特点

键与值

键与值一 一对应

键值对 /键值对对象 /Entry对象

常用API

遍历方式(五种)

键找值:增强for遍历

键找值:迭代器遍历

键找值:Collection.forEach()遍历

键值对遍历 (entrySet)

Map.forEach()遍历

HashMap

特点

练习

LinkedHashMap

特点

TreeMap

特点

Collections工具类

数据结构小结

常见的数据结构

队列

数组

链表

结点

单向链表与双向链表

二叉树

二叉树遍历方法(四种)

前序遍历

中序遍历(最常用)

后序遍历

层序遍历

二叉查找树

特点

添加节点

查找节点

弊端

平衡二叉树

旋转机制

确定支点(易错点)

需要旋转的情况(四种)

三棵树的演变

红黑树

与平衡二叉树比较

红黑规则(重点)

添加节点默认是红色则效率高

添加节点规则(难点)

为什么红黑树的增删改查效率都高?

哈希表(难点)

Java中哈希表的组成

哈希值(重点)

对象的哈希值特点

哈希碰撞

代码测试

泛型

泛型的好处

泛型的擦除(重点)

泛型的细节

泛型类

TEKV

泛型方法

泛型接口

泛型通配符?

可变参数

只能存在一个可变参数


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的方法学会,下面实现类都不需要再学新的方法了,够用

遍历方式(五种)
  1. 键找值
    1. 增强for
    2. 迭代器
    3. Collection.forEach(匿名内部类/Lambda表达式)
  2. 键值对
  3. 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个数的和

以前的时候是这样子写比较麻烦

现在是这样子写

只能存在一个可变参数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值