题目描述

请设计一个文件缓存系统,该文件缓存系统可以指定缓存的最大值(单位为字节)。
文件缓存系统有两种操作:

  • 存储文件(put)
  • 读取文件(get)

操作命令为:

  • put fileName fileSize
  • get fileName

存储文件是把文件放入文件缓存系统中;
读取文件是从文件缓存系统中访问已存在,如果文件不存在,则不作任何操作。
当缓存空间不足以存放新的文件时,根据规则删除文件,直到剩余空间满足新的文件大小位置,再存放新文件。
具体的删除规则为:文件访问过后,会更新文件的最近访问时间和总的访问次数,当缓存不够时,按照第一优先顺序为访问次数从少到多,第二顺序为时间从老到新的方式来删除文件。

输入描述
第一行为缓存最大值 m(整数,取值范围为 0 < m ≤ 52428800)
第二行为文件操作序列个数 n(0 ≤ n ≤ 300000)
从第三行起为文件操作序列,每个序列单独一行,文件操作定义为:

op file_name file_size

file_name 是文件名,file_size 是文件大小

输出描述
输出当前文件缓存中的文件名列表,文件名用英文逗号分隔,按字典顺序排序,如:

a,c

如果文件缓存中没有文件,则输出NONE

备注

  1. 如果新文件的文件名和文件缓存中已有的文件名相同,则不会放在缓存中
  2. 新的文件第一次存入到文件缓存中时,文件的总访问次数不会变化,文件的最近访问时间会更新到最新时间
  3. 每次文件访问后,总访问次数加1,最近访问时间更新到最新时间
  4. 任何两个文件的最近访问时间不会重复
  5. 文件名不会为空,均为小写字母,最大长度为10
  6. 缓存空间不足时,不能存放新文件
  7. 每个文件大小都是大于 0 的整数

用例1
输入:

50
6
put a 10
put b 20
get a
get a
get b
put c 30

输出:

a,c

用例2
输入:

50
1
get file

输出:

NONE

思考

输入有读取和存储两个操作。定义集合 fileSet 存储文件名称,哈希表 fileSizeMap 存储映射每个文件的大小,key 是文件名,value 是文件大小,readFreqMap 存储映射每个文件的读取频次,priorityQueue 维护一个文件名队列,每次出队的都是最久未被读取的文件名。 读取操作:如果读取的文件不存在,就不执行任何操作;如果文件存在就给该文件的访问频次+1,同时调整优先队列,把读取的文件名移到队尾,每次这样操作,队首的文件一定的最久未被访问的文件。存储操作:每次存储文件判断文件名是否存在,存在就不执行操作;如果不存在,判断剩余存储空间 leftSize 是否大于等于当前文件大小,如果不满足就要删除读取频次最少且最久未被访问的文件,更新 leftSize += deleteFileSize,循环执行此操作直到满足存储需求,如果文件都删完了还是不满足就不执行任何操作。存储文件到 fileSet 中,更新 leftSize -= newFileSize,题目要求存储新文件也要更新最新访问时间,那么我们就把新文件名加到优先队列的队尾,优先队列队尾文件名始终表示最新访问的文件。删除操作:首先查找访问频次最少的文件名,如果只有一个最少的访问频次,直接删这个文件就对了。如果有多个相同的最少访问频次文件名,需要进一步在其中找到最久未被访问的文件,显然由于每次优先队列的队尾都是更新最新访问时间的文件名,队首的文件就是最久未被访问的,遍历优先队列找到最少访问频次文件集合中包含当前文件名的文件中止循环,删除该文件。为什么还遍历队列匹配最少访问频次集合中的文件?难道队列队首文件名可能不在最少访问频次文件集合中?有可能的,虽然队首文件名是最久未被访问的,但是未必就是访问频次最少的文件,假如有个文件 a 在历史访问记录中达到了100次,接着存储了一个新文件 b,这时候文件 b 是最新访问的文件,存在队尾,那么 显然 a 是存在队首的最久未被访问的文件,但它的访问频率达到100次,b 只有一次,显然不能直接删除队首元素 a 。由于 JavaScript 没有优先队列 API,如果不熟悉,做题的时候写个正确的优先队列数据结构也不容易,因此我先用 JS 数组模拟优先队列的特性实现这个算法,实际能在有效时间内通过所有测试用例才是最重要的。然后提供一个 JS 版二叉堆实现的优先队列版本代码。

算法过程(哈希表+数组模拟优先队列)

  1. 数据结构

    • fileSet记录缓存中的文件;fileSizesMap存储文件大小;readFreqMap记录访问次数;priorityQueue(数组)按访问时间排序(最新访问在末尾)。
    • leftSize追踪剩余缓存空间。
  2. 核心操作

    • put操作
      • 若文件已存在,直接跳过。
      • 若缓存空间不足,按规则淘汰文件:先筛选访问次数最少的文件列表,再从列表中删除priorityQueue中最早出现(最久未访问)的文件,直到空间足够。
      • 空间足够时,新增文件至缓存,初始化访问次数为0,加入priorityQueue末尾。
    • get操作
      • 若文件存在,访问次数+1,从priorityQueue中移除后重新加入末尾(更新为最新访问)。
  3. 结果输出
    缓存中的文件按字典排序,空则输出NONE

时间复杂度

  • 单次put操作
    • 淘汰文件时,遍历查找最少访问次数文件的时间为O(k)k为当前缓存文件数),最坏情况下需淘汰k次,总复杂度为O(k²)
    • 新增文件为O(1)
  • 单次get操作
    • 查找并移动文件在priorityQueue中的位置为O(k)
  • 整体复杂度
    设操作总数为n,单个操作涉及的最大文件数为k(≤缓存可容纳的最大文件数),总时间复杂度为O(n·k²)
    (注:因k通常远小于n,实际性能可接受,尤其适合中小规模操作场景。)

参考代码(Least Frequence Used + JS 数组模拟优先队列)

function solution(totalSize, operations) {const fileSet = new Set();const fileSizesMap = new Map();const readFreqMap = new Map();const priorityQueue = [];let leftSize = totalSize;for (const operation of operations) {const [op, fileName, fileSize] = operation;if (op === 'put') {if (fileSet.has(fileName)) {continue;}// 缓存不够,要删文件while (leftSize - fileSize < 0 && priorityQueue.length) {let count = Infinity;let toDeleteList = [];for (let [k, v] of readFreqMap) { // 查找访问频率最少的文件if (v < count) {toDeleteList = [k];count = v;} else if (v === count) {toDeleteList.push(k);}}if (toDeleteList.length === 1) { // 如果找到一个访问频率最少的文件直接删除fileSet.delete(toDeleteList[0]);const delSize =  fileSizesMap.get(toDeleteList[0]);leftSize += delSize;readFreqMap.delete(toDeleteList[0]);let index = priorityQueue.indexOf(toDeleteList[0]);priorityQueue.splice(index, 1);} else { // 多个访问频率最少的文件需要筛选最久未被访问的文件,然后删除for (let i = 0; i < priorityQueue.length; i++) {if (toDeleteList.includes(priorityQueue[i])) {let deleteFileName = priorityQueue[i];fileSet.delete(deleteFileName);const delSize =  fileSizesMap.get(deleteFileName);leftSize += delSize;priorityQueue.splice(i, 1);readFreqMap.delete(deleteFileName);break;}}}        }if (leftSize - fileSize >= 0) {fileSet.add(fileName);fileSizesMap.set(fileName, fileSize);readFreqMap.set(fileName, 0);priorityQueue.push(fileName);leftSize -= fileSize;console.log('leftSize: ', leftSize, ' fileSize: ', fileSize);}} else { // getif (fileSet.has(fileName)) {readFreqMap.set(fileName, readFreqMap.get(fileName) + 1);  // 更新访问频率let index = priorityQueue.indexOf(fileName);if (index !== -1) {priorityQueue.splice(index, 1);}priorityQueue.push(fileName); // 移到访问队列队首,表示最新访问的文件        }}}const fileList = Array.from(fileSet);fileList.sort();let result = "NONE";if (!fileList.length) {console.log("NONE");return "NONE";}result = fileList.join(',');console.log(result);return result;
}const cases = [`50
6
put a 10
put b 20
get a
get a
get b
put c 30`, `50
1
get file`
];let caseIndex = 0;
let lineIndex = 0;const readline = (function () {let lines = [];return function () {if (lineIndex === 0) {lines = cases[caseIndex].trim().split("\n").map((line) => line.trim());}return lines[lineIndex++];};
})();cases.forEach((_, i) => {caseIndex = i;lineIndex = 0;// console.time(`${i} cost time`);solution();// console.timeEnd(`${i} cost time`);  console.log('-------');
});

算法过程(优先队列)

  1. 数据结构

    • 核心使用MinPriorityQueue(最小优先队列,基于堆实现)存储文件信息,堆的比较规则为“访问次数少优先,次数相同则时间早优先”。
    • _cache(Map)快速映射文件名到文件详情(大小、访问次数、时间戳);_usedSize记录已用缓存空间;_timeCount生成唯一时间戳。
  2. 核心操作

    • put操作
      • 若文件已存在或大小超缓存上限,直接跳过。
      • 若缓存空间不足,循环删除堆顶元素(优先级最高的待淘汰文件),直到空间足够。
      • 新增文件至_cache和堆,初始化访问次数为0,更新时间戳和已用空间。
    • get操作
      • 若文件存在,先从堆中删除该文件(通过替换+堆调整实现),再更新其访问次数和时间戳,重新加入堆。
  3. 堆调整细节

    • 中间删除元素时,用堆尾元素替换目标位置,再通过swim(上浮)和sink(下沉)确保堆结构正确。
    • 堆顶始终是“访问次数最少且最久未访问”的文件,淘汰时直接删除堆顶。
  4. 结果输出
    缓存中的文件按字典排序,空则输出NONE

时间复杂度

  • 单次put操作
    • 入队(enqueue)和出队(dequeue)均为堆操作,时间复杂度O(log k)k为当前缓存文件数)。
    • 最坏情况下需淘汰k个文件,总复杂度O(k·log k)
  • 单次get操作
    • 查找文件在堆中的位置为O(k),删除并重新入队为O(log k),总复杂度O(k + log k)
  • 整体复杂度
    设操作总数为n,总时间复杂度为O(n·k·log k)k为缓存中最大文件数)。
    (注:堆优化了淘汰阶段的查找效率,相比数组模拟的O(k²),在大k场景下性能更优。)

参考代码(Least Frequence Used + 最小优先队列)

class MinPriorityQueue {constructor(compare) {this._data = [];this._compare = compare || ((a, b) => a - b);}enqueue(e) {this._data.push(e);this.swim(this._data.length-1);}dequeue() {if (this.isEmpty()) return null;const last = this._data.pop();if (this._data.length > 0) {this._data[0] = last;this.sink(0);}}swim(index) {while (index > 0) {let parentIndex = Math.floor((index - 1) / 2);if (this._compare(this._data[index], this._data[parentIndex]) < 0) {[this._data[parentIndex], this._data[index]] = [this._data[index],this._data[parentIndex],];index = parentIndex;continue;}break;}}// 从指定索引开始下沉sink(index) {const n = this._data.length;while (true) {let left = 2 * index + 1;let right = left + 1;let smallest = index;if (left < n && this._compare(this._data[left], this._data[index]) < 0) {smallest = left;}if (right < n && this._compare(this._data[right], this._data[smallest]) < 0) {smallest = right;}if (smallest !== index) {[this._data[smallest], this._data[index]] = [this._data[index],this._data[smallest],];index = smallest;continue;}break;}}front() {return this._data[0];}isEmpty() {return this._data.length === 0;}size() {return this._data.length;}
}class LFUCacheSystem {constructor(capibility) {this._capibility = capibility; // 最大缓存大小this._usedSize = 0; // 当前使用的缓存大小this._cache = new Map();this._minHeap = new MinPriorityQueue((a, b) => { // 传入自定义比较函数if (a.accessCount === b.accessCount) {return a.lastAccessTime - b.lastAccessTime; // 访问频次相同再比较访问时间}return a.accessCount - b.accessCount;});this._timeCount = 0;}put(fileName, fileSize) {if (fileSize > this._capibility) return;if (this._cache.has(fileName)) { // 如果文件已存在,更新文件的访问时间return;}// 缓存不够用了,从最小优先队列中删除优先级最高的文件(访问频次最少且最久未被访问的文件)while (fileSize + this._usedSize > this._capibility) {const toDelete = this._minHeap.front();if (toDelete) {this._minHeap.dequeue();  // 删除文件this._cache.delete(toDelete.fileName);this._usedSize -= toDelete.size; // 更新可用缓存大小}}const currentTime = this._timeCount++;const file = { fileName, size: fileSize, accessCount: 0, lastAccessTime: currentTime };this._minHeap.enqueue(file);this._cache.set(fileName, file);this._usedSize += file.size;}get(fileName) {if (!this._cache.has(fileName)) return;const file = this._cache.get(fileName);this._removeFileFromHeap(fileName);file.accessCount++;file.lastAccessTime = this._timeCount++;this._minHeap.enqueue(file);}_removeFileFromHeap(fileName) {const index = this._minHeap._data.findIndex(item => item.fileName === fileName);if (index === -1) return;// 用最后一个元素替换目标元素const last = this._minHeap._data[this._minHeap._data.length-1];this._minHeap._data[index] = last;this._minHeap._data.pop();if (index < this._minHeap._data.length) {this._minHeap.swim(index);this._minHeap.sink(index);}}getFileList() {const fileList = Array.from(this._cache.keys());fileList.sort();return fileList;}
};function solution2(totalSize, ops) {const cacheSys = new LFUCacheSystem(totalSize);for (const op of ops) {const [operation, fileName, fileSize] = op;if (operation === 'put') {cacheSys.put(fileName, parseInt(fileSize));} else { // getcacheSys.get(fileName);}}const fileList = cacheSys.getFileList();let result = "NONE";if (!fileList.length) {console.log("NONE");return "NONE";}result = fileList.join(',');console.log(result);return result;
}function entry() {const totalSize = parseInt(readline());const n = parseInt(readline());const ops = [];for (let i = 0; i < n; i++) {const [op, fileName, fileSize] = readline().split(' ');ops.push([op, fileName, parseInt(fileSize)]);}// console.log('case: totalSize  n  ops ', totalSize, n, ops);// solution1 // const result = solution(totalSize, ops);// solution2const result2 = solution2(totalSize, ops);// console.log(result === result2);}const cases = [`50
6
put a 10
put b 20
get a
get a
get b
put c 30`, `50
1
get file`
];// 生成随机 300000 个 put/get 操作用例,文件名最多100个不重复,超过26个字母范围用数字后缀
(function(){const ops = [];const fileNames = [];let totalFiles = 0;const maxFiles = 100;const maxFileSize = 100;const totalOps = 300000;const cacheSize = 1000;function getFileName(idx) {if (idx < 26) {return String.fromCharCode(97 + idx); // 'a' ~ 'z'} else {return String.fromCharCode(97 + (idx % 26)) + (Math.floor(idx / 26));}}for (let i = 0; i < totalOps; i++) {if (Math.random() < 0.5 || totalFiles === 0) {// put 操作let idx = Math.floor(Math.random() * maxFiles);let fileName = getFileName(idx);let fileSize = Math.floor(Math.random() * maxFileSize) + 1;ops.push(`put ${fileName} ${fileSize}`);if (!fileNames.includes(fileName)) {fileNames.push(fileName);totalFiles++;}} else {// get 操作let fileName = fileNames[Math.floor(Math.random() * fileNames.length)];ops.push(`get ${fileName}`);}}const input = `${cacheSize}\n${totalOps}\n${ops.join('\n')}`;cases.push(input);
})();let caseIndex = 0;
let lineIndex = 0;const readline = (function () {let lines = [];return function () {if (lineIndex === 0) {lines = cases[caseIndex].trim().split("\n").map((line) => line.trim());}return lines[lineIndex++];};
})();caseIndex = 0;
lineIndex = 0;
cases.forEach((_, i) => {caseIndex = i;lineIndex = 0;entry();console.log('-------');
});

验证

两种方法测试结果相同:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/91984.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/91984.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/91984.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python中的sys.path与PYTHONPATH全解析:模块导入路径的底层机制与最佳实践

在Python项目开发中&#xff0c;很多人遇到过类似“模块导入失败”、“路径找不到”、“相对导入与绝对导入混乱”等问题。而这些问题的根源&#xff0c;几乎都绕不开一个核心概念——Python模块搜索路径。 今天&#xff0c;我们围绕sys.path 和 PYTHONPATH环境变量&#xff0…

python:如何调节机器学习算法的鲁棒性,以支持向量机SVM为例,让伙伴们看的更明白

鲁棒性&#xff08;Robustness&#xff09;指模型在噪声数据或异常值干扰下保持性能稳定的能力。想详细了解的可参考本人之前的博文 python机器学习&#xff1a;评价智能学习算法性能与效果的常见术语&#xff1a;不收敛、过拟合、欠拟合、泛化能力、鲁棒性一句话、一张图给您…

号源加锁升级思路(解决高并发问题)

原先逻辑链接&#xff1a;号源预约加锁思路_java 预约 接口加锁-CSDN博客 一、进行治疗项目和号源数据缓存 1.新建一个定时任务&#xff0c;主要在凌晨时缓存治疗项目和号源数据 1.1.类中使用redission获取锁&#xff08;用于分布式系统获取数据&#xff0c;保证原子性&…

MCP革命:AI世界的“USB-C”接口如何重塑智能体与外部工具的连接

> 一条标准化的数据通道,让AI从“对话专家”蜕变为“行动专家”,背后是一场由协议驱动的工具连接革命。 2024年11月,Anthropic公司开源了**Model Context Protocol(MCP)**。在短短9个月内,这项技术彻底改变了AI与外部世界的交互方式。截至2025年8月,MCP服务数量**从…

启用“安全登录”组合键(Ctrl+Alt+Delete)解锁

文章目录背景目标功能操作步骤效果背景 在日常工作中&#xff0c;我们有时需要让电脑长期开机运行&#xff08;如处理长任务、作为服务器等&#xff09;。然而&#xff0c;这其中存在一个潜在风险&#xff1a;当电脑处于锁屏或登录界面时&#xff0c;如果有人无意中触碰键盘比…

【08】C++实战篇——C++ 生成动态库.dll 及 C++调用DLL,及实际项目中的使用技巧

文章目录一、创建动态库dll (方法一)1 生成C 动态库dll1.1 创建项目MyDLL1.2 编写.h 和 .cpp文件1.3 设置 及 生成 DLL2 调用 C 动态库dll2.1 创建C 空项目DLLtest2.2 动态库配置 及代码调用测试3 实际项目中的使用技巧3.1 设置dll输出路径3.2 设置头文件引入路径3.3 改进后 测…

kettle插件-kettle http client plus插件,轻松解决https接口无法调用文件流下载问题

场景&#xff1a;小伙伴在使用kettle调用https接口过程中无法正常调用&#xff0c;程序出错问题&#xff0c;今天演示下用自研插件轻松解决这个问题。1、使用openssl 生成自签名证书openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 3652、使用…

C#中的除法

在C#中&#xff0c;除法操作可以通过使用 / 运算符执行。这个运算符可以进行整数除法或浮点除法&#xff0c;这取决于操作数的类型。整数除法当两个整数相除时&#xff0c;结果将自动向下取整到最接近的整数。这意味着结果是一个整数&#xff0c;而不是小数。int a 10; int b …

PPT文件密码解密工具推荐:Tenorshare PassFab for PPT绿色免安装一键解除密码限制,附详细教程和下载地址

前段时间&#xff0c;我帮朋友做一个商业演示的 PPT&#xff0c;为了防止文件被误操作或者内容泄露&#xff0c;我给 PPT 设置了密码。结果等朋友来拿文件的时候&#xff0c;我居然把密码忘得干干净净&#xff0c;这下可把我俩都急坏了。朋友那边马上就要用这个 PPT 去参加重要…

【数据结构】二叉树接口实现指南:递归方法的高效运用 (附经典算法OJ)

文章目录 1、前置说明 1、定义二叉树结点结构 2、创建新节点 3、手动创建二叉树 2、二叉树的遍历 1、前序&#xff0c;中序和后序遍历 1.1、二叉树前序遍历 1.2、二叉树中序遍历 1.3、二叉树后序遍历 2、二叉树层序遍历 3、二叉树的基础操作 1、二叉树节点总数 2、…

自动驾驶控制算法——LQR控制算法

自动驾驶控制算法——LQR控制算法 文章目录自动驾驶控制算法——LQR控制算法**一、LQR 是什么&#xff1f;**二、LQR 原理2.1 线性状态空间模型 (State–Space Model)2.2 二次型性能指标 JJJ2.3 代数黎卡提方程 (ARE)2.4 特点总结2.5 一句话总结 LQR 原理&#xff1a;2.5.1 场景…

Jotai:React轻量级原子化状态管理,告别重渲染困扰

简介 Jotai 是一个为 React 提供的原子化状态管理库&#xff0c;采用自下而上的方法来进行状态管理。Jotai 受 Recoil 启发&#xff0c;通过组合原子来构建状态&#xff0c;并且渲染基于原子依赖性进行优化。这解决了 React 上下文的额外重新渲染问题&#xff0c;并消除了对 m…

C语言数据结构(7)贪吃蛇项目2.贪吃蛇项目实现

8. 核心逻辑实现分析 8.1 游戏主逻辑 程序开始就设置程序支持本地模式&#xff0c;然后进入程序的主逻辑。 主逻辑分为3个过程&#xff1a; • 游戏开始&#xff08;GameStart&#xff09;完成游戏的初始化。 • 游戏运行&#xff08;GameRun&#xff09;完成游戏运行逻辑的…

知识蒸馏 - 最小化KL散度与最小化交叉熵是完全等价的

知识蒸馏 - 最小化KL散度与最小化交叉熵是完全等价的 flyfish KL散度与交叉熵的数学关系 对于两个概率分布 PPP&#xff08;真实分布&#xff09;和 QQQ&#xff08;模型预测分布&#xff09;&#xff0c;KL散度的定义是&#xff1a; DKL(P∥Q)∑xP(x)log⁡(P(x)Q(x)) D_{KL}(P…

设计心得——网络包的处理

一、介绍 在程序的开发中&#xff0c;网络开发是一个重要的应用场景。毕竟这些年IT行业之所以火&#xff0c;主要还是互联网&#xff08;移动互联网&#xff09;带来的。网络开发&#xff0c;有各种平台、框架以及系统和库提供的API&#xff0c;如果说网络开发是一个特别复杂和…

sqli-labs通关笔记-第30关GET字符注入(WAF绕过 双引号闭合 手工注入+脚本注入两种方法)

目录 一、源码分析 1、index.php代码审计 2、login.php代码审计 3、java_implimentation函数 4、whitelist函数 5、SQL安全性分析 二、渗透实战 1、进入靶场 2、WAF探测 &#xff08;1&#xff09;触发WAF &#xff08;2&#xff09;绕过WAF 3、手工注入 &#xf…

【openlayers框架学习】九:openlayers中的交互类(select和draw)

文章目录openlayers进阶28 openlayers中的事件29 openlayers中select交互类的使用30 openlayers中select常见的配置选项31 openlayers中绘制交互类&#xff08;Draw&#xff09;openlayers进阶 28 openlayers中的事件 常用进行事件交互的对象&#xff1a;map\view\source29 o…

Java企业级应用性能优化实战

在企业级Java应用开发中,性能优化是确保系统稳定运行的关键因素。本文将从多个维度深入分析Java应用性能瓶颈,并提供实战优化方案。 🎯 性能优化核心领域 1. 对象操作性能优化 在企业应用中,对象拷贝是一个高频操作,特别是在分层架构中的DO、DTO、VO转换。选择合适的拷…

LLM Prompt与开源模型资源(3)如何写一个好的 Prompt

学习材料&#xff1a;https://www.hiascend.com/developer/courses/detail/1935520434893606913 &#xff08;3.5&#xff09;学习时长&#xff1a; 预计 60 分钟学习目的&#xff1a; 了解提示工程的定义与作用熟悉提示工程的关键技术相关概念掌握基于昇腾适配的大模型提示工程…

日志管理工具 ——ELK Stack

一、ELK Stack 概述1.1 核心组件ELK Stack&#xff08;现更名为 Elastic Stack&#xff09;是一套开源的日志收集、存储、分析和可视化平台&#xff0c;由三个核心组件构成&#xff1a;Elasticsearch&#xff1a;分布式搜索引擎&#xff0c;负责日志数据的存储、索引和快速查询…