Another way-Using algorithmic techniques to optimize the problem of comparing the size of complex objects

problem background

Consider such a problem: there are a batch of queues, and there are a batch of tasks in this batch of queues. The fields in the tasks are defined as follows:


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)The function that needs to implement the task according to the following rules :

  1. The comparison result conforms to the definition of the comparison function in Java: if task1 < task2, then return -1; if task1 == task2, then return 0; if task1 > task2, then return 1.

  2. The comparison rules are as follows:

    • If there are tasks in the exclusive queue, select the task with the longest queue time from the exclusive queue

    • If there is no task in the exclusive queue and there is a task in the VIP queue, select the task in the VIP queue

      • Sort the tasks in the VIP queue according to the queue priority, and select the queue with the highest priority
      • If there is only one queue with the highest priority, select the task with the longest queue time from this queue
      • If there are multiple queues with the highest priority, select the task with the longest queue time from the multiple queues
    • If there is no task in the exclusive queue and no task in the VIP queue, select the task in the non-VIP queue

      • Sort the tasks in the non-VIP queue according to the queue priority, and select the queue with the highest priority
      • If there is only one queue with the highest priority, select the task with the longest queue time from this queue
      • If there are multiple queues with the highest priority, select the task with the longest queue time from the multiple queues

Express the comparison rules with a mind map as follows:

c28bcd49018d4e79b20b31fd80a3f176.jpg

Faced with such a requirement, how would you implement compareTo(Task o)the method ?

Small scale chopper

According to the mind map, you may write code like this:


@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);
    }
}

However, such code logic has too many branches to judge the condition, which is very inconvenient to understand. If common functions were extracted for the repeated logic, the code might look like this:


@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;
    }
}


After writing in this way, the readability is much improved compared to the first one, but there are still such problems:

  1. There are still places where the code logic is not easy to understand. For example, in the third if, when it is judged that the two tasks are in the VIP queue and the priorities are different, the priorities are compared. This logic is not in one-to-one correspondence with the use case branches of the above mind map. See After the code, you have to think a little bit to understand how it corresponds to the use case branch in the mind map, which is not very intuitive.
  2. Does not scale well. If you want to increase the logic of task priority in this logic, it is required that tasks with higher priority in the same queue be processed first. It is not so easy to add it to the above logic, and it needs to sort out the logic here before adding it.

Is there a more understandable solution?

Algorithm stone, you can tap jade

Serialization of binary tree paths

经常刷算法题的同学应该知道, 在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());
}

相比最开始的两个版本,这样的代码是否更加清晰易读,且易于扩展呢?

总结

对于这个问题或许有更优的写法,但重点在于,我们在平时刷的题,积累的技巧,其实并不是一无是处,有时会以不一样的方式应用到我们实际的业务中。只是在负责这块功能的时候,我们能否用之前总结和积累过的内容,通过联想、类比等方式,对现有的业务代码或问题更深入地思考。

Guess you like

Origin juejin.im/post/7233808084086636581
Recommended