常用数据结构介绍
常用的数据结构,包括顺序表、链表、队列、栈、哈希表和二叉树。
一、线性结构
首先我们要了解一个很重要的概念什么是线性结构。
线性结构
线性结构是指数据元素之间存在一对一的线性关系的数据结构。
- 在这种结构中,数据元素按照线性顺序排列,每个元素都有唯一的前驱(除第一个元素)和唯一的后继(除最后一个元素)。
- 线性结构的特点是数据元素之间的关系是线性的,呈现为一条直线。
2. 属于线性结构的数据结构
在我介绍的几个简单数据类型里
线性结构
- 顺序表(数组)
- 链表
- 队列
- 栈
非线性结构
- 哈希表
-
二叉树
因为它们的数据元素之间的关系并非线性,而是呈现为多对多或层次关系。
数组(又称顺序表)
数组(Array)是一种线性数据结构,用于存储相同类型的数据元素。
数组的特点
1. 固定大小
这是他的限制之一,但也有解决方案。但他的限制也有可能成为他的优势。
- 技术层面:数组的大小在创建后是固定的,不能动态改变。这意味着数组在内存中分配了一块连续的内存区域,大小等于元素类型大小乘以数组长度。
- 日常生活:数组表示的集合通常具有固定数量的元素,例如一周7天的温度记录。
2. 相同类型的元素
- 技术层面:数组中的所有元素必须是相同的数据类型
- 业务/日常生活:公司部门,不同部门有相同职能的员工。
3. 连续内存分配
- 技术层面:数组在内存中是连续存储的,这使得通过索引访问元素的时间复杂度为 O(1),非常高效。
- 日常生活:在物理存储中,连续的数据更容易管理和访问,例如目录、书签
4. 支持随机访问
- 技术层面:可以通过索引直接访问任意位置的元素,无需从头遍历。
链表的
链表(Linked List)是一种线性数据结构。
- 由一系列节点(Node)组成,每个节点包含数据和指向下一个节点的引用(指针)。
- 链表在计算机科学中广泛应用,具有灵活的内存分配和高效的插入、删除操作。
一、链表的特点
1. 动态大小
链表的长度不固定,可以根据需要动态地增加或减少节点。内存是在运行时动态分配的,不需要预先指定大小。
2. 非连续内存存储
链表的节点在内存中可以不连续存储,通过指针(引用)连接起来。这与数组的连续存储不同。
3. 高效的插入和删除操作
在链表中插入或删除节点,只需修改相关节点的指针,时间复杂度为 O(1),不需要移动其他元素。
4. 不支持随机访问
链表不支持通过索引直接访问任意节点,需要从头节点开始逐一遍历,查找特定元素的时间复杂度为 O(n)。
简答来说:增删效率极高,查询相对较慢
5. 多种类型
- 单向链表:每个节点只指向下一个节点。
- 双向链表:每个节点有两个指针,分别指向前一个和后一个节点,支持双向遍历。
- 循环链表:尾节点的指针指向头节点,形成一个环状结构。
应用场景:
- 技术上:实现栈和队列、处理大量未知规模的数据、实现LRU缓存、符号表和邻接表、递归操作。
- 业务、生活:音乐播放列表、火车车厢连接、任务调度链、导航路径、社交媒体的消息链、多人游戏的玩家列表。
链表的优势
动态大小:链表可以在运行时动态增长或收缩,不需要预先定义大小,适用于数据规模不确定的场景。
高效的插入和删除操作:在链表中插入或删除元素时,只需修改相关节点的指针,时间复杂度为 O(1),无需像数组那样移动大量元素。
内存利用率高:链表节点在内存中非连续存储,内存分配灵活,可以有效利用碎片内存。
链表的应用
- 实现其他数据结构:如栈、队列、哈希表(冲突处理)等。
- 操作系统中的内存管理:如空闲内存块的管理。
- 图的邻接表表示:用于存储图中节点的邻接关系。
- 用于需要频繁插入和删除操作的场景:如浏览器的前进后退功能、音乐播放列表等。
单向链表与双向链表的区别
一、结构区别
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)的原则。这意味着最早进入队列的元素将最先被移除。队列只允许在一端进行插入操作(称为队尾),在另一端进行删除操作(称为队头)。
二、队列的特点
线性结构:队列是一种有序的线性数据结构,元素按顺序排列。
先进先出(FIFO):第一个加入队列的元素最先被移除。
-
受限操作:
- 入队(Enqueue):在队尾添加元素。
- 出队(Dequeue):从队头移除元素。
- 查看队头元素(Peek):获取队头元素但不移除。
- 判空(isEmpty):判断队列是否为空。
- 获取队列长度(size):获取队列中元素的数量。
单向性:元素只能从队尾进入,从队头移出,不能直接访问队列中间的元素。
-
多种实现方式:
- 顺序队列:使用数组实现,容量固定。
- 链式队列:使用链表实现,容量可动态变化。
- 循环队列:为了解决顺序队列的假溢出问题,将队列逻辑上连接成一个环。
三、队列的应用场景
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
类是一个双向链表,实现了List
、Deque
、Queue
等接口。 -
特点:
- 双向链表结构:支持在任意位置高效地插入和删除元素。
-
遍历:支持从头到尾和从尾到头的遍历(通过
ListIterator
)。
-
应用:
-
队列和双端队列:实现
Queue
和Deque
接口,用于队列、双端队列的场景。 -
需要频繁插入和删除的列表:当在列表中间频繁插入和删除元素时,
LinkedList
比ArrayList
更高效。
-
队列和双端队列:实现
2. ConcurrentLinkedQueue
类
-
实现:
ConcurrentLinkedQueue
是一个基于单向链表的无界非阻塞线程安全队列。 -
特点:
- 单向链表结构:只需要前向指针,减少内存开销。
- 高并发性能:采用无锁算法,适用于高并发环境。
-
应用:
- 多线程环境下的任务队列:用于生产者-消费者模型,消息队列等。
3. LinkedBlockingQueue
类
- 实现:基于链表的阻塞队列,通常用于线程池中任务队列的实现。
-
特点:
- 双向链表结构:支持在两端进行插入和删除。
- 线程安全:内部使用锁机制,保证并发安全。
-
应用:
- 线程池任务队列:用于在多线程环境中存储和传递任务。
4. LRU 缓存机制
- 实现:通常使用双向链表和哈希表的结合。
-
特点:
- 双向链表:维护缓存的访问顺序,最近使用的元素放在前面。
- 哈希表:提供快速的查找能力。
-
应用:
- 缓存系统:如浏览器缓存、数据库缓存,移除最久未使用的数据。
栈
一、什么是栈
栈(Stack)是一种线性数据结构,遵循后进先出(LIFO, Last In First Out)的原则。
- 也就是说,最后插入栈的元素最先被删除或访问。
- 栈只允许在一端进行插入和删除操作,这一端被称为栈顶(Top),另一端为栈底(Bottom)。
二、栈的数据结构特点
线性结构:栈是一种特殊的线性表,只允许在一端进行操作。
后进先出(LIFO):新元素总是放在栈顶,取元素也是从栈顶开始,最后插入的元素最先被取出。
-
操作受限:主要支持以下两种基本操作:
- 入栈(Push):将元素添加到栈顶。
- 出栈(Pop):从栈顶移除元素。
访问受限:只能访问栈顶元素,无法直接访问栈中其他位置的元素。
-
动态或静态实现:
- 顺序栈:使用数组实现的栈,容量固定,需要管理栈的容量和栈顶指针。
- 链式栈:使用链表实现的栈,容量不受限制,插入和删除操作更灵活。
栈的应用
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,但是要知道为什么不用它,以及用了它在高并发架构里引起问题的修复方案
-
简单的线程安全需求:在不使用 ConcurrentMap 的情况下。
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 本地缓存
- 描述:在应用内部使用哈希表作为本地缓存,存储频繁访问的数据。
-
实现:使用
HashMap
或ConcurrentHashMap
。 -
应用场景:
- 配置数据缓存:如系统配置、字典数据。
- 热点数据缓存:如热门商品信息、排行榜等。
2. 负载均衡与路由
- 描述:在分布式系统中,通过一致性哈希算法将请求路由到特定的服务器节点。
- 哈希表应用:使用哈希函数计算请求的路由目标,确保请求的均匀分布。
-
应用场景:
- 分布式缓存:如一致性哈希在 Redis 集群中的应用。
- 微服务路由:请求路由到特定的微服务实例。
3. 去重和统计
- 描述:利用哈希表的键唯一性,快速判断元素是否存在,实现数据的去重和统计。
-
实现:使用
HashSet
或HashMap
存储已处理的元素。 -
应用场景:
- 日志处理:统计独立用户访问量(UV),过滤重复请求。
- 消息系统:避免消息重复消费。
4. 数据索引
- 描述:在数据库或搜索引擎中,使用哈希表构建索引,加速数据查询。
-
应用场景:
- 数据库索引:如哈希索引,加速等值查询。
- 全文检索:倒排索引中的词项与文档映射。
5. 配置和参数管理
- 描述:在应用中,使用哈希表存储配置项和参数,以键值对形式管理系统配置。
-
应用场景:
- 系统配置中心:如 Spring Cloud Config,使用哈希表存储配置项。
- 环境变量管理:存储不同环境的参数。
六、哈希表的小总结
1. 哈希表的特点
- 快速的查找和插入:平均时间复杂度为 O(1)。
- 键值对存储:通过键唯一确定值。
- 无序性:数据存储顺序不可预测。
- 哈希冲突:需要合适的哈希函数和冲突解决策略。
2. 应用
-
Java 集合框架:
HashMap
、Hashtable
、ConcurrentHashMap
等广泛使用哈希表,实现高效的数据存储和访问。 - 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 的
TreeMap
、TreeSet
底层就是红黑树。
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 集合框架
-
TreeMap
和TreeSet
:- 底层实现:红黑树。
- 特点:自动对元素进行排序,提供有序的键值映射和集合。
- 应用场景:需要有序的键值对存储,如按字典序排序的字符串、按时间排序的事件等。
-
PriorityQueue
:(重点)- 底层实现:最小堆(默认情况下)。
- 特点:支持按照优先级进行元素的插入和提取。
- 应用场景:任务调度、算法中的优先级队列(如 Dijkstra 算法)。
PriorityQueue--》任务调度和处理
在任务管理和调度系统中,经常需要根据任务的优先级来处理任务。
优先级可以基于任务的紧急程度、到期时间或其他业务规则。
应用场景:
操作系统的进程调度:根据进程的优先级,决定哪个进程先执行。
线程池中的任务队列:高优先级的任务可以先被线程池执行。
2.2 并发包(java.util.concurrent
)
-
ConcurrentSkipListMap
和ConcurrentSkipListSet
:- 底层实现:跳表(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 技术栈:
TreeMap
、TreeSet
、PriorityQueue
等集合类,底层使用二叉树。 - Spring 全家桶:Bean 管理、权限控制、数据绑定等可能涉及树结构。
- 架构层面:缓存系统、文件系统、路由管理、配置管理、机器学习算法等。
4. 重要性
- 高效性:二叉树结构使得查找、插入、删除操作更加高效。
- 灵活性:二叉树可以演变出多种变种,满足不同的需求。
- 广泛应用:从底层数据结构到高层架构设计,二叉树无处不在。