【问题标题】:Lookup substring in map, case-insensitive在 map 中查找子字符串,不区分大小写
【发布时间】:2020-06-22 15:00:10
【问题描述】:

我编写了一个小型词法分析器,可将字符缓冲区转换为标记流。一种类型的标记是标识符,它可以是“原始”或关键字。为了测试后者,我有一个包含所有关键字的地图。

Map<String, MyType> lookup = new HashMap<>();
lookup.put("RETURN", KEYWORD_RETURN);
[...]

地图由所有大写字符串填充。

现在,我从输入字符缓冲区中得到的只是一个偏移量和长度,我可以在其中找到我的标识符(在那里不必是大写的)。

显而易见的解决方案看起来有点像这样。

bool lookupIdentifier(CharBuffer buffer, int offset, int length, Map<String, MyType> lookupTable) {
    int current = buffer.position();
    buffer.rewind();
    String toCheck = buffer.subSequence(offset, offset + length).toString().toUpperCase();
    buffer.position(current);
    return lookupTable.containsKey(toCheck);
}

地图中有大约 50 个条目。带有不区分大小写比较器的 TreeMap 是 O(1) HashMap 查找的良好替代方案吗?

我不喜欢我的方法是toCheck 字符串的创建分配。有没有办法重用CharBuffer 中的子字符串进行查找?

【问题讨论】:

  • 地图的键是一个字符串。无论您是否使用toCheck,都会在某处使用字符串。仅仅因为您不将其存储在变量中并不意味着它不存在。虽然,imo,使用变量更简洁。
  • @HarshalParekh 您能否提供 O(1) 声明的来源? TreeMap 的 javadoc 声明“此实现为 containsKey、get、put 和 remove 操作提供有保证的 log(n) 时间成本。”

标签: java string hashmap


【解决方案1】:

您可以通过使用CharBuffer 作为键类型来避免昂贵的字符串构造:

Map<CharBuffer, MyType> lookup = new TreeMap<>(Comparator
    .comparingInt(CharBuffer::remaining)
    .thenComparing((cb1,cb2) -> {
        for(int p1 = cb1.position(), p2 = cb2.position(); p1 < cb1.limit(); p1++, p2++) {
            char c1 = cb1.get(p1), c2 = cb2.get(p2);
            if(c1 == c2) continue;
            c1 = Character.toUpperCase(c1);
            c2 = Character.toUpperCase(c2);
            if(c1 != c2) return Integer.compare(c1, c2);
        }
        return 0;
    }));
lookup.put(CharBuffer.wrap("RETURN"), MyType.KEYWORD_RETURN);
boolean lookupIdentifier(
    CharBuffer buffer, int offset, int length, Map<CharBuffer, MyType> lookupTable) {

    int currentPos = buffer.position(), currLimit = buffer.limit();
    buffer.clear().position(offset).limit(offset + length);
    boolean result = lookupTable.containsKey(buffer);
    buffer.clear().position(currentPos).limit(currLimit);
    return result;
}

比较器在执行不区分大小写的字符比较之前使用廉价的长度比较。这假设您使用诸如 RETURN 之类的关键字,这些关键字具有简单的大小写映射。

对于包含 50 个关键字的地图,使用 log₂ 比较进行查找可能仍会产生合理的性能。请注意,每次比较都会在第一次不匹配时停止。


您可以将散列与专用包装对象一起使用:

final class LookupKey {
    final CharBuffer cb;
    LookupKey(CharBuffer cb) {
        this.cb = cb;
    }
    @Override public int hashCode() {
        int code = 1;
        for(int p = cb.position(); p < cb.limit(); p++) {
            code = Character.toUpperCase(cb.get(p)) + code * 31;
        }
        return code;
    }
    @Override public boolean equals(Object obj) {
        if(!(obj instanceof LookupKey)) return false;
        final LookupKey other = (LookupKey)obj;
        CharBuffer cb1 = this.cb, cb2 = other.cb;
        if(cb1.remaining() != cb2.remaining()) return false;
        for(int p1 = cb1.position(), p2 = cb2.position(); p1 < cb1.limit(); p1++, p2++) {
            char c1 = cb1.get(p1), c2 = cb2.get(p2);
            if(c1 == c2) continue;
            c1 = Character.toUpperCase(c1);
            c2 = Character.toUpperCase(c2);
            if(c1 != c2) return false;
        }
        return true;
    }
}
Map<LookupKey, MyType> lookup = new HashMap<>();
lookup.put(new LookupKey(CharBuffer.wrap("RETURN")), MyType.KEYWORD_RETURN);
boolean lookupIdentifier(
    CharBuffer buffer, int offset, int length, Map<LookupKey, MyType> lookupTable) {

    int currentPos = buffer.position(), currLimit = buffer.limit();
    buffer.clear().position(offset).limit(offset + length);
    boolean result = lookupTable.containsKey(new LookupKey(buffer));
    buffer.clear().position(currentPos).limit(currLimit);
    return result;
}

LookupKey 这样的轻量级对象的构造,与String 构造不同,不需要复制字符内容,可以忽略不计。但请注意,与比较器不同,散列必须预先处理所有字符,这可能比小型 TreeMap 的日志 2 比较更昂贵。

如果这些关键字不太可能更改,则显式查找代码(即在键字符串的不变属性上使用 switch)会更有效。例如。从切换length 开始,如果大多数关键字的长度不同,然后切换一个对于大多数关键字都不同的字符(包括case 用于大写和小写变体的标签)。另一种选择是这些属性的分层查找结构。

【讨论】:

  • 这个伟大的过程答案中有很多有用的信息。
  • 您能解释一下比较器函数的最后一步是做什么的吗? if(c1 != c2) return Integer.compare(c1, c2); 两个字符都是大写的,当它们不相等时...?
  • @a.ilchinger 比较器的约定是返回一个正整数或负整数,这取决于第一个对象是小于还是大于第二个对象。方法Integer.compare(…) 具有相同的约定,因此将第一个不匹配字符委托给它会使顺序取决于最左边的不同字符,就像标准字典顺序一样,只是我们使用大写字符使其不区分大小写.但如前所述,由于这是comparingInt(CharBuffer::remaining) 之后的次要顺序,因此地图的主要顺序是按长度。
猜你喜欢
  • 2012-03-04
  • 2012-01-19
  • 2015-04-22
  • 2013-11-05
  • 1970-01-01
  • 2022-11-22
  • 1970-01-01
  • 1970-01-01
  • 2011-08-18
相关资源
最近更新 更多