# 二分算法

本文通过二分算法解决实际业务问题,语言实现 Java。

# 业务需求

线索id(eventId)与最终线索 id(finalEventId)的 mapping 关系。

背景

有一张线索表,线索 id 为主键,当检测到线索 id 的信息有重复之后(例如同样的电话、邮箱等)会对线索 id 进行合并。然后生成一个新的线索。

# 线索6和7合并为8 ,然后线索8和9合并为10
10
├── 8
│   ├── 6
│   └── 7
└── 9
5
├── 4
│   └── 3
└── 1

# 最终要呈现的效果如下
eventId     finalEventId
1           5
3           5
4           5
9           10
7           10
6           10
8           10

数据表字段

# merge_result 里面记录的是 event_id ,被合并成了 新的 event_id, 如果 merge_result字段为空则表示event_id不是其他event_id合并而来的。
            merge_result             | event_id
-------------------------------------+----------
 13405000,7297679                    | 13405001
 13017581,12782752,13405046,13017582 | 13405047
 13405345,13012323                   | 13405346
 13405368,13012335                   | 13405369
 13405391,13012348                   | 13405392
 13405414,13012431                   | 13405415
 13017741,12782840,13405437,13017742 | 13405438
 13405529,13012450                   | 13405530
 13405575,13012491                   | 13405576
 13405598,13091543                   | 13405599
                                     | 14730559

# 处理方案

要获得finalEventId就需要一层层查询合并记录,一个eventId被合并之后,新的eventId还有可能被合并。因此我们需要进行递归查询。当我们将合并关系(merge_result)和(event_id)数据量为1亿条,合并前需要获得最终event_id 的记录为4千万条。4万千与1亿条数据进行递归查询就有点可怕了。因此采用二分算法进行查询。

# 数据准备

先在数据库中将数据展开,方便程序读处理。

select
  event_id,from_event_id from table
lateral view explode(split(merge_result,',')) dual as from_event_id

-- 「业务背景」中的例子数据展开后如下:

event_id,from_event_id
10        8
8         6
8         7
10        9
5         4
4         3
5         1

读取展开后的数据保存到列表

try (Stream<String> lines = Files.lines(Paths.get(file.getPath()))) {
    resultList.addAll(lines
            .map(line -> line.split("\001"))
            .collect(Collectors.toList()));
}

生成递归查找Event清单,这里一定要排序哦

List<Integer[]> eventList = resultList.stream()
        .filter(i->i.length==2)
        .map(i -> new Integer[]{Integer.parseInt(i[0]), Integer.parseInt(i[1])})
        .sorted(Comparator.comparing(i -> i[1]))
        .collect(Collectors.toList());

生成需要匹配Event清单

List<Integer[]> result = querySet.parallelStream().map(i-> new Integer[]{i,findFinalEventId(i,eventList,i)}).collect(Collectors.toList());

# 匹配结果

建立二分查找的函数

    /**
     * 二分查找
     *
     * @param num          被查询的列表
     * @param queryEventId 查询的eventId
     * @return 匹配到的finalEventid
     */
    public static int binarySearch(List<Integer[]> num, int queryEventId) {

        //开始位置
        int start = 0;
        //结束位置
        int end = num.size() - 1;
        while (start <= end) {
            //中间索引元素
            int middle = (start + end) / 2;
            if (num.get(middle)[1] > queryEventId) {
                end = middle - 1;
            } else if (num.get(middle)[1] < queryEventId) {
                start = middle + 1;
            } else {
                return num.get(middle)[0];
            }
        }
        return -1;
    }

建立递归查找函数,递归的过程中通过二分算法获取数据。

    /**
     * @param queryEventId  需要查询的 eventId
     * @param fullEventList 所有 eventId 清单
     * @param finalEventId  最终 eventId
     * @return 最终 eventId
     */
    private static Integer findFinalEventId(Integer queryEventId, List<Integer[]> fullEventList, Integer finalEventId) {

        Integer nextQueryEventId = null;

        int tmpFinalEventId = binarySearch(fullEventList, queryEventId);
        if (tmpFinalEventId != -1) {
            finalEventId = tmpFinalEventId;
            nextQueryEventId = finalEventId;
        }

        if (nextQueryEventId != null) {
            finalEventId=findFinalEventId(nextQueryEventId, fullEventList, finalEventId);
        }

        return finalEventId;
    }

通过并行的 stream 对每个 eventid 进行递归查找

List<Integer[]> result = querySet.parallelStream().map(i-> new Integer[]{i,findFinalEventId(i,eventList,i)}).collect(Collectors.toList());

源码:get_final_event_id