【问题标题】:C base conversion from decimal to ternary从十进制到三进制的 C 基础转换
【发布时间】:2018-10-14 21:10:05
【问题描述】:

我正在将一个整数值转换为一个三进制数(它是一个字符串)。我怀疑应该有更好的方法来减少计算量。

我的算法直接实现了长除法,将数字在十进制和任意基数之间转换。

char *get_ternary_str(int value) {
  char buffer[3] = { "000" };
  int dividend = value;

  int i = 0;
  do {
    int remainder = dividend % 3;
    dividend = dividend / 3;
    buffer[i] = ((char)remainder + '0');
    i++;
  } while(dividend >= 1 && i < 3);

  char temp = buffer[2];
  buffer[2] = buffer[0];
  buffer[0] = temp;

  return buffer;
}

即使在这个例子中三进制数是 3 位数长,我的目标是让它长 n 位数。也欢迎提出如何对 n 位三进制数执行此操作的建议。

【问题讨论】:

  • 您展示的函数存在三个主要问题:首先是您忘记了 C 中的 char 字符串实际上称为 null-terminated 字节串。 null-terminated 位很重要,这意味着三个字符的字符串需要四个空间来包含终止符。第二个问题与前一个问题有关,因为您不会终止字符串。第三个问题是你返回一个指向buffer的第一个元素的指针,一旦函数返回buffer超出范围并实际上不复存在,留下一个杂散和无效的指针
  • @Someprogrammerdude:我认为前两个问题不是真正的问题,只要 char 数组用作 char 数组而不是 cstring(对元素而不是整个数组的操作)。
  • @CoryKramer:几乎没有其他语言像 C 和 C++ 这样在许多层面上紧密结合在一起。对我来说,他要求任何一种语言的解决方案都是完全可以的。此外,将 C 解决方案转换为 C++ 通常很简单。
  • @ErikAlapää 见this meta discussion。这两种语言在某种程度上是兼容的,但具有完全不同的库特性、习语、模式等。因此,在这两种语言中寻求答案是没有意义的,因为正确的解决方案应该只针对其中一种语言。
  • @ThomasSablik 来自问题:“我正在将整数值转换为三进制数(这是一个 String)”(强调我的)。这似乎表明返回的指针将用作字符串。

标签: c string algorithm math


【解决方案1】:
  • 您应该使用 malloc 动态分配缓冲区。
  • 您可以使用 memset 将 char 数组初始化为零。
  • 最后,您可以用数学方法计算 char 数组的长度。以 Log [value] base 3 为基础的上限将为您提供字符串的字符大小。请记住添加一个额外的字符来使用 null。

由于您要求建议,我不提供代码。但是,如果您需要进一步的帮助,您可以在评论中指出。

【讨论】:

  • log() 在数学中听起来不错,但在代码中并不总是如此。当value 为0 或负数时,log(value) 有问题。 log()/log(3) 也有 3 次幂的问题,其中除法不准确且刚好低于预期的整数值。建议不要使用log()
  • 试试for (int i=1; i&lt;=INT_MAX/3; i *= 3) { printf("%.17e\n", log(i)/log(3)); } 看看意外的商数。
  • 亲爱的@chux,对于 (int i=1; i
  • 很公平:使用ceil() 是一个很好的步骤。对于一些 int value 和各种 FP 问题,ceil(log(i)/log(3)) 可能会不时提供一个过高的字符串大小预计算。当然,如果它用于malloc(),或者定义一个 VLA,一个额外的字节很少会受到伤害。如果用于驱动代码的其他部分,+1 可能会使代码失败。我发现,处理本质上整数问题的 FP 细微差别会增加代码移植和维护成本。
【解决方案2】:

代码有问题。

返回指向局部变量的指针

返回指向本地(非静态)变量的指针是未定义的行为 (UB)。不要依赖该内存在函数完成后有效。

char *get_ternary_str(int value) {
  char buffer[3] = ...
  ...
  return buffer;  // UB
}

以负数失败

-1 % 3 --> -1,所以remainder + '0'不会生成数字字符。

int remainder = dividend % 3;
...
buffer[i] = ((char)remainder + '0');

空间不足

buffer[3] 对于"000"字符串 来说太小了,因为null 字符 需要空格。 buffer[3] 足以容纳 3 位字符的数组,但肯定需要 字符串

// char buffer[3] = { "000" };
char buffer[3+1] = { "000" };

从未分配空字符

当然,结果应该是一个 字符串,它需要一个 空字符 '\0'。需要修改代码以确保这一点。


我的目标是让它长 n 位数
也欢迎提出如何对 n 位三进制数执行此操作的建议。

需要一个char 缓冲区来处理形成的字符串。一个简单的解决方案是传入一个足够大的缓冲区指针以应对最坏的情况。我建议也传递缓冲区的大小。

以下是处理任何基数 2-36 的解决方案。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

// Maximum buffer size (worst case: INT_MIN and base 2)
#define ITOA_BASE_N (1 + sizeof(int)*CHAR_BIT + 1)

char *itoa_base(char *dest, size_t sz, int i, unsigned base) {
  char buf[ITOA_BASE_N];
  char *s = &buf[sizeof buf - 1];  // set to last character.
  *s = '\0';
  unsigned u = (unsigned) i;  // Use unsigned to cope with INT_MIN ...
  if (i < 0) {
    u = -u;                   // ... as -INT_MIN is UB
  }
  do {
    *(--s) = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"[u % base];
    u /= base;
  } while (u);
  if (i < 0) {
    *(--s) = '-';
  }
  size_t size_used = &buf[sizeof buf] - s;
  if (size_used > sz) {
    return NULL;  // or some other error handling.
  }
  return memcpy(dest, s, size_used);
}

测试使用复合文字进行临时存储,但如果需要,可以传入固定缓冲区。

#define TO_BASE3(x) itoa_base((char [ITOA_BASE_N]){0}, ITOA_BASE_N, (x), 3)

void test3(int x) {
  char *s = TO_BASE3(x);
  // do stuff with `s`, it is valid for until the end of this block
  printf("base10:%11d base3:'%s'\n", x, s);
}

int main(void) {
  test3(0);
  test3(1);
  test3(42);
  test3(-81);
  test3(INT_MAX);
  test3(INT_MIN);
}

输出

base10:          0 base3:'0'
base10:          1 base3:'1'
base10:         42 base3:'1120'
base10:        -81 base3:'-10000'
base10: 2147483647 base3:'12112122212110202101'
base10:-2147483648 base3:'-12112122212110202102'

【讨论】:

  • 啊,谢谢,我总是忘记-INT_MIN &gt; INT_MAX
  • @Bob__: -INT_MIN 可能有未定义的行为。 INT_MIN &lt; -INT_MAX 在 2 的补码架构上已定义且为真。
  • @chqrlie 评论写得不好。我的意思是INT_MIN的绝对(数字,而不是int表示)大于INT_MAX,所以我同意UB。
  • @Bob__ 是的,I feel your pain。类似文本的代码具有这种不幸的效果。现在,如果编译器只能有一个 do_what_i_want_not_what_i_code(code); 函数 .... ;-)
  • @chux 我赞成你的回答。我会让它成为正确答案,但我认为你错过了一些东西:未受影响数字的前导零。您认为以全 0 开始缓冲区以实现此目的是一个好习惯吗?是否有任何技巧或有效的方法可以在不填充循环的情况下做到这一点?谢谢。
【解决方案3】:

如果您担心,您可以使用一些内存空间来换取计算时间,使用一个简单而小的查找表。

考虑这个 C++(你的问题的早期版本也要求用该语言提供想法)sn-p 作为我提议的算法的一个例子。

#include <iostream>
#include <array>
#include <vector>
#include <string>
#include <iomanip>
#include <algorithm>
#include <limits>

std::string ternary_from(int value)
{ 
    // The idea is to elaborate three figures (in base 3) at a time
    constexpr int dim = 3 * 3 * 3;

    // Note the values, are "reversed"
    static const std::array<std::string, dim> inner_parts {
        "000", "100", "200", "010", "110", "210", "020", "120", "220", 
        "001", "101", "201", "011", "111", "211", "021", "121", "221", 
        "002", "102", "202", "012", "112", "212", "022", "122", "222" 
    };

    static const std::array<std::string, dim> parts {
        "", "1", "2", "01", "11", "21", "02", "12", "22", 
        "001", "101", "201", "011", "111", "211", "021", "121", "221", 
        "002", "102", "202", "012", "112", "212", "022", "122", "222" 
    };

    if ( value == 0 )
        return std::string{"0"};

    std::string result;

    // Thanks @Chux for recalling that -INT_MIN is UB
    unsigned tmp = value;
    if ( value < 0 )
        tmp = -tmp;

    // note that 'dim' = 27, so you are performing a third of the calculations
    while (tmp >= dim)
    {
        unsigned remainder = tmp % dim;
        // concatenating a string at the end is easier...
        result += inner_parts[remainder];
        tmp = tmp / dim;
    }
    result += parts[tmp];

    if (value < 0)
        result += '-';

    // now the string needs to be reversed. e.g. 3 -> 10(3), not 01 
    std::reverse(result.begin(), result.end());
    return result;
}

int main()
{
    std::vector<int> tests {
        42, -8, 0, 81, 27, -28, std::numeric_limits<int>::max(), std::numeric_limits<int>::min()
    };

    std::cout << "   decimal           ternary\n";
    for (int i : tests)
    {
        std::cout << std::setw(12) << i << std::setw(23) << ternary_from(i) << '\n';
    }
}

输出

 十进制 三进制
          42 1120
          -8 -22
           0 0
          81 10000
          27 1000
         -28 -1001
  2147483647 12112122212110202101
 -2147483648 -12112122212110202102

【讨论】:

    【解决方案4】:

    我在main 函数中分配动态内存并将指针传递给该函数。这样我可以管理内存。对于每个分配,您都需要重新分配。我为字符串终止分配了超过字符串长度的 1 个元素。你的算法几乎没有变化。我更改了缓冲区的索引以避免重新排序缓冲区元素。

    #include<stdio.h>
    #include<stdlib.h>
    
    void get_ternary_str(char* buffer, int value, unsigned int n) {
      int dividend = value;
    
      int i = 0;
      do {
        int remainder = dividend % 3;
        dividend = dividend / 3;
        buffer[n - 1 - i] = ((char)remainder + '0');
        i++;
      } while(i < n);
    
      buffer[i] = 0;
    }
    
    int main() {
        int n = 5;
        char* buffer = (char*) malloc(n * sizeof(char));
    
        int value = 31;
        get_ternary_str(buffer, value, n - 1);
    
        printf("%s", buffer);
        free(buffer);
        return 0;
    }
    

    根据 Y.Doktur 的想法,这里是具有动态长度的相同代码。我使用指针来传递和接收指针。

    #include <stdio.h>
    #include <stdlib.h>
    #include <math.h>
    
    char* get_ternary_str(unsigned int value) {
      int n = value > 2 ? 1 + floor(log(value) / log(3)) : 1;
      char* buffer = (char*) malloc((n+1) * sizeof(char));
      int dividend = value;
    
      int i = 0;
      do {
        int remainder = dividend % 3;
        dividend = dividend / 3;
        buffer[n - 1 - i] = ((char)remainder + '0');
        i++;
      } while(i < n);
    
      buffer[i] = 0;
      return buffer;
    }
    
    int main() {
    
      int value = 31;
      char* buffer = get_ternary_str(value);
    
      printf("%s", buffer);
      free(buffer);
      return 0;
    }
    

    【讨论】:

    • 您能否详细说明为什么使用指向接收字符缓冲区的双指针,因为它是一维数组?为什么我们不能在函数内部进行分配并将指针传递给它的调用函数呢?谢谢!
    • 我在答案中修复了它
    • log(value)value 为0 或负数时出现问题。
    • @chux:谢谢。我修好了。
    • 试试this 看看你是否总能得到预期的商数。使用log() 确定log3() 容易出现计算意外。 (即错误答案)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-11-18
    • 1970-01-01
    • 2011-02-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多