问题背景:
- 跨端项目中,客户端提供了一个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;
};
该方案中,设计思路如下:
- 将【组件id】转为5位的2进制数值,此时组件id转为2^5的5位2进制数值;
- 卡片中每有1个组件,将这个组件的5位2进制数值拼接在一起,构成一个位数上限为30的二进制数值,由于位数上限为30故其上限小于2^32-1,此时生成的这个30位二进制数值就是【卡片id】;
- 同理可以通过将该【卡片id】转为2进制,然后5位一裁剪,即可转化为组件id;
此时已满足需求,但存在以下问题:
- 存在组件上限,且上限较低,因为是5位的二进制,为2^5-1即31。
- 当前这个算法是通过位运算执行,没有存在循环,复杂度也较低。
故要解决组件支持上限的问题,首先要考虑为什么会有上限,且抛弃算法实现,是否存在上限。
已知安卓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:故新思路如下:
- 每次渲染卡片,将卡片使用组件id保存为2位数字符串(保证未来拓展组件上限为
99
,后续如需拓展更多,保存为3位或更高位即可) - 组件id的2位字符串拼接为一个
6*2
的字符串 - 判断缓存数组是否包含该字符串,若不包含,推入缓存数组,包含缓存数组不变
- 向客户端传入卡片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;
}
}