缓存算法上限引起的思考

问题背景:

  • 跨端项目中,客户端提供了一个list组件,该组件具有缓存功能,能够缓存最多n条组件,n为int值,故存在上限2^32-1。若需展示组件为新组件,则需要生成并缓存其id,若为已生成过组件即可使用缓存的组件模板渲染。
  • 前端实现中,每一个组件(后称为卡片)是由m条原子组件(后称为组件)组成,客户端缓存的是卡片,故前端需要构建一套支持【卡片内组件列表】转【卡片id】、【卡片id】转【卡片内组件列表】的可逆算法。
  • 该问题中涉及缓存及底层循环,故需要重视时间复杂度。

原实现方案:

ini 复制代码
// 卡片内最大组件数量
const MAX_SIZE = 6;

export const intArr2int = (value: number[]): number => {
  let result: number = 0;
  const size = value.length;
  for (let i = 0; i < size; i++) {
    let tmp = value[i] & 0x1f;
    result = (tmp << (5 * i)) | result;
  }
  return result;
};

export const int2IntArr = (value: number): number[] => {
  let result: number[] = new Array(MAX_SIZE);
  for (let i = 0; i < MAX_SIZE; i++) {
    result[i] = (value >> (5 * i)) & 0x1f;
  }
  return result;
};

该方案中,设计思路如下:

  1. 将【组件id】转为5位的2进制数值,此时组件id转为2^5的5位2进制数值;
  2. 卡片中每有1个组件,将这个组件的5位2进制数值拼接在一起,构成一个位数上限为30的二进制数值,由于位数上限为30故其上限小于2^32-1,此时生成的这个30位二进制数值就是【卡片id】;
  3. 同理可以通过将该【卡片id】转为2进制,然后5位一裁剪,即可转化为组件id;

此时已满足需求,但存在以下问题:

  1. 存在组件上限,且上限较低,因为是5位的二进制,为2^5-1即31。
  2. 当前这个算法是通过位运算执行,没有存在循环,复杂度也较低。

故要解决组件支持上限的问题,首先要考虑为什么会有上限,且抛弃算法实现,是否存在上限。

已知安卓int型数值为32位,存在一位符号位。故实际上限为2^31-1。

由于包括正负值,故可选数值为-2\^31-1,2\^31-1,约等于2^32,一个卡片最多包括6个组件,设支持最大组件数为x,得出以下不等式:

x^6<2^32-1

解得 x<40.1...

故最大整数解为40,所以支持组件的所有类型上限为40

方案2:尝试以40作为进制底数构建新算法:

ini 复制代码
// 组件最大数量,也是使用40进制计算
const OFFSET=40
const INT_LIMIT=Math.pow(2,16)-1

function getArr(value){
    let multiple=value+INT_LIMIT
    let leftVal=0
    const arr=[]
    while(multiple>OFFSET){
        leftVal=multiple%OFFSET
        multiple=Math.floor(multiple/OFFSET)
        arr.unshift(leftVal)
    }
    arr.unshift(multiple)
    return arr
}

function getVal(arr){
    let val=0
    for(let i=0;i<arr.length-1;i++){
        val=(arr[i]+val)*OFFSET
    }
    val+=arr[arr.length-1]
    return val -INT_LIMIT
}

此方案依然满足需求,根本问题没有解决,组件上限为40,依然较小,且存在进制运算产生的一轮循环,造成了复杂度升高O(n)。故依然不可取。

重新考虑思路是否被局限。目标是通过组件id生成唯一的卡片id,并可互逆转换。原算法以固定逻辑生成卡片id,好处是可以产生唯一的对应关系,但缺点是产生了很多业务中并没有用到的卡片的对应关系。

方案3:故新思路如下:

  1. 每次渲染卡片,将卡片使用组件id保存为2位数字符串(保证未来拓展组件上限为99,后续如需拓展更多,保存为3位或更高位即可)
  2. 组件id的2位字符串拼接为一个6*2的字符串
  3. 判断缓存数组是否包含该字符串,若不包含,推入缓存数组,包含缓存数组不变
  4. 向客户端传入卡片id即为缓存数组的index值
typescript 复制代码
export class ListStorage {
  storageList: string[];
  constructor() {
    this.storageList = [];
  }
  addStorage(list) {
    const result = list
      .map(num => {
        if (num < 10) {
          return '0' + num;
        } else {
          return num.toString();
        }
      })
      .join('');
    if (!this.storageList.includes(result)) {
      this.storageList.push(result);
    }
    return this.storageList.indexOf(result);
  }
  storageItem2Arr(value: number) {
    const VALUE_SIZE = 2; // 替换为实际的大小
    let arr: number[] = [],
      str = this.storageList[value];
    while (str.length > 0) {
      if (str.length > VALUE_SIZE) {
        arr.unshift(Number(str.slice(-VALUE_SIZE)));
        str = str.slice(0, str.length - VALUE_SIZE);
      } else {
        arr.unshift(Number(str));
        str = '';
      }
    }
    return arr;
  }
  getStorage() {
    return this.storageList;
  }
}

当前方案使基本功能已完成,但用数组保存,由于判重操作以及indexof产生了O(n)的复杂度,存在优化空间。

方案4:由于存在互逆查询的操作,故不可简单将数组使用Map替代,否则用卡片id获取组件组成时依然存在循环,产生时间复杂度。故考虑空间换时间,存两个缓存对象,一个Map一个数组,分别可以键值互换。

kotlin 复制代码
export class ListStorage3 {
  storageList: Map<string, number>;
  storageArr: string[];
  constructor() {
    this.storageList = new Map();
    this.storageArr = [];
  }

  addStorage(list) {
    const result = list
      .map(num => {
        if (num < 10) {
          return '0' + num;
        } else {
          return num.toString();
        }
      })
      .join('');

    if (this.storageList.has(result)) {
      return this.storageList.get(result);
    }
    this.storageList.set(result, this.storageList.size);
    this.storageArr.push(result);
    return this.storageList.size;
  }

  storageItem2Arr(value) {
    const VALUE_SIZE = 2; // 替换为实际的大小
    let arr: number[] = [],
      str = this.storageArr[value];
    while (str.length > 0) {
      if (str.length > VALUE_SIZE) {
        arr.unshift(Number(str.slice(-VALUE_SIZE)));
        str = str.slice(0, str.length - VALUE_SIZE);
      } else {
        arr.unshift(Number(str));
        str = '';
      }
    }
  }

  getStorage() {
    return this.storageList;
  }
}

此时 addStorage,storageItem2Arr相对组件数的时间复杂度均为O(1),组件上线为99,并依然可以扩展为百位,故最终使用【方案4】

相关推荐
To_OC2 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic3 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy3 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志3 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble4 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯5 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜6 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司6 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
江华森7 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http
IT_陈寒7 小时前
Redis的KEYS命令把我搞崩溃了,改用SCAN才活过来
前端·人工智能·后端