另辟蹊径-利用算法技巧优化复杂对象比较大小的问题
问题背景
考虑这样一个问题:有一批队列,这批队列中有一批任务,在任务中的字段定义如下:
public class Task implements Comparable<Task> {
/**
* 任务id
*/
private long id;
/**
* 入队时间
*/
private long addTime;
/**
* 是否处于专属队列中
*/
private boolean inPrivateQueue;
/**
* 是否处于vip队列中
*/
private boolean inVipQueue;
/**
* 所处队列的优先级
*/
private int priority;
// 比较函数
// 如果task1 < task2, 则返回-1;
// 如果task1 == task2, 则返回0;
// 如果task1 > task2, 则返回1
@Override
public int compareTo(Task o) {
// TODO
return 0;
}
}
// 使用如下方式对task进行排序,当执行sorted方法的时候,任务大小的比较规则就会按照Task中的compareTo() 方法来进行比较
public void sort(List<Task> tasks) {
tasks.stream().sorted().collect(Collectors.toList());
}
需要按如下规则实现任务的 compareTo(Task o)
函数:
-
比较结果符合Java中对于比较函数的定义:如果task1 < task2, 则返回-1;如果task1 == task2, 则返回0;如果task1 > task2, 则返回1。
-
比较规则如下:
-
如果专属队列有任务,则从专属队列中选择排队时间最长的任务
-
如果专属队列无任务,VIP队列中有任务,则选择VIP队列中的任务
- 对VIP队列中的任务按队列优先级进行排序,选择优先级最高的队列
- 如果优先级最高的队列只有一个,则从该队列中选择排队时间最长的任务
- 如果优先级最高的队列有多个,则从这多个队列中选择排队时间最长的任务
-
如果专属队列无任务,VIP队列中无任务,则选择非VIP队列中的任务
- 对非VIP队列中的任务按队列优先级进行排序,选择优先级最高的队列
- 如果优先级最高的队列只有一个,则从该队列中选择排队时间最长的任务
- 如果优先级最高的队列有多个,则从这多个队列中选择排队时间最长的任务
-
将比较规则用思维导图表示如下:
面对这样一个需求,你会如何实现 Task 中的 compareTo(Task o)
方法呢?
小试牛刀
根据思维导图,你可能会对着写出这样的代码:
@Override
public int compareTo(Task o) {
// 先比较是否在专属队列中和排序时间
if (inPrivateQueue && !o.inPrivateQueue) {
return -1;
} else if (!inPrivateQueue && o.inPrivateQueue) {
return 1;
} else if (inPrivateQueue && o.inPrivateQueue && addTime != o.addTime) {
return Long.compare(addTime, o.addTime);
}
// 再比较是否在 VIP 队列中和优先级
if (inVipQueue && !o.inVipQueue) {
return -1;
} else if (!inVipQueue && o.inVipQueue) {
return 1;
} else if (inVipQueue && o.inVipQueue) {
if (priority != o.priority) {
return Integer.compare(o.priority, priority);
} else if (addTime != o.addTime) {
return Long.compare(addTime, o.addTime);
}
}
// 最后比较普通队列的优先级和排序时间
if (priority != o.priority) {
return Integer.compare(o.priority, priority);
} else {
return Long.compare(addTime, o.addTime);
}
}
但是,这样的代码逻辑,判断条件的分支过多,理解起来很不方便。如果对重复逻辑提取公共函数,那么代码可能会变成这样:
@Override
public int compareTo(Task o) {
if (inPrivateQueue != o.inPrivateQueue) {
return inPrivateQueue ? -1 : 1;
}
if (inVipQueue != o.inVipQueue) {
return inVipQueue ? -1 : 1;
}
if (inVipQueue && o.inVipQueue && priority != o.priority) {
return Integer.compare(o.priority, priority);
}
return compareAddTime(o);
}
private int compareAddTime(Task o) {
if (addTime == o.addTime) {
return 0;
} else {
return addTime < o.addTime ? -1 : 1;
}
}
这么写了之后,可读性相比第一种提高了很多,但是,仍然会有这样的问题:
- 代码逻辑依然存在不太容易理解的地方。比如,第三个if里面,判断两个任务之间均在vip队列,且优先级不一样时,就比较优先级,这个逻辑和上述思维导图的用例分支并不是一一对应的,看到代码之后,还要稍稍思考一下才能想明白是如何和思维导图中的用例分支对应上,并不十分直观。
- 可扩展性不佳。如果想要在这段逻辑中,增加任务优先级的逻辑,要求同一队列中,优先级较高的任务优先处理。那在上述逻辑里就并不那么容易加上去了,需要好好梳理这里的逻辑才能加上去。
那有没有更易于理解的方案呢?
算法之石,可以攻玉
二叉树路径的序列化
经常刷算法题的同学应该知道, 在leetcode的652题里,运用到了一个算法技巧,叫做二叉树的序列化,意思是可以将一棵二叉树的每条路径串起来,转换成一个字符串,以此来表示一条路径,即将二叉树的路径 ”序列化“ 成了字符串。这么做的好处在于可以通过比较字符串,从而比较方便地比较一个复杂的数据结构。
那这个技巧和我们现在遇到的任务排序的问题有什么关系呢?
多字段的序列化
从二叉树序列化的技巧中我们可以学到,当需要对一个复杂的数据结构判断重复时,可以将这个复杂数据结构序列化成易于比较的数据结构。同理,现在Task类存在多个字段需要比较,Task也是一个复杂的对象了,能否也将Task中的多个字段序列化成一个易于比较的数据结构呢?也就是说,如果我们能将这几个用于比较的字段,形成 类似于 {inPrivateQueue}_{inVipQueue}_{priority}_{addTime}
这样的序列,然后逐个比较,当大小相等时就比较下一个元素,当大小不一致时就返回比较结果,这样是不是整个逻辑会简化很多呢?
另外,考虑到这多个字段有不同的数据类型,将它们单纯地拼接在一起形成字符串的话,最终还是要切割开来才能比较。所以这里就不选用字符串作为最终序列化的数据结构,而是选用一个长整型列表,使用列表作为序列化后的数据结构,在比较时不用进行切割,而且列表元素是长整型,可以同时满足Task中 布尔型、整型和长整型的数据类型。
因此,对于这种方案,可以抽象出一个 SerialOrder
类,并实现一个比较函数,其功能满足对元素逐个比较,当大小相等时就比较下一个元素,当大小不一致时就返回比较结果的逻辑即可。代码如下:
@ToString
@Getter
public class SerialOrder {
/**
* 需要序列化的顺序数
*/
private final int nOrders;
private final List<Long> orders;
public SerialOrder(int nOrders) {
this.nOrders = nOrders;
this.orders = new ArrayList<>(nOrders);
}
public void addOrder(long order) {
orders.add(order);
}
/**
* 逐个比较,当大小相等时就比较下一个元素,当大小不一致时就返回比较结果
* @param o 其他的SerialOrders
* @return 比较结果
*/
public int compareTo(SerialOrder o) {
for (int i = 0; i < nOrders; i++) {
Long order1 = orders.get(i);
Long order2 = o.getOrders().get(i);
if (order1.equals(order2)) {
continue;
}
return Long.compare(order1, order2);
}
return 0;
}
}
一个对象的多个字段最终就会序列化成 SerialOrder
对象。使用时,将复杂对象的多个字符分别转换成 Long 类型,然后填入SerialOrder
对象即可,其他的比较操作,完全交给 SerialOrder
类处理。对应的代码如下:
private SerialOrder getSerialOrder() {
SerialOrder serialOrder = new SerialOrder(4);
serialOrder.addOrder(inPrivateQueue ? 1 : 0);
serialOrder.addOrder(inVipQueue ? 1 : 0));
serialOrder.addOrder(priority);
serialOrder.addOrder(addTime);
return serialOrder;
}
public int compareTo(Task o) {
return getSerialOrder().compareTo(o.getSerialOrder());
}
这样处理之后,使用逻辑上就非常清晰了,SerialOrder
中的每个元素,都遵循对元素逐个比较,当大小相等时就比较下一个元素,当大小不一致时就返回比较结果的逻辑。在使用时,只需要对着用例,将每个元素对应的比较字段按顺序填好,就可以了。
如果想要在这个排序规则中加上任务优先级这样的判断条件呢?那么只需要改动少量代码即可满足,其他代码都不需要改动,而且逻辑非常清晰:
private SerialOrder getSerialOrder() {
SerialOrder serialOrder = new SerialOrder(5); // 元素数量从4改成5
serialOrder.addOrder(inPrivateQueue ? 1 : 0);
serialOrder.addOrder(inVipQueue ? 1 : 0));
serialOrder.addOrder(priority);
serialOrder.addOrder(taskPriority); // 新增这一行
serialOrder.addOrder(addTime);
return serialOrder;
}
public int compareTo(Task o) {
return getSerialOrder().compareTo(o.getSerialOrder());
}
相比最开始的两个版本,这样的代码是否更加清晰易读,且易于扩展呢?
总结
对于这个问题或许有更优的写法,但重点在于,我们在平时刷的题,积累的技巧,其实并不是一无是处,有时会以不一样的方式应用到我们实际的业务中。只是在负责这块功能的时候,我们能否用之前总结和积累过的内容,通过联想、类比等方式,对现有的业务代码或问题更深入地思考。
转载自:https://juejin.cn/post/7233808084086636581