在.NET中将十进制数转换为任何进制的最快方法是什么?

130

我有一个有点老的C#方法,它将一个数字转换为任何进制:

string ConvertToBase(int number, char[] baseChars);

它并不是非常快速和整洁。有没有一种好的已知方法可以在.NET中实现这个?

我正在寻找一种允许我使用任何基数和任意字符串的东西。

这只允许基数为16、10、8和2:

Convert.ToString(1, x);

我希望利用数字、全小写字母和全大写字母来实现极高的进位。就像这个帖子中所示,但是在C#而不是JavaScript中实现。

有人知道在C#中实现这个的好且高效的方法吗?

12个回答

160

Convert.ToString可用于将数字转换为指定基数下的字符串表示形式。

示例:

string binary = Convert.ToString(5, 2); // convert 5 to its binary representation
Console.WriteLine(binary);              // prints 101

然而,正如评论所指出的那样,Convert.ToString仅支持以下有限但通常足够的一组进制:2、8、10或16。
更新(以满足转换为任何基数的要求):
我不知道BCL中是否有任何方法能够将数字转换为任何基数,因此您需要编写自己的小型实用程序函数。一个简单的示例如下(请注意,通过替换字符串连接,这肯定可以更快):
class Program
{
    static void Main(string[] args)
    {
        // convert to binary
        string binary = IntToString(42, new char[] { '0', '1' });

        // convert to hexadecimal
        string hex = IntToString(42, 
            new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
                         'A', 'B', 'C', 'D', 'E', 'F'});

        // convert to hexavigesimal (base 26, A-Z)
        string hexavigesimal = IntToString(42, 
            Enumerable.Range('A', 26).Select(x => (char)x).ToArray());

        // convert to sexagesimal
        string xx = IntToString(42, 
            new char[] { '0','1','2','3','4','5','6','7','8','9',
            'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
            'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x'});
    }

    public static string IntToString(int value, char[] baseChars)
    {
        string result = string.Empty;
        int targetBase = baseChars.Length;

        do
        {
            result = baseChars[value % targetBase] + result;
            value = value / targetBase;
        } 
        while (value > 0);

        return result;
    }

    /// <summary>
    /// An optimized method using an array as buffer instead of 
    /// string concatenation. This is faster for return values having 
    /// a length > 1.
    /// </summary>
    public static string IntToStringFast(int value, char[] baseChars)
    {
        // 32 is the worst cast buffer size for base 2 and int.MaxValue
        int i = 32;
        char[] buffer = new char[i];
        int targetBase= baseChars.Length;

        do
        {
            buffer[--i] = baseChars[value % targetBase];
            value = value / targetBase;
        }
        while (value > 0);

        char[] result = new char[32 - i];
        Array.Copy(buffer, i, result, 0, 32 - i);

        return new string(result);
    }
}

更新2(性能改进)

使用数组缓冲区而非字符串拼接来构建结果字符串可以提高性能,特别是在大数字上(见方法IntToStringFast)。在最优情况下(即最长的输入),该方法大约快三倍。但是,对于1位数(即目标基数中的1位数),IntToString将更快。


6
需要注意的是,这仅支持2、8、10、16进制 - 而不是问题中提到的“任何”进制。因为你永远不知道何时会需要六十进制;-p - Marc Gravell
54
六十进制听起来很有趣。 - rein
9
太棒了!但反函数在哪里? :/ - ashes999
2
我这里有一个初步的反函数:https://dev59.com/ilDTa4cB1Zd3GeqPGBFW#3579977 - ashes999
1
对于“二十六进制”转换(用于类似Excel的列),我不得不将这一行result = baseChars[value % targetBase] + result;替换为result = baseChars[--value % targetBase] + result;。由于我只需要将列号转换为Excel列名,因此我没有测试其他情况。 - FercoCQ
显示剩余10条评论

95

我最近写了一篇博客,我的实现在计算过程中没有使用任何字符串操作,这使得它非常快速。支持将数字转换为任意进制(从2到36):

/// <summary>
/// Converts the given decimal number to the numeral system with the
/// specified radix (in the range [2, 36]).
/// </summary>
/// <param name="decimalNumber">The number to convert.</param>
/// <param name="radix">The radix of the destination numeral system (in the range [2, 36]).</param>
/// <returns></returns>
public static string DecimalToArbitrarySystem(long decimalNumber, int radix)
{
    const int BitsInLong = 64;
    const string Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    if (radix < 2 || radix > Digits.Length)
        throw new ArgumentException("The radix must be >= 2 and <= " + Digits.Length.ToString());

    if (decimalNumber == 0)
        return "0";

    int index = BitsInLong - 1;
    long currentNumber = Math.Abs(decimalNumber);
    char[] charArray = new char[BitsInLong];

    while (currentNumber != 0)
    {
        int remainder = (int)(currentNumber % radix);
        charArray[index--] = Digits[remainder];
        currentNumber = currentNumber / radix;
    }

    string result = new String(charArray, index + 1, BitsInLong - index - 1);
    if (decimalNumber < 0)
    {
        result = "-" + result;
    }

    return result;
}

如果有人需要的话,我也实现了一个快速反函数:

任意进制转换为十进制数

7
我对这个页面上所有的解决方案进行了性能测试,结果发现这个方法是最快的,速度大约是最后一个简短解决方案的两倍。 - Justin R.
这个 result = "-" + result 是什么意思?是一种填充吗?我该如何修改代码,以便只使用 A-Z 或 0-9 作为填充字符? - oscilatingcretin
result = "-" + result 中的 "-" 代表负数的负号,而不是填充字符。 - Pavel Vladov
如果您将“-”放入charArray的“index”中并在创建新字符串之前将其减小,则可以省去字符串连接。 - Caius Jard
请问如何在数字前面保留“0”? - Niroshan K

27

快速的“从”和“到”方法

虽然我来晚了,但我综合之前的答案并改进了它们。我认为这两种方法比迄今为止发布的任何其他方法都要快。在单核机器上,我能够在不到400毫秒的时间内将1,000,000个数字从基数36转换为另一个基数36。

以下示例是针对基数62的。更改BaseChars数组以从任何其他基数进行转换。

private static readonly char[] BaseChars = 
         "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray();
private static readonly Dictionary<char, int> CharValues = BaseChars
           .Select((c,i)=>new {Char=c, Index=i})
           .ToDictionary(c=>c.Char,c=>c.Index);

public static string LongToBase(long value)
{
   long targetBase = BaseChars.Length;
   // Determine exact number of characters to use.
   char[] buffer = new char[Math.Max( 
              (int) Math.Ceiling(Math.Log(value + 1, targetBase)), 1)];

   var i = buffer.Length;
   do
   {
       buffer[--i] = BaseChars[value % targetBase];
       value = value / targetBase;
   }
   while (value > 0);

   return new string(buffer, i, buffer.Length - i);
}

public static long BaseToLong(string number) 
{ 
    char[] chrs = number.ToCharArray(); 
    int m = chrs.Length - 1; 
    int n = BaseChars.Length, x;
    long result = 0; 
    for (int i = 0; i < chrs.Length; i++)
    {
        x = CharValues[ chrs[i] ];
        result += x * (long)Math.Pow(n, m--);
    }
    return result;  
} 

编辑(2018年7月12日)

修复了由@AdrianBotor(请参见评论)发现的转换46655为基数36的特殊情况。这是由于计算Math.Log(46656, 36)时出现了小浮点误差,其准确值为3,但.NET返回3 + 4.44e-16,这会在输出缓冲区中导致多一个字符。


@AdrianBotor 无法重现您的问题:BaseToLong(LongToBase(46655)) == 46655 - Diego
2
@Diego,抱歉回复晚了。让我们用“0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ”初始化数组,并将值“46655”转换。结果应该是“ZZZ”,但在调试器中我得到了“\0ZZZ”。只有这个值会额外添加“\0”。例如,值“46654”可以正确转换为“ZZY”。 - Adrian Botor
@AdrianBotor 很好的发现。通过调整 LongToBase 中的返回语句为 return new string(buffer, (int) i, buffer.Length - (int)i); 来解决。 - Diego

12

一个人也可以使用稍微修改过的已接受版本,并根据自己的需要调整基本字符字符串:

public static string Int32ToString(int value, int toBase)
{
    string result = string.Empty;
    do
    {
        result = "0123456789ABCDEF"[value % toBase] + result;
        value /= toBase;
    }
    while (value > 0);

    return result;
}

3
非常晚才了解到这一点,但我最近为工作中的一个项目编写了以下帮助类。它旨在将短字符串转换为数字并反向转换(简单的完美哈希函数),但它也可以在任意进制之间执行数字转换。实现Base10ToString方法回答了最初发布的问题。
传递给类构造函数的shouldSupportRoundTripping标志是必需的,以防止在从数字字符串转换为十进制数和反向转换过程中丢失前导数字(鉴于我的要求,这是至关重要的!)。大多数情况下,数字字符串中前导0的丢失可能不是问题。
无论如何,以下是代码:
using System;
using System.Collections.Generic;
using System.Linq;

namespace StackOverflow
{
    /// <summary>
    /// Contains methods used to convert numbers between base-10 and another numbering system.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This conversion class makes use of a set of characters that represent the digits used by the target
    /// numbering system. For example, binary would use the digits 0 and 1, whereas hex would use the digits
    /// 0 through 9 plus A through F. The digits do not have to be numerals.
    /// </para>
    /// <para>
    /// The first digit in the sequence has special significance. If the number passed to the
    /// <see cref="StringToBase10"/> method has leading digits that match the first digit, then those leading
    /// digits will effectively be 'lost' during conversion. Much of the time this won't matter. For example,
    /// "0F" hex will be converted to 15 decimal, but when converted back to hex it will become simply "F",
    /// losing the leading "0". However, if the set of digits was A through Z, and the number "ABC" was
    /// converted to base-10 and back again, then the leading "A" would be lost. The <see cref="System.Boolean"/>
    /// flag passed to the constructor allows 'round-tripping' behaviour to be supported, which will prevent
    /// leading digits from being lost during conversion.
    /// </para>
    /// <para>
    /// Note that numeric overflow is probable when using longer strings and larger digit sets.
    /// </para>
    /// </remarks>
    public class Base10Converter
    {
        const char NullDigit = '\0';

        public Base10Converter(string digits, bool shouldSupportRoundTripping = false)
            : this(digits.ToCharArray(), shouldSupportRoundTripping)
        {
        }

        public Base10Converter(IEnumerable<char> digits, bool shouldSupportRoundTripping = false)
        {
            if (digits == null)
            {
                throw new ArgumentNullException("digits");
            }

            if (digits.Count() == 0)
            {
                throw new ArgumentException(
                    message: "The sequence is empty.",
                    paramName: "digits"
                    );
            }

            if (!digits.Distinct().SequenceEqual(digits))
            {
                throw new ArgumentException(
                    message: "There are duplicate characters in the sequence.",
                    paramName: "digits"
                    );
            }

            if (shouldSupportRoundTripping)
            {
                digits = (new[] { NullDigit }).Concat(digits);
            }

            _digitToIndexMap =
                digits
                .Select((digit, index) => new { digit, index })
                .ToDictionary(keySelector: x => x.digit, elementSelector: x => x.index);

            _radix = _digitToIndexMap.Count;

            _indexToDigitMap =
                _digitToIndexMap
                .ToDictionary(keySelector: x => x.Value, elementSelector: x => x.Key);
        }

        readonly Dictionary<char, int> _digitToIndexMap;
        readonly Dictionary<int, char> _indexToDigitMap;
        readonly int _radix;

        public long StringToBase10(string number)
        {
            Func<char, int, long> selector =
                (c, i) =>
                {
                    int power = number.Length - i - 1;

                    int digitIndex;
                    if (!_digitToIndexMap.TryGetValue(c, out digitIndex))
                    {
                        throw new ArgumentException(
                            message: String.Format("Number contains an invalid digit '{0}' at position {1}.", c, i),
                            paramName: "number"
                            );
                    }

                    return Convert.ToInt64(digitIndex * Math.Pow(_radix, power));
                };

            return number.Select(selector).Sum();
        }

        public string Base10ToString(long number)
        {
            if (number < 0)
            {
                throw new ArgumentOutOfRangeException(
                    message: "Value cannot be negative.",
                    paramName: "number"
                    );
            }

            string text = string.Empty;

            long remainder;
            do
            {
                number = Math.DivRem(number, _radix, out remainder);

                char digit;
                if (!_indexToDigitMap.TryGetValue((int) remainder, out digit) || digit == NullDigit)
                {
                    throw new ArgumentException(
                        message: "Value cannot be converted given the set of digits used by this converter.",
                        paramName: "number"
                        );
                }

                text = digit + text;
            }
            while (number > 0);

            return text;
        }
    }
}

这也可以被子类化以派生自定义的数字转换器:

namespace StackOverflow
{
    public sealed class BinaryNumberConverter : Base10Converter
    {
        public BinaryNumberConverter()
            : base(digits: "01", shouldSupportRoundTripping: false)
        {
        }
    }

    public sealed class HexNumberConverter : Base10Converter
    {
        public HexNumberConverter()
            : base(digits: "0123456789ABCDEF", shouldSupportRoundTripping: false)
        {
        }
    }
}

代码将会被用作以下方式:

using System.Diagnostics;

namespace StackOverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                var converter = new Base10Converter(
                    digits: "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz",
                    shouldSupportRoundTripping: true
                    );

                long number = converter.StringToBase10("Atoz");
                string text = converter.Base10ToString(number);
                Debug.Assert(text == "Atoz");
            }

            {
                var converter = new HexNumberConverter();

                string text = converter.Base10ToString(255);
                long number = converter.StringToBase10(text);
                Debug.Assert(number == 255);
            }
        }
    }
}

1

这个论坛帖子中的类能帮到你吗?

public class BaseConverter { 

public static string ToBase(string number, int start_base, int target_base) { 

  int base10 = this.ToBase10(number, start_base); 
  string rtn = this.FromBase10(base10, target_base); 
  return rtn; 

} 

public static int ToBase10(string number, int start_base) { 

  if (start_base < 2 || start_base > 36) return 0; 
  if (start_base == 10) return Convert.ToInt32(number); 

  char[] chrs = number.ToCharArray(); 
  int m = chrs.Length - 1; 
  int n = start_base; 
  int x; 
  int rtn = 0; 

  foreach(char c in chrs) { 

    if (char.IsNumber(c)) 
      x = int.Parse(c.ToString()); 
    else 
      x = Convert.ToInt32(c) - 55; 

    rtn += x * (Convert.ToInt32(Math.Pow(n, m))); 

    m--; 

  } 

  return rtn; 

} 

public static string FromBase10(int number, int target_base) { 

  if (target_base < 2 || target_base > 36) return ""; 
  if (target_base == 10) return number.ToString(); 

  int n = target_base; 
  int q = number; 
  int r; 
  string rtn = ""; 

  while (q >= n) { 

    r = q % n; 
    q = q / n; 

    if (r < 10) 
      rtn = r.ToString() + rtn; 
    else 
      rtn = Convert.ToChar(r + 55).ToString() + rtn; 

  } 

  if (q < 10) 
    rtn = q.ToString() + rtn; 
  else 
    rtn = Convert.ToChar(q + 55).ToString() + rtn; 

  return rtn; 

} 

}

完全未经测试...如果它有效,请告诉我!(复制粘贴以防论坛帖子消失或其他情况...)


关闭。我稍后会试一下。它需要一些工作来能够接受任何字符,但这是朝着正确方向迈出的一步。我将与我的方法进行速度比较! - joshcomley
记得在这里分享,如果您对它进行了改进。其他人可能也会需要它 =) - Svish
@joshcomley 周末过得怎么样?;) - Mikkel R. Lund
3
这是一个漫长的周末 :D - joshcomley

0
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConvertToAnyBase
{
   class Program
    {
        static void Main(string[] args)
        {
            var baseNumber = int.Parse(Console.ReadLine());
            var number = int.Parse(Console.ReadLine());
            string conversion = "";


            while(number!=0)
            {

                conversion += Convert.ToString(number % baseNumber);
                number = number / baseNumber;
            }
            var conversion2 = conversion.ToArray().Reverse();
            Console.WriteLine(string.Join("", conversion2));


       }
    }
}

这是针对从1到10的基本数字。 - Martin Dimitrov

0

我也在寻找一种快速将十进制数转换为另一种[2..36]范围内的基数的方法,因此我开发了以下代码。它很容易理解,并使用Stringbuilder对象作为字符缓冲区的代理,我们可以逐个字符地索引。与其他替代方案相比,该代码似乎非常快,并且比初始化字符数组中的单个字符要快得多。

对于您自己的使用,您可能更喜欢: 1/ 返回空字符串而不是抛出异常。 2/ 删除基数检查以使方法运行得更快 3/ 使用32个“0”初始化Stringbuilder对象并删除结果行。Remove(0, i);。这将导致字符串带有前导零并进一步增加速度。 4/ 将Stringbuilder对象设置为类中的静态字段,因此无论调用DecimalToBase方法多少次,Stringbuilder对象都只会初始化一次。如果您这样做,上面的第3个更改将不再起作用。

希望有人会发现这有用 :)

AtomicParadox

        static string DecimalToBase(int number, int radix)
    {
        // Check that the radix is between 2 and 36 inclusive
        if ( radix < 2 || radix > 36 )
            throw new ArgumentException("ConvertToBase(int number, int radix) - Radix must be between 2 and 36.");

        // Create a buffer large enough to hold the largest int value represented in binary digits 
        StringBuilder result = new StringBuilder("                                ");  // 32 spaces

        // The base conversion calculates the digits in reverse order so use
        // an index to point to the last unused space in our buffer
        int i = 32; 

        // Convert the number to the new base
        do
        {
            int remainder = number % radix;
            number = number / radix;
            if(remainder <= 9)
                result[--i] = (char)(remainder + '0');  // Converts [0..9] to ASCII ['0'..'9']
            else
                result[--i] = (char)(remainder + '7');  // Converts [10..36] to ASCII ['A'..'Z']
        } while ( number > 0 );

        // Remove the unwanted padding from the front of our buffer and return the result
        // Note i points to the last unused character in our buffer
        result.Remove( 0, i );
        return (result.ToString());
    }

0

这是一种相当直接的方法,但可能不是最快的。它非常强大,因为它是可组合的。

public static IEnumerable<int> ToBase(this int x, int b)
{
    IEnumerable<int> ToBaseReverse()
    {
        if (x == 0)
        {
            yield return 0;
            yield break;
        }
        int z = x;
        while (z > 0)
        {
            yield return z % b;
            z = z / b;
        }
    }

    return ToBaseReverse().Reverse();
}

将此与此简单的扩展方法相结合,任何获取任何基数现在都是可能的:

public static string ToBase(this int number, string digits) =>
    String.Concat(number.ToBase(digits.Length).Select(x => digits[x]));

它可以像这样使用:

var result = 23.ToBase("01");
var result2 = 23.ToBase("012X");

Console.WriteLine(result);
Console.WriteLine(result2);

输出结果为:

10111
11X

-1

我使用这个来将Guid存储为较短的字符串(但限制为使用106个字符)。 如果有人感兴趣,这是我的代码,用于将字符串解码回数值(在这种情况下,我使用了2个ulong作为Guid值,而不是编码Int128(因为我在3.5而不是4.0)。 为了清晰起见,CODE是一个具有106个唯一字符的字符串常量。ConvertLongsToBytes非常无聊。

private static Guid B106ToGuid(string pStr)
    {
        try
        {
            ulong tMutl = 1, tL1 = 0, tL2 = 0, targetBase = (ulong)CODE.Length;
            for (int i = 0; i < pStr.Length / 2; i++)
            {
                tL1 += (ulong)CODE.IndexOf(pStr[i]) * tMutl;
                tL2 += (ulong)CODE.IndexOf(pStr[pStr.Length / 2 + i]) * tMutl;
                tMutl *= targetBase;
            }
            return new Guid(ConvertLongsToBytes(tL1, tL2));
        }
        catch (Exception ex)
        {
            throw new Exception("B106ToGuid failed to convert string to Guid", ex);
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接