背景概述
前段时间在做机票的查询航班业务时,发现这样一种业务场景:
会员查询航班时选择的行程为:
- 5月18日,武汉飞北京
- 5月18日,北京飞大连
- 5月18日,大连飞武汉
调用航信接口获得的航班列表如下:
业务上为了保证客人两趟行程之间能够及时乘机,规定两趟航班之间的间隔时间不能小于一小时,所以用户可选择的情况共四种:
从上面可以看出:
- 北京到大连行程中的航班CA123由于无法与大连到武汉行程的任一航班共同乘坐,导致该航班为无效航班
- 武汉到北京行程中的航班CA113本可与CA123共同乘坐,但由于CA123为无效航班,导致该航班也为无效航班
所以在多段航班查询的结果中,需要经过一些逻辑处理将无效航班从结果中去除。
并且假如会员第一段航程选择CA111,则第二段航程可选CA121和CA122,如果选择CA112,则第二段航程只可选CA122,所以在不同情况下给会员展示的后续航班列表也可能会发生变化。
问题分析
经过仔细的思考后,可以将该业务场景抽象化为以下问题:
假设存在N个连续相关问题:Q1、Q2、Q3、……、Qn,每个问题都存在个不同个数的已知解:
A11、A12、A13、……、A1x
A21、A22、A23、……、A2y
……
An1、An2、An3、……、Anz
且相邻问题的解之间需要满足特定的条件才可以共存,求N个问题的解集合S
该问题的难点在于:
- 如何找出并删除无效解
- 如何处理不同解之间的关联关系
解决思路
- 创建数据结构类有向图(DirectionGraph),图中存在若干个节点,不同的节点之间存在指向关系,称之为边
- 将每一个解构造成一个节点,将前后相关问题的解创建边
- 删除图不满足条件的指向边
- 删除图中不能构造成有效解的节点
- 将图转换成解集合
代码展示
先构造节点类如下:
import java.util.ArrayList;
import java.util.List;
/**
* 节点类
* @param <T>
*/
public class Node<T> {
/** 所有前驱节点 */
private List<Node<T>> prevList = new ArrayList<>();
/** 存储的数据 */
private T data;
/** 所有后继节点 */
private List<Node<T>> nextList = new ArrayList<>();
public Node(T data) {
this.data = data;
}
/**
* 获取数据
* @return
*/
public T getData() {
return data;
}
/**
* 增加前驱节点
* @param prevNode
*/
public void addPrevNode(Node<T> prevNode) {
if (!prevList.contains(prevNode)) {
prevList.add(prevNode);
}
}
/**
* 删除前驱节点
* @param prevNode
*/
public void deletePrevNode(Node<T> prevNode) {
prevList.remove(prevNode);
}
/**
* 增加后继节点
* @param nextNode
*/
public void addNextNode(Node<T> nextNode) {
if (!nextList.contains(nextNode)) {
nextList.add(nextNode);
}
}
/**
* 删除后继节点
* @param nextNode
*/
public void deleteNextNode(Node<T> nextNode) {
nextList.remove(nextNode);
}
/**
* 是否存在前驱节点
* @return
*/
public boolean hasPrev() {
return prevList != null && !prevList.isEmpty();
}
/**
* 是否存在后继节点
* @return
*/
public boolean hasNext() {
return nextList != null && !nextList.isEmpty();
}
/**
* 获取所有前驱节点
* @return
*/
public List<Node<T>> getPrevList() {
return prevList;
}
/**
* 获取所有后继节点
* @return
*/
public List<Node<T>> getNextList() {
return nextList;
}
}
再构造有向图类:
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 有向图类
* @param <T>
*/
public class DirectionGraph<T> {
/** 所有数据节点 */
private List<Node<T>> nodes = new ArrayList<>();
/**
* 深度优先搜索获取遍历集合
* @return
*/
public List<List<T>> dfs() {
List<List<T>> results = new ArrayList<>();
for (Node<T> node : nodes) {
// 只从初始节点开始遍历
if (!node.hasPrev()) {
for (Node<T> next : node.getNextList()) {
List<T> result = new ArrayList<>();
result.add(node.getData());
dfsNode(next, result, results);
}
}
}
return results;
}
/**
* 深搜的递归函数
* @param cur
* @param curResult
* @param results
*/
private void dfsNode(Node<T> cur, List<T> curResult, List<List<T>> results) {
curResult.add(cur.getData());
if (!cur.hasNext()) {
List<T> result = new ArrayList<>();
result.addAll(curResult);
results.add(result);
} else {
for (Node<T> next : cur.getNextList()) {
dfsNode(next, curResult, results);
}
}
curResult.remove(cur.getData());
}
/**
* 获取所有节点
* @return
*/
public List<T> getAll() {
List<T> all = new ArrayList<>();
for (Node<T> node : nodes) {
all.add(node.getData());
}
return all;
}
/**
* 添加节点
* @param data
*/
public void add(T data) {
if (!contains(data)) {
nodes.add(new Node<T>(data));
}
}
/**
* 删除节点
* @param data
*/
public void delete(T data) {
Node<T> node = getNode(data);
if (node == null) {
return;
}
Iterator prevListItr = node.getPrevList().iterator();
while (prevListItr.hasNext()) {
Node<T> prevNode = (Node<T>) prevListItr.next();
prevNode.deleteNextNode(node);
prevListItr.remove();
}
Iterator nextListItr = node.getNextList().iterator();
while (nextListItr.hasNext()) {
Node<T> nextNode = (Node<T>) nextListItr.next();
nextNode.deletePrevNode(node);
nextListItr.remove();
}
nodes.remove(node);
}
/**
* 获取节点
* @param data
* @return
*/
private Node<T> getNode(T data) {
for (Node node : nodes) {
if (node.getData().equals(data)) {
return node;
}
}
return null;
}
/**
* 是否存在节点
* @param data
* @return
*/
public boolean contains(T data) {
return getNode(data) != null;
}
/**
* 增加指向关系
* @param prev
* @param next
*/
public void addEdge(T prev, T next) {
Node<T> prevNode = getNode(prev);
if (prevNode == null) {
add(prev);
prevNode = getNode(prev);
}
Node<T> nextNode = getNode(next);
if (nextNode == null) {
add(next);
nextNode = getNode(next);
}
if (prevNode != null && nextNode != null) {
prevNode.addNextNode(nextNode);
nextNode.addPrevNode(prevNode);
}
}
/**
* 删除指向关系
* @param prev
* @param next
*/
public void deleteEdge(T prev, T next) {
Node<T> prevNode = getNode(prev);
if (prevNode == null) {
return;
}
Node<T> nextNode = getNode(next);
if (nextNode == null) {
return;
}
prevNode.deleteNextNode(nextNode);
nextNode.deletePrevNode(prevNode);
}
/**
* 是否存在指向关系
* @param prev
* @param next
* @return
*/
public boolean hasEdge(T prev, T next) {
return getNextList(prev).contains(next);
}
/**
* 获取所有的后继节点
* @param data
* @return
*/
public List<T> getNextList(T data) {
Node<T> node = getNode(data);
List<T> nextList = new ArrayList<>();
if (node != null ) {
for (Node<T> nextNode : node.getNextList()) {
nextList.add(nextNode.getData());
}
}
return nextList;
}
/**
* 获取所有的前驱节点
* @param data
* @return
*/
public List<T> getPrevList(T data) {
Node<T> node = getNode(data);
List<T> prevList = new ArrayList<>();
if (node != null ) {
for (Node<T> prevNode : node.getPrevList()) {
prevList.add(prevNode.getData());
}
}
return prevList;
}
}
最后创建测试类:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
public class SearchFlightTest {
public static void main(String[] args) throws ParseException {
// 初始化节点数据,并记录初始节点和结束节点
DirectionGraph<Flight> graph = initialGraph();
List<Flight> starts = new ArrayList<>();
List<Flight> ends = new ArrayList<>();
for (Flight prev : graph.getAll()) {
if (graph.getPrevList(prev).isEmpty()) {
starts.add(prev);
}
if (graph.getNextList(prev).isEmpty()) {
ends.add(prev);
}
}
// 删除无效的边
deleteInvalidEdge(graph);
// 删除无效节点
deleteInvalidNode(graph, starts, ends);
// 深度优先搜索图,获取解集合
List<List<Flight>> results = graph.dfs();
// 输出解集合
for (List<Flight> result : results) {
StringBuilder sb = new StringBuilder();
for (Flight flight : result) {
sb.append(flight.getFlightNo() + " " + flight.getStart() + "起飞 " + flight.getEnd() + "降落 -> ");
}
System.out.println(sb.substring(0, sb.length() - 4));
}
}
/**
* 初始化有向图数据
* @return
*/
private static DirectionGraph<Flight> initialGraph() {
// 初始化航班数据
List<Flight> whu2pek = new ArrayList<>();
whu2pek.add(new Flight("CA111", "2018-05-20 06:00", "2018-05-20 08:10"));
whu2pek.add(new Flight("CA112", "2018-05-20 09:30", "2018-05-20 11:40"));
whu2pek.add(new Flight("CA113", "2018-05-20 15:40", "2018-05-20 17:50"));
List<Flight> pek2dlc = new ArrayList<>();
pek2dlc.add(new Flight("CA121", "2018-05-20 10:20", "2018-05-20 11:50"));
pek2dlc.add(new Flight("CA122", "2018-05-20 15:40", "2018-05-20 17:00"));
pek2dlc.add(new Flight("CA123", "2018-05-20 19:30", "2018-05-20 21:05"));
List<Flight> dlc2whu = new ArrayList<>();
dlc2whu.add(new Flight("CA131", "2018-05-20 16:50", "2018-05-20 19:10"));
dlc2whu.add(new Flight("CA132", "2018-05-20 21:00", "2018-05-20 23:20"));
DirectionGraph<Flight> graph = new DirectionGraph<>();
// 添加节点
for (Flight flight : whu2pek) {
graph.add(flight);
}
for (Flight flight : pek2dlc) {
graph.add(flight);
}
for (Flight flight : dlc2whu) {
graph.add(flight);
}
// 添加边,默认前后航班均可连接
for (Flight prev : whu2pek) {
for (Flight next : pek2dlc) {
graph.addEdge(prev, next);
}
}
for (Flight prev : pek2dlc) {
for (Flight next : dlc2whu) {
graph.addEdge(prev, next);
}
}
return graph;
}
/**
* 删除无效的边
* @param graph
* @throws ParseException
*/
private static void deleteInvalidEdge(DirectionGraph<Flight> graph) throws ParseException {
List<List<Flight>> deletedEdges = new ArrayList<>(); // 即将被删除的边
for (Flight prev : graph.getAll()) {
// 遍历每个父子关系,将不满足条件的进行记录后续删除
for (Flight next : graph.getNextList(prev)) {
if (!canMatch(prev, next)) {
List<Flight> deletedEdge = new ArrayList<>();
deletedEdge.add(prev);
deletedEdge.add(next);
deletedEdges.add(deletedEdge);
}
}
}
// 删除不满足条件的边
for (List<Flight> edge : deletedEdges) {
graph.deleteEdge(edge.get(0), edge.get(1));
}
}
/**
* 判断两个节点是否能满足条件
* @param prev
* @param next
* @return
* @throws ParseException
*/
private static boolean canMatch(Flight prev, Flight next) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
Date prevEnd = sdf.parse(prev.getEnd());
Date nextStart = sdf.parse(next.getStart());
return nextStart.getTime() - prevEnd.getTime() >= 60 * 60 * 1000;
}
/**
* 删除图中无法构成有效解的节点
* @param graph
* @param starts
* @param ends
*/
private static void deleteInvalidNode(DirectionGraph<Flight> graph, List<Flight> starts, List<Flight> ends) {
List<Flight> deletedFlights = new ArrayList<>(); // 需要被删除的节点list
for (;;) {
// 重新补充list
for (Flight flight : graph.getAll()) {
List<Flight> prevList = graph.getPrevList(flight);
List<Flight> nextList = graph.getNextList(flight);
if (starts.contains(flight) && !ends.contains(flight)
&& nextList.isEmpty()) {
deletedFlights.add(flight); // 初始节点如果没有子节点就需要被删除
}
if (!starts.contains(flight) && ends.contains(flight)
&& prevList.isEmpty()) {
deletedFlights.add(flight); // 结束节点如果没有父节点就需要被删除
}
if (!starts.contains(flight) && !ends.contains(flight)
&& (prevList.isEmpty() || nextList.isEmpty())) {
deletedFlights.add(flight); // 中间节点如果没有父节点或者子节点就需要被删除
}
}
// 没有需要被删除的节点才终止循环
if (deletedFlights.isEmpty()) {
break;
}
// 删除节点,删除后清空list
for (Flight flight : deletedFlights) {
graph.delete(flight);
}
deletedFlights.clear();
}
}
}
测试结果如下:
总结思考
目前上述代码还存在一些可以思考的点,比如这个测试方法没有考虑单段查询的情况,有向图和节点类无法支持多线程,无法处理环状节点问题,初始节点、结束节点、还有抽象后的问题(Qn)都并没有很好的构建在数据结构中,是否可以将图增加层级的概念,以及是否可以采用单向而非双向的节点关联关系解决问题等等,希望大家能够一起多深入思考,以便理解和学习。