当前位置 博文首页 > dennis_jiang的博客:速度提高几百倍,记一次数据结构在实际工作

    dennis_jiang的博客:速度提高几百倍,记一次数据结构在实际工作

    作者:[db:作者] 时间:2021-09-20 22:55

    这段时间写了一堆源码解析,这篇文章想换换口味,跟大家分享一个我工作中遇到的案例。毕竟作为一个打工人,上班除了摸鱼看源码外,砖还是要搬的。本文会分享一个使用恰当的数据结构来进行性能优化,从而大幅提高响应速度的故事,提高有几百倍那么多。

    事情是这样的,我现在供职一家外企,我们有一个给外国人用的线下卖货的APP,卖的商品有衣服,鞋子,可乐什么的。某天,产品经理找到我,提了一个需求:需要支持三层的产品选项。听到这个需求,我第一反应是我好像没有见到过三层的产品选项,毕竟我也是一个十来年的资深剁手党,一般的产品选项好像最多两层,比如下面是某电商APP一个典型的鞋子的选项:

    image-20201116172602218

    这个鞋子就是两层产品选项,一个是颜色,一个是尺码,颜色总共有11种,尺码总共也是11种。为了验证我的直觉,我把我手机上所有的购物APP,啥淘宝,京东,拼多多,苏宁易购全部打开看了一遍。在我看过的商品中,没有发现一个商品有三层选项的,最多也就两层

    本文可运行的示例代码已经上传GitHub,大家可以拿下来玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/DataStructureAndAlgorithm/OptimizeVariations

    为什么没人做三层选项?

    一两家不做这个,可能是各家的需求不一样,但是大家都不做,感觉事情不对头。经过仔细分析后,我觉得不做三层选项可能有以下两个原因:

    1. 这可能是个伪需求

    上面这个鞋子有11种颜色,11种尺码,意味着这些选项后面对应的是11 * 11,总共121个商品。如果再来个第三层选项,假设第三层也有11个选项,那对应的商品总共就是11 * 11 * 11,也就是1331个商品,好多店铺总共可能都没有1331个商品。也就是说,第三层选项可能是个伪需求,用户并没有那么多选项放在第三层,还是以上面的鞋子为例,除了颜色,尺码外,非要再添一个层级,那只能是性别了,也就是男鞋和女鞋。对于男鞋和女鞋来说,版型,尺码这些很不一样,一般都不会放到一个商品下面,更常用的做法是分成两个商品,各自有自己的颜色和尺码。

    2. 有性能问题

    仅仅是加上第三层选项这个功能并没有什么难的,也就是多展示几个可以点击的按钮而已,点击逻辑跟两层选项并没有太大区别。但是细想下去,我发现了他有潜在的性能问题。以上面这双鞋子为例,我从后端API拿到的数据是这样的:

    const merchandise = {
      // variations存放的是所有选项
      variations: [
        {
          name: '颜色',
          values: [
            { name: '限量版574海军蓝' },
            { name: '限量版574白粉' },
            // 下面还有9个
          ]
        },
        {
          name: '尺码',
          values: [
            { name: '38' },
            { name: '39' },
            // 下面还有9个
          ]
        },
      ],
      // products数组存放的是所有商品
      products: [
        {
          id: 1,
          price: 208,
          // 与上面variations的对应关系在每个商品的variationMappings里面
          variationMappings: [
            { name: '颜色', value: '限量版574白粉' },
            { name: '尺码', value: '38'},
          ]
        },
        // 下面还有一百多个产品
      ]
    }
    

    上面这个结构本身还是挺清晰的,merchandise.variations是一个数组,有几层选项,这个数组就有几个对象,每个对象的name就是当前层级的名字,values就是当前层级包含的选项,所以merchandise.variations可以直接拿来显示在UI上,将他们按照层级渲染成按钮就行。

    上面图片中,用户选择了第一层的限量版574白粉,第二层的4041等不存在的商品就自动灰掉了。用上面的数据结构可以做到这个功能,当用户选择限量版574白粉的时候,我们就去遍历merchandise.products这个数组,这个数组的一个项就是一个商品,这个商品上的variationMappings会有当前商品的颜色尺码信息。对于我当前的项目来说,如果这个商品可以卖,他就会在merchandise.products这个数组里面,如果不可以卖,这个数组里面压根就不会有这个商品。比如上图的限量版574白粉40码的组合就不会出现在merchandise.products里面,查找的时候找不到这个组合,那就会将它变为灰色,不可以点。

    所以对于限量版574白粉40这个鞋子来说,为了知道它需不需要灰掉,我需要整个遍历merchandise.products这个数组。按照前面说的11个颜色,11个尺码来说,最多会有121个商品,也就是最多查找121次。同样的要知道限量版574白粉41这个商品可以不可以卖,又要整个遍历商品数组,11个尺码就需要将商品数组整个遍历11次。对于两层选项来说,11 * 11已经算比较多的了,每个尺码百来次运算可能还不会有严重的性能问题。但是如果再加一层选项,新加这层假如也有11个可选项,这复杂度瞬间就增加了一个指数,从 O ( n 2 ) O(n^2) O(n2)变成 O ( n 3 ) O(n^3) O(n3)!现在我们的商品总数是11 * 11 * 11,也就是1331个商品,假如第三层是性别,现在为了知道限量版574白粉40男性这个商品可不可以卖,我需要遍历1331个商品,如果遍历121个商品需要20ms,还比较流畅,那遍历1331个商品就需要220ms,这明显可以感觉到卡顿了,在某些硬件较差的设备上,这种卡顿会更严重,变得不可接受了。而且我们APP使用的技术是React Native,本身性能就比原生差,这样一来,用户可能就怒而卸载了!

    我拿着上述对需求的疑问,和对性能的担心找到了产品经理,发生了如下对话:

    我:大佬,我发现市面上好像没有APP支持三层选项的,这个需求是不是有问题哦,而且三层选项相较于两层选项来说,复杂度是指数增长,可能会有性能问题,用户用起来会卡的。

    产品经理:兄弟,你看的都是国内的APP,但是我们这个是给外国人用的,人家外国人就是习惯这么用,咱要想卖的出去就得满足他们的需求。太卡了肯定不行,性能问题,想办法解决嘛,这就是在UI上再加几个按钮,设计图都跟以前是一样的,给你两天时间够了吧~

    我:啊!?额。。。哦。。。

    咱也不认识几个外国人,咱也不敢再问,都说了是用户需求,咱必须满足了产品才卖的出去,产品卖出去了咱才有饭吃,想办法解决吧!

    解决方案

    看来这个需求是必须要做了,这个功能并不复杂,因为三层选项可以沿用两层选项的方案,继续去遍历商品数组,但是这个复杂度增长是指数级的,即从 O ( n 2 ) O(n^2) O(n2)变成 O ( n 3 ) O(n^3) O(n3),用户用起来会卡。现在,我需要思考一下,有没有其他方案可以提高性能。经过仔细思考,我发现,这种指数级的复杂度增长是来自于我们整个数组的遍历,如果我能够找到一个方法不去遍历这个数组,立即就能找到限量版574白粉40男性对应的商品存不存在就好了。

    这个具体的问题转换一下,其实就是:在一个数组中,通过特定的过滤条件,查找符合条件的一个项。嗯,查找,听起来蛮耳熟的,现在我之所以需要去遍历这个数组,是因为这些查找条件跟商品间没有一个直接的对应关系,如果我能建立一个直接的对应关系,不就可以一下就找到了吗?我想到了:查找树假如我重组这些层级关系,将它们组织为一颗树,每个商品都对应树上的一个叶子节点,我可以将三层选项的查找复杂度从 O ( n 3 ) O(n^3) O(n3)降到 O ( 1 ) O(1) O(1)

    两层查找树

    为了说明白这个算法,我先简化这个问题,假设我们现在有两层选项,颜色尺码,每层选项有两个可选项:

    1. 颜色:白色,红色
    2. 尺码:39,40

    我们现在对应有4个商品:

    1. 一号商品:productId为1,白色,39码
    2. 二号商品:productId为2,白色,40码
    3. 三号商品:productId为3,红色,39码
    4. 四号商品:productId为4,红色,40码

    如果按照最简单的做法,为了查找红色39码鞋子存不存在,我们需要遍历所有的这四个商品,这时候的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。但是如果我们构建像下面这样一颗树,可以将时间复杂度降到 O ( 1 ) O(1) O(1)

    image-20201117160534500

    上面这颗树,我们忽略root节点,在本例中他并没有什么用,仅仅是一个树的入口,这棵树的第一层淡黄色节点是我们第一层选项颜色,第二层淡蓝色节点是我们的第二层选项尺码,只是每个颜色节点都会对应所有的尺码,这样我们最后第二层的叶子节点其实就对应了具体的商品。现在我们要查找红色39码鞋子,只需要看图中红色箭头指向的节点上有没有商品就行了。

    那这种数据结构在JS中该怎么表示呢?其实很简单,一个对象就行了,像这样:

    const tree = {
      "颜色:白色": {
        "尺码:39": { productId: 1 },
        "尺码:40": { productId: 2 }
      },
      "颜色:红色": {
        "尺码:39": { productId: 3 },
        "尺码:40": { productId: 4 }
      }
    }
    

    有了上面这个数据结构,我们要查找红色39码直接取值tree["颜色:红色"]["尺码:39"]就行了,这个复杂度瞬间就变为 O ( 1 ) O(1) O(1)了。

    三层查找树

    理解了上面的两层查找树,要将它扩展到三层就简单了,直接再加一层就行了。假如我们现在第三层选项是性别,有两个可选项,那我们的查找树就是这样子的:

    image-20201118133333379

    对应的JS对象:

    const tree = {
      "颜色:白色": {
        "尺码:39": { 
        	"性别:男": { productId: 1 },
          "性别:女": { productId: 2 },
        },
        "尺码:40": { 
        	"性别:男": { productId: 3 },
          "性别:女": { productId: 4 },
        }
      },
      "颜色:红色": {
        "尺码:39": { 
        	"性别:男": { productId: 5 },
          "性别:女": { productId: 6 },
        },
        "尺码:40": { 
        	"性别:男": { productId: 7 },
          "性别:女": { productId: 8 },
        }
      }
    }
    

    同样的,假如我们要查找一个白色的,39码的鞋子,直接tree["颜色:白色"]["尺码:39"]["性别:男"]就行了,这个时间复杂度也是 O ( 1 ) O(1) O(1)

    写代码

    上面算法都弄明白了,剩下的就是写代码了,我们主要需要写的代码就是用API返回的数据构建一个上面的tree这种结构就行了,一次遍历就可以做到。比如上面这个三层查找树对应的API返回的结构是这样的:

    const merchandise = {
      variations: [
        {
          name: '颜色',
          values: [
            { name: '白色' },
            { name: '红色' },
          ]
        },
        {
          name: '尺码',
          values: [
            { name: '39' },
            { name: '40' },
          ]
        },
        {
          name: '性别',
          values: [
            { name: '男' },
            { name: '女' },
          ]
        }
    
    下一篇:没有了