【问题标题】:How to convert an alphanumeric phone number to digits如何将字母数字电话号码转换为数字
【发布时间】:2023-06-25 05:46:01
【问题描述】:

更新:

我的实用程序的最终版本如下所示:

StringBuilder b = new StringBuilder();

for(char c : inLetters.toLowerCase().toCharArray())
{
    switch(c)
    {
    case '0':                                          b.append("0"); break;
    case '1':                                          b.append("1"); break;
    case '2': case 'a': case 'b': case 'c':            b.append("2"); break;
    case '3': case 'd': case 'e': case 'f':            b.append("3"); break;
    case '4': case 'g': case 'h': case 'i':            b.append("4"); break;
    case '5': case 'j': case 'k': case 'l':            b.append("5"); break;
    case '6': case 'm': case 'n': case 'o':            b.append("6"); break;
    case '7': case 'p': case 'q': case 'r': case 's':  b.append("7"); break;
    case '8': case 't': case 'u': case 'v':            b.append("8"); break;
    case '9': case 'w': case 'x': case 'y': case 'z':  b.append("9"); break;
    }
}

return builder.toString();

原始问题:

我正在承担将字母数字电话号码转换为一串数字的简单任务。例如,1-800-HI-HAXOR 将变为 1-800-44-42967。我最初的尝试是创建一个讨厌的 switch 语句,但我想要一个更优雅、更有效的解决方案。这是我得到的:

for(char c : inLetters.toLowerCase().toCharArray())
{
    switch(c)
    {
    case '0':                                         result+="0"; break;
    case '1':                                         result+="1"; break;
    case '2': case 'a': case 'b': case 'c':           result+="2"; break;
    case '3': case 'd': case 'e': case 'f':           result+="3"; break;
    case '4': case 'g': case 'h': case 'i':           result+="4"; break;
    case '5': case 'j': case 'k': case 'l':           result+="5"; break;
    case '6': case 'm': case 'n': case 'o':           result+="6"; break;
    case '7': case 'p': case 'q': case 'r': case 's': result+="7"; break;
    case '8': case 't': case 'u': case 'v':           result+="8"; break;
    case '9': case 'w': case 'x': case 'y': case 'z': result+="9"; break;
    }
}

谢谢!

【问题讨论】:

  • 尽管大多数人认为他们可以做到“优雅”得多,但您的“讨厌的 switch 语句”更容易理解。讽刺:)
  • 如果你想改进它,你有太多时间了。 ;)

标签: java performance algorithm switch-statement


【解决方案1】:

switch 语句并没有那么糟糕。您的算法与电话号码的长度成线性关系。该代码是可读的,并且很容易通过检查来验证。我不会弄乱它,除了添加一个default 案例来处理错误。 (我不是 Java 程序员,所以如果它被称为别的东西,请原谅我。)

如果您使其更快,则按字符索引的预初始化表将避免超出基本错误检查的任何比较。您甚至可以通过复制表中的值来避免大小写转换 (digit['A'] = digit['a'] = "2";)。初始化表的成本将在转换总数中摊销。

【讨论】:

  • 这也可以,但我会犹豫以这种方式过早优化。虽然 时间 会在所有转化中摊销,但 空间 是持久的,对于您希望获得的时间节省量而言,可能完全是浪费精力。
  • 我喜欢。它更容易阅读,并且性能也很好。我认为该表所需的 100 字节左右在许多环境中都不会成为阻碍因素。
  • 哦,在几乎任何环境中,我都不会认为它是一个表演障碍。我只是反对在小情况下不必要的膨胀,因为它们加起来。我发现愿意在这里和那里花费 100 字节来获取在程序生命周期内持久的对象的程序员最终会摸不着头脑,为什么他们的程序最终会使用太多内存,并且很少知道如何减少他们的内存。脚印。确实,CPU 寄存器比 RAM 快得多,并且为了方便而将某些内容加载到内存中可能会损害应用程序其他部分的性能。因此,首先要测量。
  • 您似乎忽略了switch语句所需的存储空间,这大致相当于数组所需的空间(省略了是否包含大小写的决定)。
【解决方案2】:

您可以使用 Apache Commons Lang StringUtils 执行此操作,如下所示:

String output = StringUtils.replaceChars(StringUtils.lowerCase(input),
                    "abcdefghijklmnopqrstuvwxyz",
                    "22233344455566677778889999");

当然,假设速度不是您主要关心的问题,并且您想要一个紧凑的解决方案;)

【讨论】:

  • 什么时候“高效”不代表速度了?
  • 这是一个非常酷的解决方案!它绝对是紧凑的,但我正在寻找更高效的东西。我认为我不想为此转换加载 Apache Commons jar。
  • @Quinn 高效可能意味着更少的代码。在这种情况下,优雅和高效会更好地结合在一起。高效也可能意味着记忆。鉴于 OP 没有使用 StringBuilder,速度似乎并不是首要任务。无论如何,取决于 StringUtils 所做的事情,速度可能会很好,并且内存占用不需要增加,它可以使用原语和传入的引用来完成它的工作。
  • @Reed,如果您不想要整个项目,可以复制该方法 - 毕竟它是开源的。
  • OP 可能不知道 StringBuilder,虽然使用它是一种很好的做法,但我认为它对于电话号码长度的东西并不重要。 :-)
【解决方案3】:

使用Map,其中键是字母和数字,值是小键盘上的数字。 (因此每个键盘编号将由三个或四个字母和一个数字索引)。

Map<Character, Character> keypad = new HashMap<Character, Character>();
...
StringBuilder buf = new StringBuilder(inLetters.length());
for (int idx = 0; idx < inLetters.length(); ++idx) {
  Character ch = keypad.get(inLetters.charAt(idx));
  if (ch != null)
    buf.append(ch);
}

更新:我很好奇手工编码的查找表是否会比密集集 switch 的情况更好。在我的随意测试中,我发现以下代码是我能想到的最快的:

  private static final char[] lut = 
    "0123456789:;<=>?@22233344455566677778889999[\\]^_`22233344455566677778889999".toCharArray();

  private static final char min = lut[0];

  String fastest(String letters)
  {
    int n = letters.length();
    char[] buf = new char[n];
    while (n-- > 0) {
      int ch = letters.charAt(n) - min;
      buf[n] = ((ch < 0) || (ch >= lut.length)) ? letters.charAt(n) : lut[ch];
    }
    return new String(buf);
  }

令人惊讶的是,它的速度是使用 switch 语句(编译为 tableswitch 指令)的类似代码的两倍多。请注意,这只是为了好玩,但在我的笔记本电脑上,在单线程中运行,我可以在大约 1.3 秒内转换 1000 万个 10 字母“数字”。我真的很惊讶,因为据我了解,tableswitch 的运行方式基本相同,但我预计它会更快,因为它是 JVM 指令。

当然,除非我只为我可以转换的无限数量的电话号码中的每一个获得报酬,否则我永远不会编写这样的代码。开关更具可读性,按原样执行,并且可能在未来的某些 JVM 中获得免费的性能提升。

很明显,对原始代码的最大改进来自于使用StringBuilder 而不是连接字符串,这不会影响代码的可读性。使用charAt 而不是将输入转换为char[] 也使代码更简单、更易于理解并且 也提高了性能。最后,附加 char 文字而不是 String 文字('1' 而不是 "1")是一种性能改进,也有助于提高可读性。

【讨论】:

  • 这种方式和@Adrian的方式类似,但也涉及到Character到char之间的隐形拆箱,效率上确实节省不了多少。
  • 它是为清晰和易于维护而设计的,而不是为了原始效率......尽管猜测,我敢冒险它比级联 if-else 更快。 Adrian 的解决方案应该比 switch 或 if-else 提供最好的性能和更好的清晰度。
  • 当然有可能它具有可比性甚至更快(我没有测量过),但是查阅地图确实需要将其加载到缓存中(速度很快但不是免费的)并且在至少 2 个 Character 对象上调用 hash()。 CPU/JVM 分支预测也非常快,因此在实践中它们可能非常接近。
【解决方案4】:

简单地说:

   String convert(String inLetters) {
      String digits = "22233344455566677778889999";
      String alphas = "abcdefghijklmnopqrstuvwxyz";
      String result = "";
      for (char c : inLetters.toLowerCase().toCharArray()) {
          int pos = alphas.indexOf(c);
          result += (pos == -1 ? c : digits.charAt(pos));
      }
      return result;
   }

【讨论】:

  • 除非我会使用 StringBuilder 来生成翻译结果
【解决方案5】:

如果您想要一个不强制您枚举所有字母的解决方案,您可以执行以下操作:

char convertedChar = c;
if (Character.isLetter(c)) {
    //lowercase alphabet ASCII codes: 97 (a)-122 (z)
    int charIndex = ((int)c) - 97;
    //make adjustments to account for 's' and 'z'
    if (charIndex >= 115) { //'s'
        charIndex--;
    }
    if (charIndex == 121) { //'z'-1
        charIndex--;
    }
    convertedChar = (char)(2 + (charIndex/3));
}
result += convertedChar;

【讨论】:

    【解决方案6】:

    如果你在一个紧凑的循环中运行 10^9 次并按 ctrl-break 几次,我敢打赌,几乎每次它都会深入到字符串类中,试图完成那些看似无辜的 "+ =" 运算符。

    【讨论】:

    • 非常正确 - 在实际实现中,我坚持使用 switch 语句,但使用了字符串生成器。
    • 是的。即便如此,我敢打赌花在切换代码上的时间也不会占很大比例。
    【解决方案7】:

    Switch 语句被编译为与 if-else 语句类似的形式,(每个 case 语句本质上是一个变相的 if (c == '...') 测试)所以虽然这在视觉上比级联 if 更紧凑对于每个字符的测试,可能有也可能没有任何真正的性能优势。

    您可以通过消除一些比较来简化它。关键是char 是一个整数类型(这就是你可以打开char 的原因),所以你可以使用数字比较运算符。和 'a假设您的 inLetters 字符串仅包含字母数字字符,这应该可以工作...(所有其他字符将通过不变。)

    String result = "";
    for (char c : letters.toLowerCase().toCharArray()) {
        if      (c <= '9') result += c;
        else if (c <= 'c') result += "2";
        else if (c <= 'f') result += "3";
        else if (c <= 'i') result += "4";
        else if (c <= 'l') result += "5";
        else if (c <= 'o') result += "6";
        else if (c <= 's') result += "7";
        else if (c <= 'v') result += "8";
        else if (c <= 'z') result += "9";
        else               result += c;
    }
    

    感兴趣的字符具有十六进制值:'0' = 0x30、'9' = 0x39、'a' = 0x61 和 'z' = 0x7a。

    编辑:最好使用StringBuilderappend() 来创建字符串,但对于小字符串,它的速度可能不会明显加快。 (Amdahl's Law 表明优化代码所期望的实际加速受到该代码实际花费的时间百分比的限制。)我只使用连接字符串使算法对 OP 清晰。

    【讨论】:

    • 这是错误的。 Switch 语句被编译为基于偏移量的跳转或二叉搜索树,具体取决于常量的密集程度。
    • 细节在很大程度上取决于编译器和虚拟机。我不关心字节码——不管机器指令是如何布局的,真正的一点是减少比较的数量会导致更快的代码。从执行效率上退一步,这种方法可以说是一个“更优雅”的解决方案。 (当然,这取决于人们对 char 变量的理解程度。)
    • 老实说,原版对我来说更具可读性。我根本不需要考虑它。为了理解这一点,我需要背诵字母来确定特定字符属于哪个子句。为了清楚起见,我更喜欢 switch 语句。
    • 好点。由于 switch 语句实际上是 == 比较,因此查找表很可能比 '
    • 干得好,尤其是您的最终编辑。那些小的“+=”运算符将比整个其余代码消耗更多的时间。
    最近更新 更多