算法之旅(四)常用数据结构

常用数据结构介绍

常用的数据结构,包括顺序表、链表、队列、栈、哈希表和二叉树。

一、线性结构

首先我们要了解一个很重要的概念什么是线性结构。

线性结构

线性结构是指数据元素之间存在一对一的线性关系的数据结构。

  • 在这种结构中,数据元素按照线性顺序排列,每个元素都有唯一的前驱(除第一个元素)和唯一的后继(除最后一个元素)。
  • 线性结构的特点是数据元素之间的关系是线性的,呈现为一条直线。

2. 属于线性结构的数据结构

在我介绍的几个简单数据类型里

线性结构

  • 顺序表(数组)
  • 链表
  • 队列

非线性结构

  • 哈希表
  • 二叉树
    因为它们的数据元素之间的关系并非线性,而是呈现为多对多或层次关系。

数组(又称顺序表)

数组(Array)是一种线性数据结构,用于存储相同类型的数据元素。

数组的特点

1. 固定大小

这是他的限制之一,但也有解决方案。但他的限制也有可能成为他的优势。

  • 技术层面:数组的大小在创建后是固定的,不能动态改变。这意味着数组在内存中分配了一块连续的内存区域,大小等于元素类型大小乘以数组长度。
  • 日常生活:数组表示的集合通常具有固定数量的元素,例如一周7天的温度记录。

2. 相同类型的元素

  • 技术层面:数组中的所有元素必须是相同的数据类型
  • 业务/日常生活:公司部门,不同部门有相同职能的员工。

3. 连续内存分配

  • 技术层面:数组在内存中是连续存储的,这使得通过索引访问元素的时间复杂度为 O(1),非常高效。
  • 日常生活:在物理存储中,连续的数据更容易管理和访问,例如目录、书签

4. 支持随机访问

  • 技术层面:可以通过索引直接访问任意位置的元素,无需从头遍历。

链表的

链表(Linked List)是一种线性数据结构

  • 由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的引用(指针)。
  • 链表在计算机科学中广泛应用,具有灵活的内存分配和高效的插入、删除操作。

一、链表的特点

1. 动态大小

链表的长度不固定,可以根据需要动态地增加或减少节点。内存是在运行时动态分配的,不需要预先指定大小。

2. 非连续内存存储

链表的节点在内存中可以不连续存储,通过指针(引用)连接起来。这与数组的连续存储不同。

3. 高效的插入和删除操作

在链表中插入或删除节点,只需修改相关节点的指针,时间复杂度为 O(1),不需要移动其他元素。

4. 不支持随机访问

链表不支持通过索引直接访问任意节点,需要从头节点开始逐一遍历,查找特定元素的时间复杂度为 O(n)。

简答来说:增删效率极高,查询相对较慢

5. 多种类型

  • 单向链表:每个节点只指向下一个节点。
  • 双向链表:每个节点有两个指针,分别指向前一个和后一个节点,支持双向遍历。
  • 循环链表:尾节点的指针指向头节点,形成一个环状结构。

应用场景

  • 技术上:实现栈和队列、处理大量未知规模的数据、实现LRU缓存、符号表和邻接表、递归操作。
  • 业务、生活:音乐播放列表、火车车厢连接、任务调度链、导航路径、社交媒体的消息链、多人游戏的玩家列表。

链表的优势

  1. 动态大小:链表可以在运行时动态增长或收缩,不需要预先定义大小,适用于数据规模不确定的场景。

  2. 高效的插入和删除操作:在链表中插入或删除元素时,只需修改相关节点的指针,时间复杂度为 O(1),无需像数组那样移动大量元素。

  3. 内存利用率高:链表节点在内存中非连续存储,内存分配灵活,可以有效利用碎片内存。

链表的应用

  • 实现其他数据结构:如栈、队列、哈希表(冲突处理)等。
  • 操作系统中的内存管理:如空闲内存块的管理。
  • 图的邻接表表示:用于存储图中节点的邻接关系。
  • 用于需要频繁插入和删除操作的场景:如浏览器的前进后退功能、音乐播放列表等。

单向链表与双向链表的区别

一、结构区别

1. 单向链表

  • 节点结构:每个节点包含数据域和指向下一个节点的指针(next)。
  • 方向性:只能从头节点开始,单向遍历到尾节点。
  • 内存占用:每个节点只需存储一个指针,内存开销较小。

示意图

head --> [data | next] --> [data | next] --> [data | next] --> null

2. 双向链表

  • 节点结构:每个节点包含数据域、指向下一个节点的指针(next)和指向前一个节点的指针(prev)。(这就是区别)
  • 方向性:可以从头节点和尾节点开始,双向遍历。
  • 内存占用:每个节点需要存储两个指针,内存开销较大。

示意图

null <-- [prev | data | next] <--> [prev | data | next] <--> [prev | data | next] --> null

二、增删改查操作的区别

1. 插入操作

单向链表

  • 在头部插入:最快,直接将新节点的 next 指向原来的头节点,然后更新头节点为新节点。时间复杂度 O(1)。
  • 在尾部插入:需要遍历到尾节点(除非维护尾节点引用),然后在尾节点后插入新节点。时间复杂度 O(n)。
  • 在中间插入:需要找到插入位置的前一个节点,修改指针。时间复杂度 O(n)。

双向链表

  • 在头部插入:同单向链表,但需要更新新节点的 next 和原头节点的 prev。时间复杂度 O(1)。
  • 在尾部插入:如果维护了尾节点引用,可直接在尾节点后插入,更新相关指针。时间复杂度 O(1)。
  • 在中间插入:找到插入位置的前后节点,修改四个指针。时间复杂度 O(n)。

2. 删除操作

单向链表

  • 删除头节点:直接将头节点的 next 赋值为新的头节点。时间复杂度 O(1)。
  • 删除尾节点或中间节点:需要遍历到待删除节点的前一个节点,修改其 next 指针。时间复杂度 O(n)。

双向链表

  • 删除任意节点:由于有 prev 指针,可以直接访问待删除节点的前后节点,修改指针即可,无需遍历前一个节点。时间复杂度 O(1)(前提是已知待删除节点的引用)。

3. 查找操作

  • 单向链表:只能从头节点开始,单向遍历。时间复杂度 O(n)。
  • 双向链表:可以从头或尾节点开始,双向遍历,平均情况下速度更快。时间复杂度 O(n),但在某些情况下可能更高效。

4. 修改操作

  • 单向链表和双向链表:在找到目标节点后,直接修改数据域。查找时间复杂度 O(n)。

三、应用场景的区别

单向链表的应用场景

  • 简单的栈和队列实现:由于操作集中在链表的一端,单向链表即可满足需求。
  • 不需要反向遍历的场景:如简单的集合、列表。
  • 内存敏感的环境:节点占用内存较小,适用于内存受限的场景。

双向链表的应用场景

  • 需要频繁在两端或任意位置进行插入、删除的场景:如 LRU 缓存、操作系统中的任务调度。
  • 需要反向遍历的场景:如浏览器的前进后退功能、双向导航系统。
  • 更复杂的数据结构实现:如 Java 中的 LinkedList、一些高级集合类。

队列的数据结构特点、应用场景和 Java 示例代码


一、队列的定义

队列(Queue)是一种线性数据结构,遵循先进先出(FIFO, First In First Out)的原则。这意味着最早进入队列的元素将最先被移除。队列只允许在一端进行插入操作(称为队尾),在另一端进行删除操作(称为队头)。


二、队列的特点

  1. 线性结构:队列是一种有序的线性数据结构,元素按顺序排列。

  2. 先进先出(FIFO):第一个加入队列的元素最先被移除。

  3. 受限操作

    • 入队(Enqueue):在队尾添加元素。
    • 出队(Dequeue):从队头移除元素。
    • 查看队头元素(Peek):获取队头元素但不移除。
    • 判空(isEmpty):判断队列是否为空。
    • 获取队列长度(size):获取队列中元素的数量。
  4. 单向性:元素只能从队尾进入,从队头移出,不能直接访问队列中间的元素。

  5. 多种实现方式

    • 顺序队列:使用数组实现,容量固定。
    • 链式队列:使用链表实现,容量可动态变化。
    • 循环队列:为了解决顺序队列的假溢出问题,将队列逻辑上连接成一个环。

三、队列的应用场景

1. 任务调度和处理

  • 描述:在操作系统中,任务或进程按照一定的顺序等待处理。
  • 应用
    • 打印队列:打印任务按提交顺序打印。
    • 作业调度:CPU 按照任务到达的顺序调度执行。
    • 线程池:任务在队列中等待线程执行。

2. 广度优先搜索(BFS)

  • 描述:在图或树的遍历中,使用队列实现广度优先搜索。
  • 应用
    • 最短路径求解:如在无权图中寻找从起点到终点的最短路径。
    • 社交网络分析:查找用户之间的最短连接路径。

3. 异步数据传输

  • 描述:在生产者-消费者模型中,队列用于在不同的线程或进程之间传递数据。
  • 应用
    • 消息队列(MQ):如 RabbitMQ、Kafka,用于解耦系统,削峰填谷。
    • 事件驱动系统:异步处理事件,提升系统响应速度。

4. 缓存和数据缓冲

  • 描述:在数据传输过程中,队列用于暂存数据,平衡不同速率的生产和消费。
  • 应用
    • 网络数据包处理:路由器使用队列缓存数据包,避免丢包。
    • 音视频流处理:缓冲音视频数据,防止播放卡顿。

5. 订单和请求排队

  • 描述:在电子商务和服务系统中,用户的请求按照到达顺序排队处理。
  • 应用
    • 排队系统:如银行取号系统,客户按顺序办理业务。
    • 限流和降级:高并发情况下,队列用于控制请求处理速度,保护系统。

6. 模拟现实世界的排队场景

  • 描述:模拟现实中的排队问题,如超市收银、车辆排队等。
  • 应用
    • 离散事件模拟:用于研究和优化系统性能。
    • 游戏开发:模拟 NPC 的行动顺序或事件处理。

消息队列

消息队列是实现异步通信和系统解耦的重要技术。在消息中间件领域,RabbitMQ、RocketMQ 和 Kafka 是三大主流的消息系统。它们都涉及到了队列的概念,但在具体实现和使用上有所不同。

RabbitMQ、RocketMQ 和 Kafka 是否使用了队列结构?

1. RabbitMQ

RabbitMQ 使用了队列结构。

  • RabbitMQ 是一个基于 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息代理。消息在被消费者消费之前,会被存储在队列中。RabbitMQ 的核心概念包括:

    • Producer(生产者):发送消息的应用程序。
    • Exchange(交换器):根据路由规则接收和分发消息。
    • Queue(队列):存储消息,等待消费者取出并处理。
    • Consumer(消费者):从队列中取出并处理消息的应用程序。

2. RocketMQ

RocketMQ 使用了队列的概念。

  • RocketMQ 是阿里巴巴开源的分布式消息系统,基于发布/订阅模型。RocketMQ 的核心概念包括:

    • Producer(生产者):发送消息的应用程序。
    • Consumer(消费者):接收并处理消息的应用程序。
    • Topic(主题):消息的分类标识。
    • Message Queue(消息队列):Topic 下的逻辑队列,用于存储消息。

3. Kafka

部分是,Kafka 也涉及队列的概念,但其核心数据结构是日志(Log)

  • Apache Kafka 是一种分布式流处理平台,主要用于高吞吐量的实时数据传输。Kafka 的核心概念包括:

    • Producer(生产者):发送消息到 Kafka 主题的应用程序。
    • Consumer(消费者):从 Kafka 主题中读取消息的应用程序。
    • Topic(主题):消息类别的逻辑分组。
    • Partition(分区):Topic 的物理分片,每个分区是一个有序的、不可变的消息序列。
    • Offset(偏移量):消息在分区中的位置标识。

虽然 Kafka 不直接使用传统意义上的队列结构,但它实现了类似队列的消费模型,消费者按照顺序读取消息。


RabbitMQ、RocketMQ 和 Kafka 使用队列的原因

1. RabbitMQ

  • 基于 AMQP 协议:AMQP 协议本身就是为了解决异步通信和消息传递而设计的,队列是其核心组件。

  • 消息路由:RabbitMQ 使用交换器和绑定,将消息路由到不同的队列,实现灵活的消息分发策略。

  • 特性支持

    • 确认机制:确保消息被成功消费。
    • 持久化:消息和队列可以持久化到磁盘,防止数据丢失。
    • 优先级队列:支持基于队列的消息优先级控制。

2. RocketMQ

  • 高性能和高可靠性:RocketMQ 使用队列来实现高吞吐量的消息传递,同时保证消息的可靠性。

  • 消费模型

    • 推(Push)模式:消息主动推送给消费者。
    • 拉(Pull)模式:消费者主动从队列中拉取消息。
  • 特性支持

    • 顺序消息:支持严格的消息顺序性。
    • 事务消息:支持分布式事务,保证消息的一致性。

3. Kafka

  • 日志存储模型:Kafka 将消息存储在分区日志中,消费者按照偏移量顺序读取消息,类似于队列的顺序消费。

  • 高吞吐量和可扩展性:通过分区和副本机制,实现了高并发的消息处理和存储。

  • 特性支持

    • 发布/订阅模型:支持多个消费者订阅同一个 Topic。
    • 消费者组:实现消息的负载均衡和容错。
  • 为什么使用类似队列的消费模型

    • 顺序消费:对于需要顺序处理的场景,Kafka 的分区保证了分区内的消息顺序性。
    • 高效存储和读取:日志结构使得磁盘顺序读写效率极高。

不同消息中间件对队列的实现和应用:

  • RabbitMQ:严格基于队列的模型,队列是核心组件,支持灵活的路由和消息分发。

  • RocketMQ:使用消息队列来实现主题下的消息分发,支持高性能的消息传递和丰富的特性。

  • Kafka:虽然不直接使用传统的队列数据结构,但通过分区和日志的方式,实现了类似队列的顺序消费模型。


总结

队列的特点:

  • 先进先出(FIFO):最早进入队列的元素最先被移除。
  • 受限操作:只能在队尾插入元素,在队头移除元素。
  • 线性结构:元素按顺序排列,无法直接访问中间元素。
  • 多种实现方式:顺序队列、链式队列、循环队列等。

队列的优势:

  • 简洁性:实现简单,易于理解和使用。
  • 高效性:在特定的操作(如入队和出队)上有高效的性能。
  • 适用性广:适用于多种场景,如任务调度、异步处理、广度优先搜索等。

应用场景:

  • 任务调度:管理任务的执行顺序。
  • 广度优先搜索:在图或树中查找最短路径。
  • 消息队列:实现异步通信,解耦系统。
  • 缓冲和流处理:平衡生产和消费的速率。
  • 排队系统:模拟现实中的排队场景。

通过理解队列的特点和应用场景,可以在合适的场景中使用队列来简化问题的解决方案,提高系统的效率和可维护性。
如果是我业务mq会选择rabbitmq,虽然rocketmq性能更高,但是rabbitmq具有跟其他语言的交互能力,天然优势。
数据mq选择kakfa,也是结构优势。但是kafka本身可靠性投递并不难,一旦接上其他数据类框架,它的综合方案需要非常可靠。


Java 技术栈中链表的使用

1. LinkedList

  • 实现:Java 的 LinkedList 类是一个双向链表,实现了 ListDequeQueue 等接口。
  • 特点
    • 双向链表结构:支持在任意位置高效地插入和删除元素。
    • 遍历:支持从头到尾和从尾到头的遍历(通过 ListIterator)。
  • 应用
    • 队列和双端队列:实现 QueueDeque 接口,用于队列、双端队列的场景。
    • 需要频繁插入和删除的列表:当在列表中间频繁插入和删除元素时,LinkedListArrayList 更高效。

2. ConcurrentLinkedQueue

  • 实现ConcurrentLinkedQueue 是一个基于单向链表的无界非阻塞线程安全队列。
  • 特点
    • 单向链表结构:只需要前向指针,减少内存开销。
    • 高并发性能:采用无锁算法,适用于高并发环境。
  • 应用
    • 多线程环境下的任务队列:用于生产者-消费者模型,消息队列等。

3. LinkedBlockingQueue

  • 实现:基于链表的阻塞队列,通常用于线程池中任务队列的实现。
  • 特点
    • 双向链表结构:支持在两端进行插入和删除。
    • 线程安全:内部使用锁机制,保证并发安全。
  • 应用
    • 线程池任务队列:用于在多线程环境中存储和传递任务。

4. LRU 缓存机制

  • 实现:通常使用双向链表和哈希表的结合。
  • 特点
    • 双向链表:维护缓存的访问顺序,最近使用的元素放在前面。
    • 哈希表:提供快速的查找能力。
  • 应用
    • 缓存系统:如浏览器缓存、数据库缓存,移除最久未使用的数据。

一、什么是栈

栈(Stack)是一种线性数据结构,遵循后进先出(LIFO, Last In First Out)的原则。

  • 也就是说,最后插入栈的元素最先被删除或访问。
  • 栈只允许在一端进行插入和删除操作,这一端被称为栈顶(Top),另一端为栈底(Bottom)

二、栈的数据结构特点

  1. 线性结构:栈是一种特殊的线性表,只允许在一端进行操作。

  2. 后进先出(LIFO):新元素总是放在栈顶,取元素也是从栈顶开始,最后插入的元素最先被取出。

  3. 操作受限:主要支持以下两种基本操作:

    • 入栈(Push):将元素添加到栈顶。
    • 出栈(Pop):从栈顶移除元素。
  4. 访问受限:只能访问栈顶元素,无法直接访问栈中其他位置的元素。

  5. 动态或静态实现

    • 顺序栈:使用数组实现的栈,容量固定,需要管理栈的容量和栈顶指针。
    • 链式栈:使用链表实现的栈,容量不受限制,插入和删除操作更灵活。

栈的应用

Java 语言本身中的栈应用

1. 方法调用栈

  • 描述:Java 中的每个方法调用都会在 JVM 内存的调用栈(Call Stack)中生成一个栈帧(Stack Frame),用于存储方法的局部变量、参数和返回地址等信息。
  • 作用:调用栈按顺序记录每个方法调用,当方法执行结束时,栈顶的栈帧会被弹出,返回到调用它的方法。调用栈保证了方法调用的顺序性和执行流的管理。

2. 异常处理机制中的栈

  • 描述:Java 的异常处理通过栈来记录异常发生时的调用链,称为异常栈追踪(Exception Stack Trace)。
  • 作用:当异常发生时,JVM 会将异常抛出的路径按顺序压入栈中,从而生成完整的调用路径。开发者可以根据这个栈追踪信息来定位代码中异常的源头,帮助调试和分析问题。

开发常见场景中的栈应用

1. 递归调用

  • 描述:在递归算法中,每个递归调用会将当前状态压入栈中,以便返回时从栈顶取出,恢复递归状态。
  • 作用:递归调用中的栈应用让算法更简洁高效,例如常见的二叉树遍历、深度优先搜索等,通过调用栈自动记录并回溯执行路径。

2. 深度优先搜索(DFS)

  • 描述:在图或树的遍历中,深度优先搜索算法使用栈来实现路径的递归回溯。每次进入一个新的节点时会将当前节点压入栈,完成节点后出栈。
  • 作用:栈在 DFS 中的应用可以保证顺序遍历,同时通过弹栈回溯路径,便于实现节点的递归查找和路径回溯。

3. 表达式求值和括号匹配

  • 描述:栈可以用于解析和计算复杂表达式(如四则运算表达式的求值)以及验证表达式中括号的正确匹配。
  • 作用:在表达式求值中,操作数和运算符的处理顺序可以通过栈来管理。括号匹配中,每遇到一个左括号就压入栈中,遇到右括号则弹出匹配,确保左右括号的顺序正确。

4. 浏览器的前进和后退功能

  • 描述:浏览器中的前进和后退按钮使用了两个栈。一个栈记录用户的历史访问路径,另一个栈用于存储前进路径。
  • 作用:当用户点击后退按钮时,当前页面被压入前进栈,前一页面从历史栈弹出并显示;当用户点击前进按钮时,前进栈中的页面弹出到历史栈,重新显示页面。

5. 撤销(Undo)和重做(Redo)操作

  • 描述:文本编辑器、IDE 等软件的撤销和重做功能通常使用两个栈实现。
  • 作用:撤销操作将每个修改压入栈中,按顺序出栈即可恢复到上一个状态;重做则使用另一个栈来反向恢复撤销的操作,确保顺序正确。

6. 数制转换

  • 描述:在数制转换中(如十进制转二进制),栈用于逆序保存计算余数。
  • 作用:通过栈按逆序输出余数,直接获得从低位到高位的二进制数。

哈希表

哈希表的基本概念

1. 什么是哈希表

哈希表(Hash Table),也称为散列表

  • 是一种基于键值对(Key-Value)的数据结构。
  • 它通过哈希函数(Hash Function)将键映射到表中的位置,以实现高效的数据存储和查找。

2. 哈希函数

  • 定义:哈希函数是一个将任意长度的输入(键)映射为固定长度输出(哈希值)的函数。
  • 作用:将键映射为数组中的索引,以确定键值对的存储位置。

3. 哈希冲突

  • 定义:不同的键经过哈希函数计算后得到相同的哈希值,称为哈希冲突。
  • 解决方法
    • 链地址法(拉链法):将冲突的元素存储在同一个链表中。
    • 开放地址法:发生冲突时,寻找下一个空闲位置。
    • 再哈希法:使用第二个哈希函数重新计算位置。

哈希表的数据结构特点

1. 快速的查找和插入

  • 时间复杂度:在理想情况下,查找、插入和删除操作的平均时间复杂度为 O(1)
  • 原因:通过哈希函数直接定位到数据存储的位置,无需遍历数据结构。

2. 无序性

  • 特点:哈希表中的元素没有特定的顺序,数据的存储顺序取决于哈希函数的结果。
  • 影响:不适用于需要顺序访问或排序的场景。

3. 键的唯一性

  • 特点:哈希表中的键是唯一的,每个键对应一个值。
  • 作用:可以用于快速判重、索引数据等。

4. 动态扩容

  • 特点:当哈希表中的元素数量达到一定程度(负载因子)时,需要进行扩容,重新分配更大的数组,并重新计算所有键的哈希值。
  • 影响:扩容操作会影响性能,应尽量避免频繁扩容。

5. 内存占用

  • 特点:为了减少哈希冲突,哈希表通常需要保持一定的空余空间,可能会导致内存浪费。

三、哈希表在 Java 技术栈中的应用

1. Java 集合框架中的哈希表

1.1 HashMap

  • 概述HashMap 是 Java 中最常用的基于哈希表的 Map 实现,用于存储键值对。
  • 特点
    • 线程不安全:不适用于多线程环境。
    • 允许 null 键和值:可以接受一个 null 键和多个 null 值。
  • 应用场景
    • 缓存数据:存储频繁访问的数据,提高读取速度。
    • 实现快速查找:根据键快速获取对应的值。

1.2 Hashtable

  • 概述Hashtable 是早期的哈希表实现,位于 java.util 包中。
  • 特点
    • 线程安全:方法使用 synchronized 关键字进行同步。
    • 不允许 null 键和值:会抛出 NullPointerException
  • 应用场景
    • 简单的线程安全需求:在不使用 ConcurrentMap 的情况下。
      基本不用它而用ConcurrentHashMap,但是要知道为什么不用它,以及用了它在高并发架构里引起问题的修复方案

1.3 ConcurrentHashMap

  • 概述ConcurrentHashMap 是线程安全且高效的哈希表实现,适用于高并发环境。
  • 特点
    • 高并发性:采用分段锁或 CAS 操作,减少锁的粒度。
    • 不允许 null 键和值:与 Hashtable 类似。
  • 应用场景
    • 并发缓存:在多线程环境下共享和修改数据。
    • 统计计数器:高效地累加统计数据。

2. Java 中哈希表的实现细节

2.1 HashMap 的实现原理

  • 数据结构:数组 + 链表(JDK 1.7 及之前)或数组 + 链表/红黑树(JDK 1.8 及之后)。(这一点非常重要)
  • 哈希函数:对键的 hashCode() 进行扰动计算,以减少哈希冲突。
  • 哈希冲突解决
    • 链地址法:将冲突的元素存储在链表中。
    • 红黑树优化:当链表长度超过阈值(默认 8)时,链表转为红黑树,提高查找效率。

2.2 ConcurrentHashMap 的实现

  • 数据结构:JDK 1.7 使用分段锁(Segment) + 哈希表,JDK 1.8 改为 CAS 操作 + Synchronized 锁。
  • 高并发性:通过减少锁的粒度,提高并发性能。

四、哈希表在 Spring 框架中的应用

1. Spring 容器中的 BeanDefinition 存储

  • 描述:Spring 在启动时会将所有的 BeanDefinition 信息存储在一个哈希表中,方便后续快速查找和实例化。
  • 实现:使用 ConcurrentHashMap 存储 Bean 名称和对应的 BeanDefinition。
  • 作用:提高 Bean 的注册和获取效率,保证线程安全。

2. Spring MVC 中的 HandlerMapping

  • 描述:将 URL 请求路径映射到具体的处理器(Controller 方法)。
  • 实现:使用哈希表存储请求路径和处理器的对应关系。
  • 作用:快速根据请求路径找到对应的处理器,提升请求处理效率。

3. 缓存机制

  • 描述:Spring 提供了缓存抽象,支持将方法的返回值缓存起来,减少重复计算。
  • 实现:底层使用哈希表(如 ConcurrentHashMap)存储方法参数和结果的映射关系。
  • 作用:提高应用性能,降低资源消耗。

五、哈希表在架构层面的应用场景

1. 缓存系统

1.1 分布式缓存

  • 描述:如 Redis、Memcached 等缓存系统,底层使用哈希表存储数据。
  • 特点
    • 快速读写:基于内存的哈希表,读写性能高。
    • 数据持久化:支持将内存数据持久化到磁盘。
  • 应用场景
    • 提高系统性能:缓存热点数据,减轻数据库压力。
    • 会话管理:存储用户会话信息,实现分布式会话。

1.2 本地缓存

  • 描述:在应用内部使用哈希表作为本地缓存,存储频繁访问的数据。
  • 实现:使用 HashMapConcurrentHashMap
  • 应用场景
    • 配置数据缓存:如系统配置、字典数据。
    • 热点数据缓存:如热门商品信息、排行榜等。

2. 负载均衡与路由

  • 描述:在分布式系统中,通过一致性哈希算法将请求路由到特定的服务器节点。
  • 哈希表应用:使用哈希函数计算请求的路由目标,确保请求的均匀分布。
  • 应用场景
    • 分布式缓存:如一致性哈希在 Redis 集群中的应用。
    • 微服务路由:请求路由到特定的微服务实例。

3. 去重和统计

  • 描述:利用哈希表的键唯一性,快速判断元素是否存在,实现数据的去重和统计。
  • 实现:使用 HashSetHashMap 存储已处理的元素。
  • 应用场景
    • 日志处理:统计独立用户访问量(UV),过滤重复请求。
    • 消息系统:避免消息重复消费。

4. 数据索引

  • 描述:在数据库或搜索引擎中,使用哈希表构建索引,加速数据查询。
  • 应用场景
    • 数据库索引:如哈希索引,加速等值查询。
    • 全文检索:倒排索引中的词项与文档映射。

5. 配置和参数管理

  • 描述:在应用中,使用哈希表存储配置项和参数,以键值对形式管理系统配置。
  • 应用场景
    • 系统配置中心:如 Spring Cloud Config,使用哈希表存储配置项。
    • 环境变量管理:存储不同环境的参数。

六、哈希表的小总结

1. 哈希表的特点

  • 快速的查找和插入:平均时间复杂度为 O(1)。
  • 键值对存储:通过键唯一确定值。
  • 无序性:数据存储顺序不可预测。
  • 哈希冲突:需要合适的哈希函数和冲突解决策略。

2. 应用

  • Java 集合框架HashMapHashtableConcurrentHashMap 等广泛使用哈希表,实现高效的数据存储和访问。
  • Spring 框架:在 Bean 管理、请求映射、缓存等方面使用哈希表,提高系统性能和可维护性。
  • 架构层面:缓存系统、负载均衡、去重统计、数据索引等关键组件都依赖于哈希表的高效性能。

3. 注意事项

  • 选择合适的哈希函数:减少哈希冲突,提高性能。
  • 注意线程安全:在多线程环境中,使用线程安全的哈希表实现,如 ConcurrentHashMap
  • 监控和优化负载因子:避免频繁扩容或哈希冲突过多,影响性能。

二叉树

二叉树的定义

二叉树(Binary Tree)是一种树形数据结构。

  • 其中每个节点最多有两个子节点,分别称为左子节点(Left Child)右子节点(Right Child)
  • 二叉树是一种递归数据结构,广泛应用于计算机科学的各种算法和系统中。

二叉树的基本性质

  • 层次结构:节点按照层次排列,根节点在第一层,其子节点在下一层,以此类推。
  • 递归定义:二叉树要么是空的(null),要么由一个根节点和两个分别称为左子树和右子树的二叉树组成。
  • 度的限制:每个节点的度(子节点的数量)最多为2。

二叉树的类型

根据二叉树的形态和性质,可以将其划分为多种类型,每种类型都有特定的特点和应用场景。

1. 满二叉树(Full Binary Tree)

  • 定义:除了叶子节点外,每个节点都有两个子节点的二叉树。
  • 性质
    • 叶子节点只出现在最底层。
    • 节点总数为 ( 2^{k} - 1 ),其中 ( k ) 是树的高度。

2. 完全二叉树(Complete Binary Tree)

  • 定义:一个深度为 ( k ) 的二叉树,除了第 ( k ) 层外,其他各层的节点数都达到最大值,且第 ( k ) 层的所有节点都连续集中在最左边。
  • 性质
    • 便于使用数组进行存储,孩子和父节点的位置关系可以通过索引计算。

3. 平衡二叉树(AVL 树)

  • 定义:一种自平衡的二叉搜索树,任何节点的左右子树的高度差不超过1。
  • 性质
    • 查找、插入和删除的时间复杂度均为 ( O(\log n) )。
    • 通过旋转操作保持树的平衡。

4. 红黑树(Red-Black Tree)

  • 定义:一种自平衡的二叉搜索树,节点包含颜色属性(红色或黑色),通过颜色属性和规则保持树的近似平衡。
  • 性质
    • 查找、插入和删除的时间复杂度均为 ( O(\log n) )。
    • 应用广泛,Java 的 TreeMapTreeSet 底层就是红黑树。

5. 二叉搜索树(BST, Binary Search Tree)

  • 定义:一种特殊的二叉树,满足左子节点的值小于父节点,右子节点的值大于父节点。
  • 性质
    • 中序遍历结果为有序序列。
    • 查找、插入和删除的平均时间复杂度为 ( O(\log n) ),最坏情况下退化为链表 ( O(n) )。

6. 堆(Heap)

  • 定义:一种特殊的完全二叉树,分为最大堆和最小堆。
  • 性质
    • 最大堆:每个节点的值都大于或等于其子节点的值。
    • 最小堆:每个节点的值都小于或等于其子节点的值。
    • 常用于实现优先队列、堆排序。

7. 霍夫曼树(Huffman Tree)

  • 定义:一种带权路径长度最短的二叉树,又称最优二叉树。
  • 性质
    • 用于数据压缩、编码等领域。
    • 通过构建霍夫曼树,可以得到最优的无损编码方案。

三、二叉树的应用场景

1. 数据库

B树 和 B+树**

(很重要)

  • 定义:多路搜索树,节点可以有多个子节点,广泛用于数据库和文件系统的索引结构。
  • 应用
    • B-树:在数据库索引中,用于存储键值和数据指针,适合随机存取和顺序存取。
    • B+树:B树的变种,所有数据都存储在叶子节点,内部节点只存储键,适合范围查询。

2. Java 技术栈

2.1 Java 集合框架

  • TreeMapTreeSet

    • 底层实现:红黑树。
    • 特点:自动对元素进行排序,提供有序的键值映射和集合。
    • 应用场景:需要有序的键值对存储,如按字典序排序的字符串、按时间排序的事件等。
  • PriorityQueue:(重点)

    • 底层实现:最小堆(默认情况下)。
    • 特点:支持按照优先级进行元素的插入和提取。
    • 应用场景:任务调度、算法中的优先级队列(如 Dijkstra 算法)。

PriorityQueue--》任务调度和处理

在任务管理和调度系统中,经常需要根据任务的优先级来处理任务。
优先级可以基于任务的紧急程度、到期时间或其他业务规则。
应用场景:

操作系统的进程调度:根据进程的优先级,决定哪个进程先执行。
线程池中的任务队列:高优先级的任务可以先被线程池执行。

2.2 并发包(java.util.concurrent

  • ConcurrentSkipListMapConcurrentSkipListSet
    • 底层实现:跳表(Skip List),具有与平衡二叉搜索树类似的性能。
    • 特点:线程安全的、有序的 Map 和 Set。
    • 应用场景:在高并发环境下需要有序的集合或映射。

2.3 二叉树算法的应用

  • 树的遍历

    • 深度优先遍历(DFS):前序、中序、后序遍历。
    • 广度优先遍历(BFS):层次遍历。
  • 应用场景

    • 表达式解析:将算术表达式解析为二叉树(表达式树),用于计算结果或优化表达式。
    • 文件系统:目录和文件可以表示为树结构,便于管理和访问。

3. Spring系列的应用

3.1 Spring Core

  • Bean 的依赖关系管理
    • 描述:Bean 之间的依赖关系可以形成一个有向无环图(DAG),在某些情况下,可以用树结构表示依赖关系。
    • 应用:在加载 Bean 时,按照依赖关系正确地初始化各个组件。

3.2 Spring Security

  • 权限树
    • 描述:权限管理中,权限之间可以有继承或包含关系,形成树状结构。
    • 应用:判断用户的权限级别,进行权限控制。

3.3 Spring Data

  • 数据查询和映射
    • 描述:在查询嵌套数据结构(如 JSON、XML)时,可以使用树结构来表示数据层次。
    • 应用:将嵌套的数据映射为对象树,方便访问和操作。

4. 架构层面

4.1 缓存系统

  • LRU 缓存实现
    • 描述:使用平衡二叉搜索树(如红黑树)来实现缓存的快速查找和按访问时间排序。
    • 应用:在高性能缓存系统中,快速淘汰最久未使用的缓存项。

4.2 文件系统

  • 目录树结构
    • 描述:文件系统中的目录和文件组织成树形结构,便于管理和访问。
    • 应用:如 Linux 的目录结构,操作系统通过树结构管理文件。

4.3 前端路由

  • 路由树
    • 描述:Web 应用的路由管理,URL 路径可以组织成树结构,方便匹配和解析。
    • 应用:前端框架(如 React Router)的路由配置,后端框架的请求路径匹配。

4.4 编译器和解释器

  • 抽象语法树(AST)
    • 描述:编译器在解析源代码时,将代码转换为抽象语法树,以便进行语法分析和代码优化。
    • 应用:Java 编译器、解释器等。

4.5 人工智能

  • 决策树和随机森林
    • 描述:机器学习算法中,使用决策树对数据进行分类或回归分析。
    • 应用:数据挖掘、预测模型。

二叉树的小总结

1. 二叉树的定义和性质

  • 定义:每个节点最多有两个子节点的树形结构。
  • 性质:层次性、递归性、节点度限制。

2. 二叉树的类型

  • 满二叉树:所有非叶子节点都有两个子节点,叶子节点都在同一层。
  • 完全二叉树:除了最后一层外,其他层的节点数都达到最大值,最后一层的节点连续集中在左边。
  • 平衡二叉树(AVL 树):任何节点的左右子树高度差不超过1。
  • 红黑树:通过节点颜色和规则保持树的近似平衡。
  • 二叉搜索树:左子节点小于父节点,右子节点大于父节点。
  • :满足堆性质的完全二叉树,用于实现优先队列等。

3. 应用场景

  • 数据库:B-树、B+树用于索引结构,提高查询效率。
  • Java 技术栈TreeMapTreeSetPriorityQueue 等集合类,底层使用二叉树。
  • Spring 全家桶:Bean 管理、权限控制、数据绑定等可能涉及树结构。
  • 架构层面:缓存系统、文件系统、路由管理、配置管理、机器学习算法等。

4. 重要性

  • 高效性:二叉树结构使得查找、插入、删除操作更加高效。
  • 灵活性:二叉树可以演变出多种变种,满足不同的需求。
  • 广泛应用:从底层数据结构到高层架构设计,二叉树无处不在。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容