一、题目简介
我们用“手”来表示一个距离单位,也就是 4 英寸,在英语国家常用来测量马的高度。“光年”是另一个距离单位,也就是一个粒子在一定秒数内移动的距离,大约等于一个地球年。从表面上看,这两个单位除了用来测量距离之外,几乎没有什么关系,但事实证明,谷歌可以随意在它们之间进行转换。它们毕竟都是用来测量距离的,所以互相转换也是很自然的事情。但如果你细想就会觉得有点奇怪:它们之间的转换比率是如何算出来的?肯定没有人算过一光年等于多少手吧?
二、思路分析
题目可以转换成另一个表达方式,有一个表格,每一行记录了从1unit1=n*unit2
实际在转换单位的时候,比如unita->unitb
第一步,去表格里查找unita在哪一行存在,找到指定行之后,然后再判断unitb是否在这一行,知道找到一条可达的路径,实现最终的单位转换。
三、程序实现
3.1 定义一个单位换算关系
public class UnitNode {
private String fromUnit;
private String toUnit;
private Float rate;
...... getXX() setXX()
@Override
public String toString() {
return "["+fromUnit+"|"+toUnit+"|"+rate+"]";
}
@Override
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(!(obj instanceof UnitNode)){
return false;
}
UnitNode node1=(UnitNode) obj;
return this.getFromUnit().equals(node1.getFromUnit()) && this.getToUnit().equals(node1.getToUnit());
}
}
3.2 录入一批测试数据
private static List<UnitNode> units= Lists.newArrayList();
static {
units.add(new UnitNode("m","cm",100F));
units.add(new UnitNode("hand","cm",15F));
units.add(new UnitNode("mobile","hand",0.75F));
units.add(new UnitNode("inch","hand",0.25F));
}
3.3 图形展示
3.4 如何快速找到某一行有我们需要的单位呢
我们常用的数据结构有Map List Set等。能实现快速定位的可以使用map的key。
因此将上面的list转Map<String,List>
实现效果如下:
{
m:[m->cm],
cm:[cm->m,cm-hand],
hand:[hand->cm,hand->inch,hand->mobile],
inch:[inch->hand],
mobile:[mobile->hand]
}
map里的单位转换的方向和图片示意中的方向有些事相反的,实际在转换的时候,这里要做处理,参考代码如下:
public Map<String, List<UnitNode>> toMap() {
Map<String, List<UnitNode>> maps= Maps.newHashMap();
for(UnitNode unit:units){
String fromNode=unit.getFromUnit();
String toNode=unit.getToUnit();
if(!maps.containsKey(fromNode)){
maps.put(fromNode,Lists.<UnitNode>newArrayList());
}
maps.get(fromNode).add(unit);
if(!maps.containsKey(toNode)){
maps.put(toNode,Lists.<UnitNode>newArrayList());
}
maps.get(toNode).add(new UnitNode(unit.getToUnit(),unit.getFromUnit(),1F/unit.getRate()));//调换单位转换方向
}
return maps;
}
3.5转换链
从fromUnit开始,我们可以在这个map里找到多条长短不一的链,每条链都有一个toUnit。
例如,formUnit=‘m’,
那么生成的链应该如下
[
{m->cm}
{m->cm,cm->m}
{m->cm,cm->hand}
{m-cm,cm->hand,hand->cm}
{m-cm,cm->hand,hand->inch}
.........
]
实现方式因人而异,以下是我的实现方式
public void link(String unit,List<LinkedList<UnitNode>> links,String originUnit,Map<String, List<UnitNode>> maps){
if(maps.isEmpty()){
return;
}
List<UnitNode> nodes=maps.get(unit);
if(nodes!=null){
maps.remove(unit);
for(int i=0;i<nodes.size();++i){
UnitNode node1=nodes.get(i);
if(links.isEmpty()){
links.add(Lists.newLinkedList(nodes));
}else{
for( int j=0;j<links.size();j++){
LinkedList<UnitNode> linkNode=links.get(j);
if(!linkNode.contains(node1) && linkNode.get(linkNode.size()-1).getToUnit().equals(node1.getFromUnit())){
LinkedList<UnitNode> nodes1=Lists.newLinkedList(linkNode);
nodes1.add(node1);
links.add(nodes1);
j++;
}
}
}
if(!node1.getToUnit().equals(originUnit)){
link(node1.getToUnit(),links,originUnit,maps);
}
}
}
}
3.6 单位转换
public Float getRate(String fromUnit,String toUnit){
if(fromUnit.equals(toUnit)){
return 1F;
}
Map<String, List<UnitNode>> maps=toMap();
if(!maps.containsKey(fromUnit) || !maps.containsKey(toUnit)){
return -1F;
}
List<LinkedHashSet<UnitNode>> links =Lists.newArrayList();
link(fromUnit,links,fromUnit,maps);
for(LinkedHashSet<UnitNode> node:links){
List<UnitNode> nodeList=Lists.newArrayList(node);
if(nodeList.get(0).getFromUnit().equals(fromUnit ) && nodeList.get(node.size()-1).getToUnit().equals(toUnit)){
//满足条件
Float r=1f;
for(UnitNode n:nodeList){
r*=n.getRate();
}
return r;
}
}
return -1f;
}
四、优化算法
在实现的过程中出现了Map<String,List>的结果和map->list,很容易想到我们之前学过的图搜索算法。
图有两种标准的表达形式:
- 邻接链表(适用于线条[边]少的稀疏图)
- 邻接矩阵(适用于无法快速判断边的稠密图)
4.1 图算法简介
-
广度优先搜索(BFS)
从源点开始,先遍历所有能达到的点,再遍历二级能达到的点。简单的图示为:
-
深度优先搜索(DFS)
从源点出发边开始遍历直到结点的所有出发边均被发现,才回溯。形象的理解为"不撞南墙不回头"。一条道走到尽头了才返回
4.2 构建图
每个单位当做一点节点,这个节点需要标识是否被访问
参考代码
public class UnitNode1 {
private String unit;
private Boolean isVisited=false;
public UnitNode1(){
}
public UnitNode1(String unit){
this.unit=unit;
this.isVisited=false;
}
............省略get set................
@Override
public String toString() {
return "["+getUnit()+"|"+getVisited()+"]";
}
@Override
public boolean equals(Object obj) {
if(this==obj){
return true;
}
if(!(obj instanceof UnitNode1)){
return false;
}
UnitNode1 node1=(UnitNode1) obj;
return this.getUnit().equals(node1.getUnit());
}
}
图由结点和边两部分构成,,边的话,可以由两个节点构成。这个题有一个转换率,相当于边长。
所以图定义如下
public class UnitGraph {
private static transient Logger log = LoggerFactory.getLogger(UnitGraph.class);
private LinkedHashMap<String,UnitNode1> unitMaps= Maps.newLinkedHashMap();//所有的单位(结点)
private LinkedHashMap<String, ArrayList<UnitNode1>> neiMap= Maps.newLinkedHashMap();//存放邻接结点
private Map<String,Float> rateMaps=Maps.newHashMap();//费率Map :key:(fromUnit:toUnit) value=rate 方便快速查找任意两个之间的单位换算
public LinkedHashMap<String, UnitNode1> getUnitMaps() {
return unitMaps;
}
public UnitNode1 getNode(String unit){
return this.getUnitMaps().get(unit);
}
public void setUnit(String unit){
if(!this.getUnitMaps().containsKey(unit)){
this.getUnitMaps().put(unit,new UnitNode1(unit));
}
}
public void setUnit(UnitNode1 unit){
if(!this.getUnitMaps().containsKey(unit.getUnit())){
this.getUnitMaps().put(unit.getUnit(),unit);
}
}
public ArrayList<UnitNode1> getNeiList(String node){
return this.getNeiMap().get(node);
}
public ArrayList<UnitNode1> getNeiList(UnitNode1 node){
return this.getNeiMap().get(node.getUnit());
}
public void setRate(String fromUnit,String toUnit,Float rate){
this.getRateMaps().put(fromUnit+":"+toUnit,rate);
}
public Float getRate(String fromUnit,String toUnit){
if(this.getRateMaps().containsKey(fromUnit+":"+toUnit)){
return this.getRateMaps().get(fromUnit+":"+toUnit);
}
if(this.getRateMaps().containsKey(toUnit+":"+fromUnit)){
return 1f/this.getRateMaps().get(toUnit+":"+fromUnit);
}
return -1f;
}
public void setNeiNode(String node,UnitNode1 neiNode){
ArrayList<UnitNode1> unitNode1s=this.getNeiMap().get(node);
if(unitNode1s==null){
unitNode1s=Lists.newArrayList();
this.getNeiMap().put(node,unitNode1s);
}
if(!unitNode1s.contains(neiNode)){
unitNode1s.add(neiNode);
}
}
public void setNeiNode(String node,String neiNode){
ArrayList<UnitNode1> unitNode1s=this.getNeiMap().get(node);
if(unitNode1s==null){
unitNode1s=Lists.newArrayList();
this.getNeiMap().put(node,unitNode1s);
}
if(!unitNode1s.contains(new UnitNode1(neiNode))){
unitNode1s.add(new UnitNode1(neiNode));
}
}
..............get set...................
public void print(){
log.info("节点");
log.info("{}",this.getUnitMaps());
log.info("邻接节点");
Set<String> neiKey=this.getNeiMap().keySet();
for(String key:neiKey){
log.info("{}:{}",key,this.getNeiMap().get(key));
}
log.info("rate");
log.info("{}",this.getRateMaps());
}
//将所有节点初始化为未访问
public void initialize(){
Set<String> unitKey=this.getUnitMaps().keySet();
for(String key:unitKey){
this.getUnitMaps().get(key).setVisited(false);
}
}
public boolean isNei(UnitNode1 fromNode,UnitNode1 toNode){
List<UnitNode1> neis=this.getNeiList(fromNode);
if(neis.contains(toNode)){
return true;
}
return false;
}
}
4.3将原来的单位换算关系转成新定义的UnitGraph List->UnitGraph
//转成图
private static UnitGraph graph = new UnitGraph();
static {
for (UnitNode node : units) {
graph.setUnit(node.getFromUnit());
graph.setUnit(node.getToUnit());
graph.setNeiNode(node.getFromUnit(), node.getToUnit());
graph.setNeiNode(node.getToUnit(), node.getFromUnit());
graph.setRate(node.getFromUnit(), node.getToUnit(), node.getRate());
}
}
转换为图的效果如下:
节点
{m=[m|false], cm=[cm|false], hand=[hand|false], mobile=[mobile|false], inch=[inch|false]}
邻接节点
m:[[cm|false]]
cm:[[m|false], [hand|false]]
hand:[[cm|false], [mobile|false], [inch|false]]
mobile:[[hand|false]]
inch:[[hand|false]]
rate
{m:cm=100.0, mobile:hand=0.75, inch:hand=0.25, hand:cm=10.0}
4.4 使用BFS实现单位换算
//广度优先算法
public Float getRateByBfs(String fromUnit, String toUnit) {
Float rate = graph.getRate(fromUnit, toUnit);
if (rate > 0F) {
return rate;
}
//将节点初始化未访问
graph.initialize();
ArrayList<UnitNode1> list = Lists.newArrayList();//最终访问的列表
Queue<UnitNode1> tmpList = Queues.newArrayDeque();//辅助Bfs队列
List<LinkedList<UnitNode1>> links = Lists.newArrayList();//保存搜索路径
tmpList.add(graph.getNode(fromUnit));
while (!tmpList.isEmpty()) {
UnitNode1 node = tmpList.poll();
if (!node.getVisited()) {
node.setVisited(Boolean.TRUE);
}
//保存访问节点
list.add(node);
//保存访问路径
saveLinkPath(node, links);
if (toUnit.equals(node.getUnit())) {
break;
} else {
List<UnitNode1> neiNodes = graph.getNeiList(node.getUnit());
for (UnitNode1 neiNode : neiNodes) {
UnitNode1 tmpNode = graph.getNode(neiNode.getUnit());
if (!tmpNode.getVisited() && !tmpList.contains(tmpNode)) {
tmpList.offer(tmpNode);
}
}
}
}
log.info("bfs path:{}", list);
for(int i=0;i<links.size();i++){
log.info("bfs->{}:{}", i,links.get(i));
}
if (!toUnit.equals(list.get(list.size() - 1).getUnit())) {
return -1F;
}
rate = 1F;
//最终可达路径
List<UnitNode1> node1List = links.get(links.size() - 1);
for (int i = 0; i < node1List.size() - 1; ++i) {
rate *= graph.getRate(node1List.get(i).getUnit(), node1List.get(i + 1).getUnit());
}
return rate;
}
4.5 DFS实现单位换算
//深度优先算法
public Float getRateByDfs(String fromUnit, String toUnit) {
Float rate = graph.getRate(fromUnit, toUnit);
if (rate > 0F) {
return rate;
}
//将节点初始化未访问
graph.initialize();
ArrayList<UnitNode1> list = Lists.newArrayList();//最终访问的列表
Stack<UnitNode1> tmpList = new Stack<>();//辅助Bfs队列
List<LinkedList<UnitNode1>> links = Lists.newArrayList();//保存搜索路径
tmpList.add(graph.getNode(fromUnit));
while (!tmpList.isEmpty()) {
UnitNode1 node = tmpList.pop();
if (!node.getVisited()) {
node.setVisited(Boolean.TRUE);
}
list.add(node);
saveLinkPath(node, links);
if (toUnit.equals(node.getUnit())) {
break;
} else {
List<UnitNode1> neiNodes = graph.getNeiList(node.getUnit());
for (UnitNode1 neiNode : neiNodes) {
UnitNode1 tmpNode = graph.getNode(neiNode.getUnit());
if (!tmpNode.getVisited() && !tmpList.contains(tmpNode)) {
tmpList.push(tmpNode);
}
}
}
}
log.info("dfs path:{}", list);
for(int i=0;i<links.size();i++){
log.info("dfs->{}:{}", i,links.get(i));
}
if (!toUnit.equals(list.get(list.size() - 1).getUnit())) {
return -1F;
}
rate = 1F;
//最终可达路径
List<UnitNode1> node1List = links.get(links.size() - 1);
for (int i = 0; i < node1List.size() - 1; ++i) {
rate *= graph.getRate(node1List.get(i).getUnit(), node1List.get(i + 1).getUnit());
}
return rate;
}
4.6 转换效果
mobile->inch 效果如下:
[2019-10-27 14:16:38,737] [main] () (UnitGraphTransfer.java:94) INFO - bfs path:[[mobile|true], [hand|true], [cm|true], [inch|true]]
[2019-10-27 14:16:38,738] [main] () (UnitGraphTransfer.java:96) INFO - bfs->0:[[mobile|true]]
[2019-10-27 14:16:38,738] [main] () (UnitGraphTransfer.java:96) INFO - bfs->1:[[mobile|true], [hand|true]]
[2019-10-27 14:16:38,739] [main] () (UnitGraphTransfer.java:96) INFO - bfs->2:[[mobile|true], [hand|true], [cm|true]]
[2019-10-27 14:16:38,742] [main] () (UnitGraphTransfer.java:96) INFO - bfs->3:[[mobile|true], [hand|true], [inch|true]]
[2019-10-27 14:16:38,743] [main] () (UnitGraphTransfer.java:49) INFO - bfs rate:3.0
[2019-10-27 14:16:38,743] [main] () (UnitGraphTransfer.java:152) INFO - dfs path:[[mobile|true], [hand|true], [inch|true]]
[2019-10-27 14:16:38,743] [main] () (UnitGraphTransfer.java:154) INFO - dfs->0:[[mobile|true]]
[2019-10-27 14:16:38,744] [main] () (UnitGraphTransfer.java:154) INFO - dfs->1:[[mobile|true], [hand|true]]
[2019-10-27 14:16:38,744] [main] () (UnitGraphTransfer.java:154) INFO - dfs->2:[[mobile|true], [hand|true], [inch|true]]
五 总结
5.1 BFS和DFS的区别
实现上在于一个使用了Queue,而一个使用了Stack,利用了先进先出,后进先出的特性。
5.2 适用场景
在本地题中需要用到浮点数计算,随着浮点计算次数的增加,误差扩散,错误累计。
DFS 是一种很好的搜索算法,如果存在解,它一定会把它找出来,但它缺少一个关键的属性:它不一定能找到最短路径。这跟我们很有关系,因为较短的路径意味着较少的跳数,较少的跳数意味着更少的浮点数乘法。为了解决这个问题,我们需要使用 BFS。
5.3 还可能的优化空间
如果存在多个单位需要转换,如果每个单位都去使用DFS,都去生成搜索路径,还是比较影响性能的,这题还可以引入缓存,将计算的中间结果都缓存下来空间复杂度和时间复杂度都会大大降低。