缓存算法上限引起的思考

问题背景:

  • 跨端项目中,客户端提供了一个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】

相关推荐
golitter.5 分钟前
Ajax和axios简单用法
前端·ajax·okhttp
雷特IT24 分钟前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
长路 ㅤ   1 小时前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
亚里士多没有德7751 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010141 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw1 小时前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
九圣残炎1 小时前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱2 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css
TU^2 小时前
C语言习题~day16
c语言·前端·算法