disgare 的博客
首页
博客
分类
标签
首页
博客
分类
标签
  • 网络

    • 计算机网络学习笔记
    • 网络安全相关
    • 域名和子网掩码
    • CORS 跨域资源共享
    • DNS、HTTP 与 HTTPS
    • Server-Sent Events (SSE)
    • WebSocket 长连接
  • 计算机基础

    • 操作系统 IO 相关知识
    • 操作系统学习笔记
    • 程序的机器级表示
    • 音频文件基础
    • 正则表达式相关概念
    • ffmpeg 的安装以及实现音频切分功能
    • Hex 和 Base64 编码
    • XML 的使用
  • 数据结构与算法

    • 动态规划算法学习笔记
    • 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
    • 集合与数据结构学习笔记
      • List 接口
        • ArrayList
        • LinkedList
        • Collection 集合遍历
        • RandomAccess 接口
      • Map 接口
        • LinkedHashMap
        • HashMap
        • HashMap 多线程操作导致死循环问题
        • 基本属性
        • 散列函数
        • 哈希表储存什么
        • 哈希冲突如何解决
        • 源码分析
        • 为什么要从头插法改成尾插法?
        • 为什么 HashMap 的默认长度是16
        • 常用方法
        • TreeMap
        • 源码分析
        • Set接口
        • HashSet
        • TreeSet
        • Map 的集合遍历
      • 树
        • 二叉搜索树
        • 遍历
        • 删除
        • 红黑树
      • 迭代器
      • Collections
    • 面试常见算法总结
    • 算法导论第二部分排序学习笔记
    • 算法导论第一部分学习笔记
  • Java

    • 对象之间的映射与转换
    • 反射学习笔记
    • 泛型相关概念
    • 关于 boolean 类型的坑
    • 如何使用 lambda 表达式实现排序
    • CompletableFuture 相关用法
    • CompletableFuture 源码浅要阅读
    • FutureTask 源码阅读
    • Guava 常用 API
    • Guava 源码阅读:Multimap 相关
    • Jackson 的各种使用
    • Java 的 Excel 相关操作
    • java 的常见性能问题分析以及出现场景
    • java 基础知识
    • JAVA 枚举的基础和原理
    • Java 图片文件上传下载处理
    • Java 序列化
    • Java 异常
    • Java 语法糖
    • Java 中关于字符串处理的常用方法
    • Java 中强、软、弱、虚引用
    • JAVA 注解小结
    • Java Http 访问框架
    • Java Stream 的使用
    • Java8 新特性
    • netty 学习笔记
    • Scanner 的各种用法
    • Servlet 学习笔记
    • String、StringBuffer、StringBuilder 学习笔记
  • JVM

    • 虚拟机执行子系统
    • JVM 自动内存管理
    • Linux 中 JVM 常用工具以及常见问题解决思路
  • Linux

    • crontab 表达式
    • Linux 常见命令
    • Linux 文件系统
  • 中间件

    • 关于定时任务原理
    • 详解 kafka
    • ES 搜索引擎
    • flink 提交流程
    • Grape-RAG
    • Hadoop 基础原理
  • 多线程

    • 多线程基础学习笔记
    • 简单了解并发集合
    • 如何手写单例
    • 深入理解 java 多线程安全
    • 生产者消费者问题
    • 线程池作用、用法以及原理
    • AQS 组件
    • ThreadLocal 原理以及使用
  • 非关系型数据库

    • Redis 集群
    • Redis 数据结构、对象与数据库
    • Redis 学习笔记
  • 关系型数据库

    • B+ 树的插入、删除和数据页分裂机制
    • MySQL 的 binglog、redolog、undolog
    • MySQL 的记录存储结构、存储引擎与 Buffer Pool
    • MySQL 基本的特性
    • MySQL 开发规范
    • MySQL 事务与锁与 MVCC
    • MySQL 数据类型、字符集相关内容
    • MySQL 索引与索引优化
    • PostgreSQL 更新数据时 HOT优化
    • PostgreSQL 相关用法
  • Python

    • Python 基础语法
    • Python 学习
  • Spring 项目

    • Lombok 的常用注解
    • maven 小结
    • MyBatis 框架的使用
    • MyBatis 重要知识点总结
    • MybatisPlus 的使用
    • Spring 框架基础使用
    • Spring 事务相关
    • Spring IOC 的原理及源码
    • Spring AOP 的使用和原理
    • SpringBoot 的原理
    • SpringBoot 基础使用
    • SpringWeb 重要知识点
  • 分布式

    • 初步了解 docker
    • 从 ACID 到 BASE 事务处理的实现
    • 访问远程服务
    • 分布式 id
    • 分布式缓存相关问题
    • 分布式集群理论和分布式事务协议
    • 分布式架构的观测
    • 分布式一致性算法
    • 负载均衡 Load Balancing
    • 关于分布式系统 RPC 中高可用功能的实现
    • 集群间数据同步的目的
    • 三高问题下的系统优化
    • 数据库分库分表
    • 详解 Spring Cloud
    • Dubbo 基础概念
    • Gossip 协议
    • nginx 学习笔记
    • Protobuf 通信协议
    • Zookeeper 基础学习
  • 架构设计

    • 参数校验与异常处理
    • 抽象方法与设计模式
    • 代码整洁之道
    • 权限系统设计
    • 用低内存处理大量数据
    • 设计模式——策略模式
    • 设计模式——过滤器模式在 Spring 中的实践
    • 状态模式
    • 统一结果返回
    • 为什么要打日志?怎么打日志?打什么日志?
    • 运维监控常见指标含义
    • 资深研发进阶
    • DDD 架构学习笔记
    • Java 常用的规则引擎
    • MVC 架构学习笔记
  • AI

    • 如何编写 Prompt
    • Agent 工程架构
    • LLM 相关内容
    • NLP 相关知识
    • vibe coding 最佳实践
    • windows 下 ollama 迁移到 D 盘
  • 开发工具

    • 如何画时序图、流程图、状态流转图
    • excel 关于 =vlookup 的用法
    • git 的学习以及使用
    • IDEA 插件推荐
    • IDEA 常用快捷键以及调试
    • Shell 脚本
    • swagger 的使用
  • 前端

    • 简单了解前端页面开发
    • 伪静态是什么
    • GitHub Pages 部署教程
    • Vercel 部署教程
    • vue-admin-template 简单使用
    • VuePress 博客搭建指南
  • 项目

    • 面试刷题网——技术方案
    • 影视资源聚合站——技术方案
  • 问题记录

    • 定时任务单线程消费 redis 中数据导致消费能力不足
    • 提供可传递的易受攻击的依赖项
    • Liteflow 在 SpringBoot 启动时无法注入组件问题 couldn‘t find chain with the id[THEN(NodeComponent)]
  • 金融

    • 股票分析——关于电力
    • 股票技术面——量价关系
    • 股票技术面——盘口
    • 股票技术面——基础
    • 基础的金融知识
    • 基金与股票
    • 韭菜的自我总结
    • 聊聊价值投资
  • 其他

    • 程序员职场工作需要注意什么
    • 创业全链路SOP:从灵光一现到系统化增长的实战指南
    • 观罗翔讲刑法随笔
    • 价格和价值
    • 立直麻将牌效益理论
    • 梅花易数学习笔记
    • 压力管理
2021-03-05
数据结构与算法
目录

集合与数据结构学习笔记

# List 接口

储存的对象有序,可重复

# ArrayList

像数组一样存储对象,每次增删都需要构建新数组,适合多次查询的情况

构造方法:构造空数组,传入参数后构造一个默认为10大小的对象数组

add:如果没有超过数组大小,加入

超过,进入 grow 扩容方法

    public boolean add(E e) {
        ensureCapacityInternal(size + 1); 
        elementData[size++] = e;
        return true;
    }
1
2
3
4
5

先将传入大小与原来数组大小的1.5倍(位运算,oldCapacity >> 1)进行比较,获取两者较大值。再将较大值与设定的最大数组长度(INTEGER.MAX_VALUE - 8)进行比较。如果没超过,以较大值构造数组,并复制原数组。Arrays.copyOf 是一种深拷贝,导致每次扩容会生成新数组

    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    private void grow(int minCapacity) {
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

1
2
3
4
5
6
7
8
9
10
11

如果超过,进行巨型容量处理 比较传入大小与最大数组长度,如果大于,构造 INTEGER.MAX_VALUE 长度数组,如果小于,构造最大数组长度数组

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) 
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
1
2
3
4
5
6
7

remove:删除数据,将后面的数前移,代码异常简单,将指定索引后面的元素拷贝到前一格

    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# LinkedList

像链表一样存储对象,适合多次增删的情况

内置内部类 node,是双向节点

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
1
2
3
4
5
6
7
8
9
10
11

# Collection 集合遍历

1,使用 Iterator 迭代器 2,使用 foreach 遍历集合,不能修改数据,只能查询 3,使用 forEach 方法

  • 使用迭代器的 forEachRemaining 方法
  • 使用集合的 forEach 方法
  • 两者都有函数式接口,配合 Lambda 表达式使用

# RandomAccess 接口

接口里没有任何属性和方法,只是一个标识

如果接入,表示此类支持随机访问,可以像数组一样查找数据,此时随机访问数据比较快

如果不接入,表示此类不支持随机访问,只能用链表方式查找数据,此时使用迭代器查找比较快

 if (list instanceof RandomAccess) {
      // 使用随机取值,即根据下标取值方式
} else {
     // Iterator遍历器取值
}
1
2
3
4
5

在实际应用中,当我们不明确获取到的是 Arraylist,还是 LinkedList 的时候,我们可以通过 RandomAccess 来判断其是否支持快速随机访问,若支持则采用 for 循环遍历,否则采用迭代器遍历

        if(list instanceof RandomAccess) {
            // for循环遍历
            for (int i = 0;i< list.size();i++) {
                System.out.println(list.get(i));
            }
        } else {
            // 迭代器遍历
            Iterator it = list.iterator();
            while(it.hasNext()){
                System.out.println(it.next());
            }
        }
1
2
3
4
5
6
7
8
9
10
11
12

# Map 接口

map 集合储存键值对

# LinkedHashMap

没有对 HashMap 的实现做任何修改,可以保持键值对的插入顺序。继承自 HashMap,将每个键值对当成一个节点,像链表一样储存

    static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
1
2
3
4
5
6

# HashMap

使用 hash 算法进行存储,输入的对象算出地址后,如果为空,直接存储,不为空,则视为哈希冲突

# HashMap 多线程操作导致死循环问题

hashmap 本来就不能进行多线程的操作,多线程下不能使用 hashmap 的主要原因如下:

1,在添加数据时容易出现数据丢失的错误 2,当 hashmap 扩容时由于多个线程同时进行操作,底层代码的实现会让 hashmap 出现链表循环的问题,在执行扩容函数的时候没有上锁,多个线程同时无序的访问内存时就会出现这种经典的多线程问题

//不断的把老map中的元素取出放入新map中
void transfer(Entry[] newTable)
{
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

而具体表现就是在进行数据查找的时候会引发 CPU 被占满

# 基本属性

阈值:8,链表长度超过8进行扩容,如果数组长度不小于64(这是最小树化容量)此链表转化为红黑树。如果对红黑树中节点进行删除,节点减少至6会转会链表,这是为了避免边界抖动造成性能问题,并且列表在节点小于等于6时综合性能优于红黑树,因为避免了数的左旋右旋和变色操作 最大容纳:2的30次方 填充因子:0.75,储存结点超过数组大小乘填充因子时开始扩容 默认大小:16,初次构造数组大小

这种设计在空间、时间、实现复杂度之间取得最佳平衡。不为罕见场景过度优化,但确保最坏情况有保障

# 散列函数

《算法导论》中推荐的散列函数有三种:

1,除法散列法:通过取k除以m的余数,来将关键字k映射到m个槽的某一个中去 2,乘法散列法:首先,用关键字k乘上常数A(0<A<1),并抽取kA的小数部分;然后,用m乘以这个值,再取结果的底(即整数部分) 3,全域散列:在执行开始时,从一族仔细设计的函数中,随机地选择一个作为散列函数

在 java 中的散列函数是这个对象的 hashcode 异或 hashcode 二进制无符号右移16位

    static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }
1
2
3
4

# 哈希表储存什么

储存 Node 节点,节点实现了 Entry 接口,因此哈希表可以返回键值对

哈希表开辟一块储存 Node 结点的连续内存空间,即 Node[]

可以储存 Node 与 TreeNode,分别对应链表与红黑树,TreeNode 是 Node 的子类

# 哈希冲突如何解决

1,链表法:把散列到同一槽中的所有元素都存放在一个链表中。每个槽中有一个指针,指向由所有散列到该槽的元素构成的链表的头,java 中就使用这种方法

2,开发寻址法:所有的元素都存放在散列表中,当要插入一个元素时,使用某种方法探查散列表的各项,直到找到一个空槽来放置待插入的关键字。而能使用的方法有很多。比如:

  • 线性寻址(h(k,i) = (h’(k) + i) mod m, i=0, 1, …, m-1,找散列表的下一个位置)
  • 二次寻址(h(k,i) = (h’(k) +c1i + c2i^2) mod m, i=0, 1, …, m-1,根据某个函数计算下一个位置)
  • 双重散列(h(k,i) = (h1(k) + i*h2(k)) mod m, i=0, 1, …, m-1,用第一个散列函数找到初始位置后,用第二个函数找其他位置)
  • 再哈希(h(k,i) = (h1(k)) ^ i mod m, i=0, 1, …, m-1,多次对结果通过散列函数进行计算)

那有没有完全避免 hash 冲突的方法呢,我们规定,如果某一种散列技术在进行查找时,其最坏情况内存访问次数为 O(1) 的话,则称其为完全散列。通常利用一种两级的散列方案,每一级上都采用全域散列。为了确保在第二级上不出现碰撞,需要让第二级散列表 Sj 的大小 mj 为散列到槽j中的关键字数 nj 的平方

# 源码分析

hash 函数:这个对象的 hashcode 异或 hashcode 二进制右移16位

    static final int hash(Object key) {
      int h;
      return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
  }

1
2
3
4
5

put():调用 putVal 方法

putVal():

如果大小为空或0,调用 resize 方法扩容

调用hash计算哈希值,地址为空则插入

key值相同则替换

通过链表或红黑树查找,找到 key 值相同则替换

插入到末尾,此时链表长度大于8则尝试转为红黑树

通过数组长度与填充因子判断是否需要 resize 扩容

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
        else if (p instanceof TreeNode)
            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);
                    if (binCount >= TREEIFY_THRESHOLD - 1) 
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

上面的代码是1.8之后的代码,它使用的是尾插法,在JDK8之前java使用的是头插法

resize:扩容机制

treeifyBin:链表转红黑树

# 为什么要从头插法改成尾插法?

头插法用不了红黑树了

# 为什么 HashMap 的默认长度是16

其实个人认为只要是2的幂就可以了,原因跟 hash 函数有关

因为经过 hash 函数计算之后,得到的结果会相当大,我们只取结果的后几位作为此对象应该存放的位置,比如如果 hashmap 的长度是16则是取 hash 函数计算结果的后4位,长度是32取后5位

如果是其他的数字作为 hashmap 的总大小,会导致存放数据时操作麻烦。而如果换一个合适的 hash 函数来计算数据应该存放的位置,就不需要满足是2的幂了

# 常用方法

put 方法会返回该 key 原来对应的值,如果原来没有该值则返回 null

compute:compute() 方法对 hashMap 中指定 key 的值进行重新计算。如果 key 对应的 value 不存在,则返回该 null,如果存在,则返回通过 remappingFunction 重新计算后的值

        HashMap<String, Integer> prices = new HashMap<>();
        // 无值返回 null,有值计算鞋子打了10%折扣后的值
        int newPrice = prices.compute("Shoes", (key, value) -> value - value * 10/100);
1
2
3

merge:merge() 方法会先判断指定的 key 是否存在,如果不存在,则添加键值对到 hashMap 中。如果 key 对应的 value 不存在,则返回该 value 值,如果存在,则返回通过 remappingFunction 重新计算后的值

	// 无值时返回100,并且对 Shirt 这个 key 塞入100
	// 有值时返回 oldValue(原来在 map 里的值) 加 newValue(100),并且塞入两值的和
	int returnedValue = prices.merge("Shirt", 100, (oldValue, newValue) -> oldValue + newValue);
1
2
3

computeIfAbsent:computeIfAbsent() 方法对 hashMap 中指定 key 的值进行重新计算,如果不存在这个 key,则添加到 hashMap 中。如果 key 对应的 value 不存在,则使用获取 remappingFunction 重新计算后的值,并保存为该 key 的 value,否则返回 value

        // 计算 Shirt 的值。如果没值返回 280,并且塞入 280,有值直接返回
        int shirtPrice = prices.computeIfAbsent("Shirt", key -> 280);
1
2

putIfAbsent:putIfAbsent() 方法会先判断指定的键(key)是否存在,不存在则将键/值对插入到 HashMap 中。如果所指定的 key 已经在 HashMap 中存在,则它不会修改 Map 中的值,返回和这个 key 值对应的 value, 如果所指定的 key 不在 HashMap 中存在,则返回 null

        sites.putIfAbsent(4, "Weibo");
1

retainAll:这个方法是 Entry 类提供的。你可以使用该方法来获取两个 Map 的交集

import java.util.HashMap;
import java.util.Map;

public class MapIntersectionExample {
    public static void main(String[] args) {
        // 创建第一个Map
        Map<String, Integer> map1 = new HashMap<>();
        map1.put("a", 1);
        map1.put("b", 2);
        map1.put("c", 3);

        // 创建第二个Map
        Map<String, Integer> map2 = new HashMap<>();
        map2.put("b", 2);
        map2.put("c", 3);
        map2.put("d", 4);

        // 获取两个Map的交集
        Map<String, Integer> intersection = new HashMap<>(map1);
        intersection.keySet().retainAll(map2.keySet());

        // 输出结果
        System.out.println("交集: " + intersection);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# TreeMap

使用红黑树存储键值对 自平衡排序二叉树

# 源码分析

在调用put方法时,首先执行查找算法,需要判断有没有比较器

            do {
                parent = t;
                //如果没有比较器,执行内嵌的比较方法
                //cmp = k.compareTo(t.key);
                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);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

找到要插入的位置后,执行addEntry方法,这个方法先将节点插入,然后执行红黑树的自平衡旋转

    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++;
    }
1
2
3
4
5
6
7
8
9
10

# Set接口

储存的对象无序且不能重复

# HashSet

使用 hash 算法进行存储,输入的对象算出地址后,如果为空,直接存储,不为空则不能存储

内部直接调用 HashMap,键就是 set 中的值,值是一个 Object 对象

	private static final Object PRESENT = new Object();

    private transient HashMap<E,Object> map;
    
    public HashSet() {	 map = new HashMap<>();}

	public boolean add(E e) {	return map.put(e, PRESENT)==null;}
1
2
3
4
5
6
7

# TreeSet

使用红黑树来进行存储,因此可以排序,排序分为:自然排序,定制排序

实现是调用NavigableMap(继承sortedMap的一个接口),真正的实现是TreeMap

# Map 的集合遍历

1,集合自身 forEach 方法 2,Map 有自己的返回值为 set 集合的方法,可以对其用 Collection 集合遍历的方法进行遍历:keySet、valueSet、entrySet 3,for循环遍历 4,迭代器

# 树

在上面的说明中,树这个概念出现了多次,它是一个很有用的数据结构,可以衍生出优先队列、二叉搜索树等可以方便查找特定元素的数据结构

# 二叉搜索树

它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  • 它的左、右子树也分别为二叉排序树

# 遍历

遍历二叉树,就是以某种方式逐个访问二叉树的每一个节点。根据访问树根的顺序,我们有三种方式遍历二叉树,无论哪种遍历方式,左节点一定在右节点之前:

1,前序遍历:其中间节点在左节点与右节点之前 2,中序遍历:其中间节点在左节点与右节点之中 3,后序遍历:其中间节点在左节点与右节点之后

所有的遍历都可以使用递归简单的实现

# 删除

由于二叉搜索树的插入与查询算法非常简单,这里只说明删除算法,删除分为以下三种情况,只有其中一种较为棘手:

1,当被删除节点z没有任何孩子时,直接删除即可 2,当被删除节点z只有一个孩子时,用这个唯一的孩子代替z 3,当被删除节点z有两个孩子时,寻找z的后继节点y,后继节点y是z的右子树中的最左边的节点,该节点没有左孩子。用该节点的右孩子代替该节点(如果没有右节点也没关系,使用NIL代替),然后将z中的数字直接改成y中的数字即可

就算不进行算法分析,也可以轻松的得出,二叉搜索树的插入查询与删除算法都是O(h),h为树高

# 红黑树

红黑树是一颗高度平衡的二叉搜索树,满足以下性质:

1,根节点与叶节点都是黑色的 2,红色节点的子节点都是黑色的 3,每个节点到叶节点的所以路径都包含相同数目的黑色节点

这3条规则保证了最长的路径不会超过最短路径的2倍。红黑树先用二叉树的方式插入数据,默认插入的都是红色节点,然后依靠左旋、右旋和变色在添加和删除时维持红黑树的性质

# 迭代器

说了这么多集合,来说说迭代器相关的知识吧

迭代器一般都有这三个方法,创建时获取该集合的迭代器进行循环即可,非常简单

E next()     会返回迭代器的下一个元素,并且更新迭代器的状态。在使用 next() 方法前必须使用 hasNext() 方法判断集合中是否还有下一个元素,否则可能会出现异常。
boolean hasNext()  用于检测集合中是否还有元素。存在则返回true。
default void remove()     删除上次调用next方法时返回的元素。注意,使用remove时,不能将迭代器置于第一位,否则会报错。
1
2
3

# Collections

Collections中包含了一些对于集合的基本操作

static final <T> List<T> emptyList() || emptySet() || emptyMap():返回一个新的空的list、set、map集合,注意,这些集合都是不可以写入的

static <T> Set<T> singleton(T o) || singletonList(...) || singletonMap(...):返回一个新的只有一个元素的list、set、map集合,注意,这些集合都是不可以写入的

static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key):二分查找

static void shuffle(List<?> list):打乱一个集合的元素顺序

static <T> void sort(List<T> list, Comparator<? super T> c):选择一个集合按规定排序
1
2
3
4
5
6
7
8
9
#集合#数据结构
最后更新: 1/17/2026, 2:51:21 AM
基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn)
面试常见算法总结

← 基于比较的排序算法的最坏情况下的最优下界为什么是O(nlogn) 面试常见算法总结→

最近更新
01
vibe coding 最佳实践
02-24
02
立直麻将牌效益理论
02-23
03
伪静态是什么
02-08
更多文章>
Theme by Vdoing
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式