使用可变键从表中查找vals

时间:2016-12-17 01:46:33

标签: java algorithm

有一张桌子:

enter image description here

键由3个后缀组成: 区域+ S1 + S2

区域,如美国总是被指定,但是其他区域可以不指定,因此*将用于"所有"。

例如: for key =" US_A_U" value = 2,因为:

  1. 试图找到完全匹配:在表格中找到关键字(" US_A_U") - 不是 结果
  2. 1步不太严格查找:找到关键字(" US_A _ *") - 找到== 2
  3. for key =" US_Q_Q"值= 3,因为:

    1. 试图找到完全匹配:在表格中找到关键字(" US_Q_Q") - 不是 结果
    2. 1步不太严格查找:查找键(" US_Q _ *") - 未找到
    3. 找到密钥(" US _ * _ Q") - 未找到
    4. 1步不太严格查找:查找键(" US_*_*") - found = 3
    5. for key =" US_O_P"值= 3,因为:

      1. 试图找到完全匹配:在表格中找到键(" US_O_P") - 不 结果
      2. 1步不太严格查找:查找键(" US_O _ *") - 未找到
      3. 找到密钥(" US _ * _ P") - 未找到
      4. 1步不太严格查找:查找键(" US_*_*") - found = 3
      5. 所以要使用HashMap方法,我需要调用4次map.get()来查找一个值,这个值太多了,因为这段代码会经常运行。

        有没有更好或更快的解决方案?

        package test;
        
        import java.util.HashMap;
        
        public class MainCLass {
        
            public static void main(String[] args) {
                // init map (assuming this code will be run only once)
                HashMap<String, String> map = new HashMap<>();
                map.put("US_A_B", "1");
                map.put("US_A_*", "2");
                map.put("US_*_*", "3");
                map.put("US_O_O", "4");
                map.put("US_*_W", "5");
                map.put("ASIA_*_*", "6");
        
                // now often called logic
                // incoming params, for this example hardcoded
                String reg = "US";
                String s1 = "O";
                String s2 = "P";
                String val = null;
                val = map.get(reg+"_"+s1+"_"+s2);
                if (val == null){
                    val = map.get(reg+"_"+s1+"_*");
                    if (val == null){
                        val = map.get(reg+"_"+"*_"+s2);
                        if (val == null){
                            val = map.get(reg+"_*_*");
                        }
                    }
                }
                System.out.println(val);
            }
        }
        

        upd:我需要补充的是,总有3个传入参数(区域,s1,s2)。这个参数中的每一个永远不会等于"*"并且永远不会为空,因此完整的密钥总是像US_J_K(而不是US_*_K等。)

        所以通过这3个参数我需要从init表中找到正确的值。

5 个答案:

答案 0 :(得分:3)

您可以尝试创建一层地图,例如

public static void main(String[] args) {
    RegionMap map = new RegionMap();
    String region = "US";
    String s1 = "O";
    String s2 = "P";
    String val = map.search(region, s1, s2);
    System.out.println(val);
}

public class RegionMap {
    private Map<String, Map<String, Map<String, String>>> regionMap;

    public RegionMap() {
        init();
    }

    public String search(String region, String s1, String s2) {
        String val = searchS1(regionMap.get(region), s1, s2);
        if (val == null) {
            val = searchS1(regionMap.get("*"), s1, s2);
        }
        return val;
    }

    private String searchS1(Map<String, Map<String, String>> s1Map, String s1, String s2) {
        if (s1Map == null) {
            return null;
        }
        String val = searchS2(s1Map.get(s1), s2);
        if (val == null) {
            val = searchS2(s1Map.get("*"), s2);
        }
        return val;
    }

    private String searchS2(Map<String, String> s2Map, String s2) {
        if (s2Map == null) {
            return null;
        }
        String val = s2Map.get(s2);
        if (val == null) {
            val = s2Map.get("*");
        }
        return val;
    }

    private void init() {
        regionMap = new HashMap<>();
        addEntry("US", "A", "B", "1");
        addEntry("US", "A", "*", "2");
        addEntry("US", "*", "*", "3");
        addEntry("US", "O", "O", "4");
        addEntry("US", "*", "W", "5");
        addEntry("ASIA", "*", "*", "6");
    }

    private void addEntry(String region, String s1, String s2, String value) {
        Map<String, Map<String, String>> s1Map = regionMap.get(region);
        if (s1Map == null) {
            s1Map = new HashMap<>();
            regionMap.put(region, s1Map);
        }

        Map<String, String> s2Map = s1Map.get(s1);
        if (s2Map == null) {
            s2Map = new HashMap<>();
            s1Map.put(s1, s2Map);
        }

        s2Map.put(s2, value);
    }
}

在此地图中,第一个键是区域,第二个键是s1,第三个键是s2。这样就可以轻松地独立搜索region,s1和s2。

编辑:

搜索“US_O_P”的示例用法

Original: 9.7334702479 seconds
Tiered: 2.471287074 seconds

编辑: 基准测试结果

我多次运行测试以搜索“US_O_P”并找到以下1,000,000,000次搜索结果

public class RegionMapOrig {
    private Map<String, String> map;

    public RegionMapOrig() {
        init();
    }

    private void init() {
        map = new HashMap<>();
        map.put("US_A_B", "1");
        map.put("US_A_*", "2");
        map.put("US_*_*", "3");
        map.put("US_O_O", "4");
        map.put("US_*_W", "5");
        map.put("ASIA_*_*", "6");
    }

    public String search(String reg, String s1, String s2) {
        String val = null;
        val = map.get(reg + "_" + s1 + "_" + s2);
        if (val == null) {
            val = map.get(reg + "_" + s1 + "_*");
            if (val == null) {
                val = map.get(reg + "_" + "*_" + s2);
                if (val == null) {
                    val = map.get(reg + "_*_*");
                }
            }
        }
        return val;
    }
}

private static final int N = 1000000000;

public static void main(String[] args) {
    String region = "US";
    String s1 = "O";
    String s2 = "P";

    testOrig(region, s1, s2);
    test(region, s1, s2);
}

private static void testOrig(String region, String s1, String s2) {
    RegionMapOrig map = new RegionMapOrig();

    long start = System.nanoTime();

    for (int i = 0; i < N; ++i) {
        String val = map.search(region, s1, s2);
    }

    long end = System.nanoTime();
    System.out.println((end - start) / 10E9);
}

private static void test(String region, String s1, String s2) {
    RegionMap map = new RegionMap();

    long start = System.nanoTime();

    for (int i = 0; i < N; ++i) {
        String val = map.search(region, s1, s2);
    }

    long end = System.nanoTime();
    System.out.println((end - start) / 10E9);
}

以下是基准代码

Orginal (no concatentation): 1.2068575417 seconds
Tiered: 2.2982665873 seconds

多次运行此代码产生了相同的结果。但是,这个基准很简单,可能不是确定的。要真正测试结果,您需要使用表示典型值的实际数据集来分析性能。我相信您的性能问题可能在您的字符串连接中,而不是对地图的调用次数。我的内部地图可能表现得更好的另一个原因是我的内部地图可以被缓存,从而更快地进行多次检索。

编辑:基准更新

通过删除字符串连接进一步调查后,您的原始代码得到了改进,显示了以下结果:

public String searchNoCat(String cache1, String cache2, String cache3,  String cache4) {
    String val = null;
    val = map.get(cache1);
    if (val == null) {
        val = map.get(cache2);
        if (val == null) {
            val = map.get(cache3);
            if (val == null) {
                val = map.get(cache4);
            }
        }
    }
    return val;
}

private static void testOrigNoCat(String region, String s1, String s2) {
    RegionMapOrig map = new RegionMapOrig();

    String cache1 = region + "_" + s1 + "_" + s2;
    String cache2 = region + "_" + s1 + "_*";
    String cache3 = region + "_" + "*_" + s2;
    String cache4 = region + "_*_*";

    long start = System.nanoTime();

    for (int i = 0; i < N; ++i) {
        String val = map.searchNoCat(cache1, cache2, cache3, cache4);
    }

    long end = System.nanoTime();
    System.out.println((end - start) / 10E9);
}

代码更改为:

java.lang.IllegalStateException: Application launch must not be called more than once

但是,问题仍然存在于如何有效地缓存此类值或减少通用输入的连接数。我不知道有效的方法。因此,我认为分层映射是避免连接问题的有效解决方案。

答案 1 :(得分:1)

看起来您需要一些树结构来帮助您在搜索值时使用通配符(“*”)替换来封装逻辑。

首先,我写了一些单元测试来描述预期的行为

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;

public class WildcardSearchSpec {
    private Node root;

    @Before
    public void before() {
        root = new WildcardSearch();
        root.add("US_A_B", "1");
        root.add("US_A_*", "2");
        root.add("US_*_*", "3");
        root.add("US_O_O", "4");
        root.add("US_*_W", "5");
        root.add("ASIA_*_*", "6");
    }

    @Test
    public void itShouldReturnFullWildcardCorrespondingValue() {
        String key = "US_Q_Q";

        String value = root.value(key);

        assertEquals("3", value);
    }

    @Test
    public void itShouldReturnNoWildcardCorrespondingValue() {
        String key = "US_A_B";

        String value = root.value(key);

        assertEquals("1", value);
    }

    @Test
    public void itShouldReturnS2WildcardCorrespondingValue() {
        String key = "US_A_U";

        String value = root.value(key);

        assertEquals("2", value);
    }

    @Test
    public void itShouldReturnS1WidlcardCorrespondingValue() {
        String key = "US_W_W";

        String value = root.value(key);

        assertEquals("5", value);
    }

    @Test(expected=NoValueException.class)
    public void itShouldThrowWhenNoCorrespondingValue() {
        String key = "EU_A_B";

        root.value(key);

        fail();
    }
}

可以从这些测试中提取的接口如下

public interface Node {
    void add(String key, String value);
    String value(String key);
}

WildcardSearch

实施
import java.util.HashMap;
import java.util.Map;

public final class WildcardSearch implements Node {
    private final Map<String, CountrySearch> children = new HashMap<>();

    @Override
    public void add(String key, String value) {
        String country = key.split("_")[0];
        String rest = key.substring(country.length() + 1);

        children.putIfAbsent(country, new CountrySearch());
        children.get(country).add(rest, value);
    }

    @Override
    public String value(String key) {
        String country = key.split("_")[0];
        String rest = key.substring(country.length() + 1);

        if (!children.containsKey(country)) {
            return children.get(country).value(rest);
        } else {
            throw new NoValueException();
        }
    }
}

WildcardSearch使用CountrySearch委派每个国家/地区的搜索。

import java.util.HashMap;
import java.util.Map;

final class CountrySearch implements Node {
    private final Map<String, SuffixeSearch> children = new HashMap<>();

    @Override
    public void add(String key, String value) {
        String[] splittedKey = key.split("_");
        String s1 = splittedKey[0];
        String s2 = splittedKey[1];
        children.putIfAbsent(s1, new SuffixeSearch());
        children.get(s1).add(s2, value);
    }

    @Override
    public String value(String key) {
        String[] splittedKey = key.split("_");
        String s1 = splittedKey[0];
        String s2 = splittedKey[1];

        if (children.containsKey(s1)) {
            return children.get(s1).value(s2);
        } else if (children.containsKey("*")) {
            return children.get("*").value(s2);
        } else {
            throw new NoValueException();
        }
    }
}

CountrySearch使用SuffixeSearch委托后缀中的搜索。

import java.util.HashMap;
import java.util.Map;

final class SuffixeSearch implements Node {
    private final Map<String, String> children = new HashMap<>();

    public void add(String key, String value) {
        children.put(key, value);
    }

    @Override
    public String value(String key) {
        if (children.containsKey(key)) {
            return children.get(key);
        } else if (children.containsKey("*")) {
            return children.get("*");
        } else {
            throw new NoValueException();
        }
    }
}

注意:NoValueException是自定义RuntimeException

关键是每项责任都明确分开。

SuffixeSearch只能返回相应键的值或与“*”对应的值。它不知道整体密钥是如何构建的,也不是按国家/地区聚类的值等。

CountrySearch只知道其级别,将其余部分委托给SuffixeSearch或忽略上面的内容。

WildcardSearch只知道在国家/地区分裂,并且委托CountrySearch负责执行通配符魔术。

答案 2 :(得分:0)

最好和更通用的解决方案是使用Search Tree,你可以很容易地实现自己,也是一个很好的编程练习。还有很多教程和示例,如何实现它。

对于您的特殊用例,您可以使用级联映射,因为DragonAssassin已发布,它利用了Java已经提供的功能。

答案 3 :(得分:0)

如果准备正确,你可以嵌套三个地图并为通用案例标记一个入口星(实际上*只是地图中的另一个键)。要获得所需的数字,您需要三个&#34;索引&#34;。假设总会有一个* -Map:

 Map<String, Map<String, Map<String, Integer>>> map;
 Map<String, Map<String, String> us_map = new Map<String, Map<String, String>();
 Map<String, Map<String, String> asia_map = new Map<String, Map<String, String>();

 Map<String, String> us_a_map = new Map<String, Integer>();
 us_a_map.put("B", 1);
 us_a_map.put("*", 2);

 Map<String, String> us_star_map = new Map<String, Integer>();
 us_star_map.put("*", 3);
 us_star_map.put("W", 5);

 map.put( "US", us_map);
 us_map.put( "A", us_a_map );
 us_map.put( "*", us_star_map );

 map.put( "ASIA", asia_map);

在此地图中,性能将优于您提议的情况,因为地图较小。例如,要获取元素US_A_B,您将

 Integer value = map.get( "US" ).get( "A" ).get( "B" );

要处理缺失的元素(在这种情况下必须考虑*元素),也可以在每个级别中找到Map条目&#34;&#34;:使用以下输入:

 String l0 = "US";
 String l1 = "A";
 String l2 = "unknown";

假设总有一个条目为&#34; *&#34;在每个地图中:

 Map<String, Map<String, String>> level_0
 Map<String, String> level_1;
 Integer level_2; // This will be the desired result

 level_0 = map.get(l0);
 if (level_0 == null) {
      level_0 = star_0;
 }

 level_1 = level_0.get(l1);
 if (level_1 == null) {
      level_1 = level_0.get("*");
 }

 level_2 = level_1.get(l2);
 if (level_2 == null) {
      level_2 = level_1.get("*");
 }

结果将是level_2的值。

答案 4 :(得分:0)

一种可能的优化是将地图扩展为所有可能的值,它需要更多的内存并且有一些初始化成本,但它可能是值得的。

我做了一些假设,如果它们不适用于你的问题,这种方法对你来说毫无用处。

  • 区域数据不会更改(在数据更改的情况下,部分重新启动是可以接受的)。
  • 它始终是一个字符而不是“明星”。所以“US_A_B”不是“US_AA_BB”。
  • 只有大写字母而不是“星号”。所以没有“US_a_b”或“US _ / _ /”

此方法为每个区域创建int []。在该数组中,为'A''A'计算所有可能的值 - &gt; 'Z''Z'包括'*'。因此,对于请求,您只需要找到正确的int []并根据提供的字符计算数组中的索引。

我用@DragonAssassin的基准测试来运行它并得到了他的方法的1/10。每个地区的成本约为1kb。

以下是代码:

static class AreaMapBuilder {
    private List<String> areas = new ArrayList<>();
    private Map<String, Integer> codes = new HashMap<>();

    public void put(String area, char a, char b, int value) {
        areas.add(area);
        if (a == '*')
            a = '@';
        if (b == '*')
            b = '@';
        codes.put(area + "_" + a + "_" + b, value);
    }

    public AreaMap build() {
        Map<String, int[]> codes = new HashMap<>();
        for (String area : areas) {
            codes.put(area, forArea(area));
        }

        return new AreaMap(codes);

    }

    private int[] forArea(String area) {
        int[] forArea = new int[27 * 27];
        for (int indexA = 0; indexA < 27; indexA++) {
            for (int indexB = 0; indexB < 27; indexB++) {

                forArea[indexA * 27 + indexB] = slowGet(area, (char) (indexA + '@'), (char) (indexB + '@'));
            }
        }
        return forArea;
    }

    private int slowGet(String area, char a, char b) {
        Integer val = codes.get(area + "_" + a + "_" + b);
        if (val == null) {
            val = codes.get(area + "_" + a + "_@");
            if (val == null) {
                val = codes.get(area + "_" + "@_" + b);
                if (val == null) {
                    val = codes.get(area + "_@_@");
                }
            }
        }

        return val;

    }
}

static class AreaMap {
    private Map<String, int[]> codes;

    public AreaMap(Map<String, int[]> codes) {
        this.codes = codes;
    }

    public int get(String area, char a, char b) {
        if (a == '*')
            a = 0;
        else
            a -= '@';
        if (b == '*')
            b = 0;
        else
            b -= '@';
        return codes.get(area)[a * 27 + b];
    }
}

static AreaMap getMap(){
    AreaMapBuilder areaBuilder = new AreaMapBuilder();
    areaBuilder.put("US", 'A', 'B', 1);
    areaBuilder.put("US", 'A', '*', 2);
    areaBuilder.put("US", '*', '*', 3);
    areaBuilder.put("US", 'O', 'O', 4);
    areaBuilder.put("US", '*', 'W', 5);
    areaBuilder.put("ASIA", '*', '*', 6);
    return areaBuilder.build();     
}