美文网首页
工作中遇到的一个有趣的边界合并问题

工作中遇到的一个有趣的边界合并问题

作者: alonwang | 来源:发表于2019-03-24 09:55 被阅读0次

    前言

    更新 Guava的RangeSet完美解决这个问题

    实际场景

    前端会持续向后端传递一些数字,这些数字的特征是可重复的,通常是连续的.在某些时候后端需要返回这些数据给前端

    eg. 前端传递了 1,2,3,4 此时后端的返回应该是1,2,3,4

    抽象问题

    概念定义

    image.png

    上面[0,10]称为边界,表示从0到10这个范围.

    问题特征

    按照最简单的设计,后端将这些数据存在set里,前端需要时返回所有数据即可,但是这就忽略了这些数据的特征.

    这些数字通常是连续

    举个例子前端传递了 1,2,3,4,...500 ,那么后端存储1,2,3,4,...500,这不合理,后端浪费了很多存储空间,也增大了网络传输的消耗,理想情况下 后端存储的应该是[1,500]

    上面说的是通常情况下,那么不通常时呢? 前端传递了 1,2,5,6,4 后端存储的应该是[1,2],[4,6],

    基于前端持续传递的特征,后端会取出处理好的边界组(初始时边界组为空),将这个数字插进去,更新边界组.设边界组为list,插入数字为num.会有以下三种情况

    1. num 已经在list中的某个边界内了,不需要更新
    2. num和某个边界相连,eg.list为[1,2],num为3 那么边界组应该更新为[1,3]
    3. num和左右边界相连,eg list为[1,2],[4,5],num为3 那么边界组应该更新为[1,5]

    经过上面的处理list肯定是有序(3),不交叉(2,3),不重复(1)的.

    抽象出算法

    根据上面的分析,问题就可以抽象为向一组有序不交叉不重复边界中插入一个数字

    输入: 一组有序不交叉不重复边界list,一个待插入数字num

    输出: 变化后的边界组list

    放码

    主代码如下:

    package com.github.alonwang;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.List;
    
    public class RangeMergeUtil {
    
        /**
         * 在一组有序不交叉不重复的数字边界中,插入一个新的数字
         * 1. 如果列表为空,新增边界
         * 2. 如果已存在,无变化
         * 3. 如果相连(左相连,右相连,左右均相连),融合
         * 4. 如果不相连,新增一个边界
         * 详见测试用例
         *
         * @param list 不为null
         * @param num  插入数字
         * @return 插入成功 true, 插入失败 false
         */
        public static boolean insert(List<Bound> list, int num) {
            final Bound newBound = new Bound(num, num);
            if (list.isEmpty()) {
                list.add(newBound);
                return true;
            }
    
            //num之前的Bound下标
            int previous = -1;
            //num之后的Bound下标
            int next = -1;
            for (int i = 0; i < list.size(); i++) {
                //已包含无需再处理
                int compare = list.get(i).compareTo(num);
                if (compare == 0) {
                    return false;
                }
    
                if (compare == -1) {
                    previous = i;
                } else if (next == -1) {
                    next = i;
                }
            }
    
            boolean mergeSuccess = false;
            if (previous != -1 && next != -1) {
                mergeSuccess = mergeBound(list, num, previous, next);
            } else if (previous != -1) {
                mergeSuccess = mergePrevious(num, list, previous);
            } else {
                mergeSuccess = mergeNext(num, list, next);
            }
            //两侧都未发生merge,插入新Bound
            if (!mergeSuccess) {
                list.add(previous + 1, newBound);
            }
            return true;
        }
    
        private static boolean mergeBound(List<Bound> list, int i, int previous, int next) {
            boolean mergePrevious = mergePrevious(i, list, previous);
            boolean mergeNext = mergeNext(i, list, next);
            //两边都merge成功了,合并previous和next
            if (mergePrevious & mergeNext) {
                Bound previousBound = list.get(previous);
                Bound nextBound = list.get(next);
                list.add(previous + 1, new Bound(previousBound.getStart(), nextBound.getEnd()));
                list.remove(previousBound);
                list.remove(nextBound);
            }
            return mergePrevious | mergeNext;
        }
    
        private static boolean mergeNext(int i, List<Bound> list, int next) {
            Bound nextBound = list.get(next);
            if (nextBound.getStart() == i + 1) {
                nextBound.setStart(i);
                list.set(next, nextBound);
                return true;
            }
            return false;
        }
    
        private static boolean mergePrevious(int i, List<Bound> list, int previous) {
            Bound previousBound = list.get(previous);
            if (previousBound.getEnd() == i - 1) {
                previousBound.setEnd(i);
                list.set(previous, previousBound);
                return true;
            }
            return false;
        }
    
    
        @AllArgsConstructor
        @NoArgsConstructor
        @Data
        public static class Bound {
            private int start;
            private int end;
    
            public static Bound of(int start, int end) {
                return new Bound(start, end);
            }
    
            public int compareTo(int i) {
                if (start > i) {
                    return 1;
                } else if (end < i) {
                    return -1;
                } else {
                    return 0;
                }
            }
        }
    
    }
    
    

    测试代码如下:

    import com.github.alonwang.RangeMergeUtil
    import spock.lang.Specification
    
    import static com.github.alonwang.RangeMergeUtil.Bound.of
    
    class RangeMergeUtilTest extends Specification {
    
        def "test bound"() {
    
            when:
    
            def result = RangeMergeUtil.insert(array, i)
            then:
            array[idx] == bound
            array.size() == size
            result == success
    
            where:
            array                | i  | idx | bound      | size | success
            //为空
            []                   | 3  | 0   | of(3, 3)   | 1    | true
            //已存在
            [of(1, 2), of(7, 9)] | 2  | 0   | of(1, 2)   | 2    | false
            //存在merge,边界变化
            [of(1, 2), of(7, 9)] | 3  | 0   | of(1, 3)   | 2    | true
            [of(1, 2), of(7, 9)] | 6  | 1   | of(6, 9)   | 2    | true
            [of(1, 2), of(7, 9)] | 10 | 1   | of(7, 10)  | 2    | true
            //无merge  新增bound
            [of(3, 4), of(6, 9)] | 1  | 0   | of(1, 1)   | 3    | true
            [of(3, 4), of(6, 9)] | 11 | 2   | of(11, 11) | 3    | true
            //左右merge,bound减少
            [of(3, 4), of(6, 7)] | 5  | 0   | of(3, 7)   | 1    | true
    
    
        }
    }
    
    

    ## 总结

    反思

    看到这个问题我的第一想法是用redis的bitmap来搞,这些存储逻辑会很简单,取出时用bitpos来获取左右边界值就行了,但是限于项目中jar包版本,有个指令无法使用.只能放弃(后面leader提醒bitpos是O(n)的,在我这个场景里,最坏情况下会是O(n^2),)

    由于这是个临时需求,马上就要验收了,就想着简单点搞全存全取好了,奈何leader说这样不行,虽然当时说了会改,内心还是有点不情愿的.等写出这个算法后,只想说:"leader英明",感觉很幸运,有个靠谱的leader真的会学到很多.

    更进一步

    写完之后发现这个问题还可以再抽象一下: 向一组有序不交叉不重复边界中插入一个新的边界,这个处理起来会更麻烦,后续有时间会尝试一下.

    直觉告诉我肯定有更好的方法去处理这个问题,哪位知晓的请告知


    源码已上传github

    相关文章

      网友评论

          本文标题:工作中遇到的一个有趣的边界合并问题

          本文链接:https://www.haomeiwen.com/subject/lebyvqtx.html