缓存优化模拟

缓存优化模拟

问题背景

缓存是提升数据库查询效率的关键技术。为了对缓存机制进行性能评估和优化,我们需要使用一组训练数据来模拟数据的访问过程,并找出在理想情况下,数据库的最少访问次数。

模拟访问规则

  1. 缓存命中(Hit) : 当需要查询的数据已经存在于缓存中时,直接从缓存获取,无需访问数据库

  2. 缓存未命中(Miss) : 当需要查询的数据不在缓存中时,判定为一次"未命中",此时必须访问数据库

  3. 数据加载与替换:

    • 数据库访问后,需要将查询到的数据放入缓存。
    • 如果此时缓存已满,必须先从缓存中删除一个已有的数据,为新数据腾出空间。这个删除的过程称为"替换"或"驱逐"。

优化目标:最少的数据库访问

为了实现最少的数据库访问次数,我们在缓存已满、需要替换数据时,必须做出最优选择。最优的替换策略是:

替换掉那个在未来最长时间内不会被访问的数据。

这是一种"先知"般的策略,因为它需要预知未来的所有访问请求来做出当前最好的决定。

任务要求

给定缓存的大小 cacheSize 和一组按顺序访问的训练数据 dataIds,请模拟整个访问过程。在每次需要替换缓存数据时,都采用上述的最优策略,最终计算出整个过程所产生的最少数据库访问次数。


输入格式

  • cacheSize: 第一个参数,一个整数,表示缓存的大小。

    • 1 <= cacheSize <= 100
  • dataIds: 第二个参数,一个整数数组,表示按顺序需要查询的数据编号。

    • 0 <= dataIds.length <= 1000
    • 0 <= dataIds[i] <= 1000
  • 假设 : 每个数据在缓存中占用的空间都为 1


输出格式

  • 一个整数,代表在最优替换策略下,数据库的最少访问次数。

样例说明

样例 1

  • 输入:

    • cacheSize = 2
    • dataIds = [1, 2, 3, 1, 2, 3]
  • 输出 : 4

  • 解释:

    我们逐步模拟这个过程,并记录数据库访问次数(DB Accesses)和缓存状态(Cache)。

    1. 查询 1:

      • Miss : 缓存中没有 1
      • DB Accesses: 1
      • Cache : [1]
    2. 查询 2:

      • Miss : 缓存中没有 2
      • DB Accesses: 2
      • Cache : [1, 2] (缓存已满)
    3. 查询 3:

      • Miss : 缓存中没有 3
      • DB Accesses: 3
      • 决策 : 缓存 [1, 2] 已满,需要替换一个。查看未来的访问序列 [1, 2, 3]。数据 1 紧接着就会被访问,数据 21 之后被访问。因此,应该替换掉未来最晚被访问的 2
      • Cache : [1, 3]
    4. 查询 1:

      • Hit : 缓存中有 1
      • DB Accesses: 3
      • Cache : [1, 3]
    5. 查询 2:

      • Miss : 缓存中没有 2
      • DB Accesses: 4
      • 决策 : 缓存 [1, 3] 已满,需要替换一个。查看未来的访问序列 [3]。数据 3 马上会被访问,而数据 1 在未来不再出现。因此,替换掉 1
      • Cache : [2, 3]
    6. 查询 3:

      • Hit : 缓存中有 3
      • DB Accesses: 4
      • Cache : [2, 3]

    整个过程结束,总共访问了 4 次数据库。

样例 2

  • 输入:

    • cacheSize = 1
    • dataIds = [100, 200, 100, 200]
  • 输出 : 4

  • 解释:

    • 缓存大小为 1,意味着每次访问一个新数据,都必须从数据库读取,并替换掉缓存中已有的数据。
    • 序列 100, 200, 100, 200 中的每一次访问都是一次 Miss。
    • 因此,总共需要访问 4 次数据库。
java 复制代码
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.Arrays;

/**
 * 解决模拟缓存访问,计算最少数据库访问次数的问题。
 */
public class Main {
    public static void main(String[] args) {
        // ACM 模式的输入处理
        Scanner scanner = new Scanner(System.in);
        int cacheSize = Integer.parseInt(scanner.nextLine().trim());
        String dataLine = scanner.nextLine().trim();
        scanner.close();

        // 将输入的字符串行解析为整数数组
        int[] dataIds;
        if (dataLine.isEmpty()) {
            dataIds = new int[0];
        } else {
            String[] dataIdStrings = dataLine.split("\s+");
            dataIds = new int[dataIdStrings.length];
            for (int i = 0; i < dataIdStrings.length; i++) {
                dataIds[i] = Integer.parseInt(dataIdStrings[i]);
            }
        }

        // 调用核心逻辑并打印结果
        System.out.println(minDatabaseAccess(cacheSize, dataIds));
    }

    /**
     * 计算在最优缓存替换策略下的最少数据库访问次数。
     *
     * @param cacheSize 缓存大小
     * @param dataIds   按顺序访问的数据ID列表
     * @return 最少的数据库访问次数
     */
    public static int minDatabaseAccess(int cacheSize, int[] dataIds) {
        // --- 边界情况处理 ---
        // 如果没有缓存容量,每次访问都需要读数据库
        if (cacheSize <= 0) {
            return dataIds.length;
        }
        // 如果没有数据访问请求,则访问次数为0
        if (dataIds.length == 0) {
            return 0;
        }

        // --- 初始化 ---
        // 使用 HashSet 模拟缓存,提供 O(1) 的平均查找时间
        Set<Integer> cache = new HashSet<>();
        // 数据库访问次数计数器
        int dbAccessCount = 0;

        // --- 遍历访问序列 ---
        for (int i = 0; i < dataIds.length; i++) {
            int currentDataId = dataIds[i];

            // 1. 检查是否缓存命中
            if (cache.contains(currentDataId)) {
                // 缓存命中,无需访问数据库,继续下一次访问
                continue;
            }

            // 2. 缓存未命中,必须访问数据库
            dbAccessCount++;

            // 3. 将数据放入缓存
            // a. 如果缓存未满
            if (cache.size() < cacheSize) {
                cache.add(currentDataId);
            }
            // b. 如果缓存已满,执行最优替换策略 (Belady's Algorithm)
            else {
                int itemToEvict = -1;      // 记录要被替换出的数据
                int farthestNextUse = -1; // 记录最远的下一次访问位置

                // 遍历当前缓存中的所有数据,找到在未来最晚被使用的一个
                for (int itemInCache : cache) {
                    int nextUseIndex = Integer.MAX_VALUE; // 默认为无穷远(未来不再使用)

                    // 在未来的请求序列中查找 itemInCache 的下一次出现位置
                    for (int j = i + 1; j < dataIds.length; j++) {
                        if (dataIds[j] == itemInCache) {
                            nextUseIndex = j; // 找到了,记录索引
                            break;          // 只关心第一次出现,所以可以 break
                        }
                    }

                    // 如果这个数据的下一次使用位置比目前记录的最远位置还要远
                    if (nextUseIndex > farthestNextUse) {
                        farthestNextUse = nextUseIndex;
                        itemToEvict = itemInCache;
                    }
                    // 如果 nextUseIndex 是 Integer.MAX_VALUE,说明它再也不会被用到,
                    // 它将成为 farthestNextUse 的最大值,是最佳的替换对象。
                }

                // 从缓存中移除确定要替换的数据
                cache.remove(itemToEvict);
                // 将新数据加入缓存
                cache.add(currentDataId);
            }
        }

        // 返回总的数据库访问次数
        return dbAccessCount;
    }
}
相关推荐
2301_801821715 分钟前
机器学习-线性回归模型和梯度算法
python·算法·线性回归
电院大学僧7 分钟前
初学python的我开始Leetcode题-13
python·算法·leetcode
enzeberg7 分钟前
全面解析前端领域的算法
算法
2301_8167431116 分钟前
Java-数构链表
java·开发语言·链表
程序无bug17 分钟前
如何使用Redis实现电商系统的库存扣减?
java·后端
玩代码35 分钟前
访问者设计模式
java·设计模式·访问者设计模式
小张快跑。44 分钟前
【Java入门到精通】(五)初识MySql数据库
java·数据库·mysql
Jinkxs1 小时前
Spring MVC 执行流程详解:一次请求经历了什么?
java·spring·mvc
程序无bug1 小时前
Java 服务性能优化,提升QPS
java·后端
阑梦清川1 小时前
使用C语言原生的字符串函数求解的一道题目总结
算法