最近在跟着《代码随想录》复习算法,顺着做题习惯复习+整理一下(我很菜,但是ipad上画图了很多笔记,终于趁着准备秋招,把这些整理成博客吧)
审题
(当然首先审题,把题目整理成一些能看懂的条件吧,这是我做题的习惯)
1.输入类型:可以简单理解为数组(或者二维数组),数组元素是成对字符串,每个字符串表示一个机场名称。
2.输出类型:也是一个数组,数组元素是字符串(机场名称),数组的顺序表示一条完整的路径
3.执行逻辑:可以通过题目给出的示例看出,输出的条件要满足:是一条完整的不重合的路径(欧拉路径),题目说的字典排序是路径的顺序。
有了审题的信息,是不是不难看出:是让我们根据字典排序做一个搜索呢?在代码中怎么处理这些字符串?用什么方法去搜索呢?
代码逻辑
根据整理的信息,我个人的第一反应是根据字典排序后,做一个图上的深度优先遍历(当然你可能有你的道理),在不考虑复杂度的情况下,这样的思考是非常直观的。尤其在第二个示例上
从JFK出发,根据字典排序,我们可以在tickets数组中,找到下一条边连接着ATL;
到达ATL后,判断可以利用的边(起点为ATL的边)根据字典排序,下一条边连接着JFK;
。。。是不是很像回溯!!!垃圾递归毁我内存....不过代码逻辑是真的直接干脆
开始写代码:
这是我写递归(回溯法)的基本代码框架(也就是说我什么都没做,定义一个递归函数呗。。。)
class Solution {
public List<String> findItinerary(List<List<String>> tickets) {
}
boolean backtracking(ArrayList<List<String>> tickets){
}
}
主函数内(跟main没关系啊!我好担心自己表达不清楚)
在类内,定义返回的数组res,以及收纳递归过程的数组path
private LinkedList<String> res;
private LinkedList<String> path = new LinkedList<>();
对于原始的tickets数组,我们希望能够得到一个以终点字典排序的顺序,这样就可以在遍历时直接顺序遍历就行。所以在主函数内,把输入的数组tickets根据tickets[i][1]排个序,这个地方是值得注意的。(参照代码随想录调了下库,如果不服自行手撸)
//以终点(tickets数组元素的第二位数据)排序
Collections.sort(tickets,(a,b)->a.get(1).compareTo(b.get(1)));
另外将题目规定的JFK作为唯一起点,也就是在递归路径path中把"JFK"固定为第一个元素
//JFK固定为起点
path.add("JFK");
在主函数内,还需要做最后一件事:“一张票只需要用一次”,也就是说,包含同样起点终点的一条边,只需要被递归函数考虑一次。我们有很多方法去完成这件事,比如利用集合,hash,当然最简单直观省事儿不费内存的方法就是,定义一个bool数组,来标记这条边是否被纳入过
//标记这张票是否已经用过
boolean []used = new boolean[tickets.size()];
主函数的任务已经基本完成,只剩下调用递归函数,然后直接返回结果数组res,res是类内的私有变量,可以被类内函数直接修改,所以接下来就来分析递归函数要干嘛了。
递归函数(单一操作,控制返回)
递归函数只考虑单层逻辑的话,它需要服务的事情就是:把当前检索的边里的节点正确纳入路径数组path。一次考虑一条边,也就是一次只会纳入一个节点,这个逻辑很清晰吧!所以其实我要讲的是递归的出口!那么总共需要多少个节点呢?
这部分请见小学数学之线段端点问题~
//路径读取的条件:路径长度等于机票数量+1
if(path.size()==tickets.size()+1){
//path中纳入了全部节点,就传给结果数组res
res = new LinkedList(path);
//结束这场内存的噩梦吧
return true;
}
单层逻辑
注意分析:为什么递归函数返回值为boolean?
因为这样可以帮助我们确定正确顺序的出口。如果没有找到,则进行回溯。
//思考单层逻辑
for(int i = 0;i < tickets.size();i++){
//纳入条件:经排序后,当前路径path的最后一个元素,和机票中的起点相同
if(!used[i]&&tickets.get(i).get(0).equals(path.getLast())){
//将终点纳入
path.add(tickets.get(i).get(1));
//标记该张机票已经被处理过
used[i] = true;
//如果能够正确返回!!!!!这里很重要
if(backtracking(tickets,used)){
//通过return true的方式跳出递归过程
return true;
}
//如果没有正确返回,则进行回溯
used[i] = false;
path.removeLast();
}
}
全部代码
class Solution {
private LinkedList<String> res;
private LinkedList<String> path = new LinkedList<>();
public List<String> findItinerary(List<List<String>> tickets) {
//以终点(tickets数组元素的第二位数据)排序
Collections.sort(tickets,(a,b)->a.get(1).compareTo(b.get(1)));
//JFK固定为起点
path.add("JFK");
//标记这张票是否已经用过
boolean []used = new boolean[tickets.size()];
backtracking((ArrayList)tickets,used);
return res;
}
boolean backtracking(ArrayList<List<String>> tickets,boolean[] used){
//路径读取的条件:路径长度等于机票数量-1
if(path.size()==tickets.size()+1){
res = new LinkedList(path);
return true;
}
//思考单层逻辑
for(int i = 0;i < tickets.size();i++){
//纳入条件:经排序后,当前路径path的最后一个元素,和机票中的起点相同
if(!used[i]&&tickets.get(i).get(0).equals(path.getLast())){
//将终点纳入
path.add(tickets.get(i).get(1));
//标记该张机票已经被处理过
used[i] = true;
//如果能够正确返回!!!!!这里很重要
if(backtracking(tickets,used)){
//通过return true的方式跳出递归过程
return true;
}
//如果没有正确返回,则进行回溯
used[i] = false;
path.removeLast();
}
}
//循环中给不出结果的,当然默认为false
return false;
}
}
总结
类似这样的题目有很多,在准备用递归解决问题时,一定要注意几个点:
1.跳出出递归的条件(这点看似简单,其实很值得深入)
比如本题中,递归出口被默认只有一个,并且当没有找到正确路径时,才进行回溯
这也是为什么要把递归函数返回类型规定为boolean的原因。
2.这不同于组合、组合加、子集等需要纳入所有路径的题目(注意思考区别)
3.这种方法只是在复习回溯法时用到,时间复杂度太高了