找到char变量中唯一'1'位的索引的最有效方法(在C中)

时间:2017-11-16 01:14:13

标签: c algorithm performance bit-manipulation bits

这是一个面试问题:
您将获得一个名为ch的char变量,当您知道它表示一个二进制形式的数字时,它的八位中只有一位将等于'1'。 I.E. ,ch唯一可能的值是:0x1, 0x2, 0x4, 0x8, 0x10, 0x20, 0x40, 0x80 给定变量ch,我需要编写最有效的代码来获得'1'位的索引。例如:if ch == 0x1 - >结果为0.如果ch == 0x4 - >结果是2.

显而易见的方法是使用switch-case,但我需要更高效的东西 为了有效实施,你可以在这里做任何操作吗?

11 个答案:

答案 0 :(得分:3)

unsigned char变量应该只有8位宽。为了编码该位的位置,我们只需要3位。这意味着我们可以构建一个24位的“表”,其中包含按自然顺序排列的所有8个可能的3位答案

111 110 101 100 011 010 001 000 =

0xFAC688

如果已知变量ch只包含一个1位,那么它是2的幂。将某些内容除以ch会将原始值右移右移您的1位。因此,如果我们将上述“表格”除以ch 三次,则答案将转移到结果的最低3位

unsigned position = (0xFAC688 / ch / ch / ch) & 0x7;

故事结束。可以更有效地重写上述内容,同时保留一般原则。

请注意,这与基于De Bruijn序列的方法中使用的原理基本相同。但是,De Bruijn序列的目的是在原始的“unpacked”表(如上面的表格)不适合整数的情况下打包索引表。作为一种“令人不快”的副作用,De Bruijn序列重新排序索引表,打破了原始的自然序列索引。这需要额外的重新映射工作,以从De Bruijn序列中提取正确的结果。

只有24位我们这里没有这个问题,这意味着没有必要涉及De Bruijn及其相应的技巧。

另一方面,打包表需要较短的移位,这将简化(并因此优化)除数的计算以实现所需的移位长度。在De Bruijn序列的情况下,根本不需要计算除数 - 你的ch已经存在了。因此,De Bruijn序列可能很容易变得更有效率。

答案 1 :(得分:2)

好吧,如果ch设置了一个比特,那么ch-1中1比特的计数就是该比特的索引。理想情况下,你想要在没有循环或分支的情况下找到它,因为分支是昂贵的,所以我写下这样的东西:

int index = ((unsigned char)ch)-1;
index = ((index & 0xAA)>>1)+(index & 0x55);  //sums of pairs of bits
index = ((index & 0xCC)>>2)+(index & 0x33);  //sums of 4s of bits
index = ((index & 0xF0)>>4)+(index & 0x0F);  //sum of 8 bits

还有一个非常聪明的答案,使用较少的操作,代价是乘法和查找:

int index = indexMap[((((int)(unsigned char)ch)*DEBRUIJN)>>16)&7];

DEBRUIJN中的位必须是De Bruijn序列(https://en.wikipedia.org/wiki/De_Bruijn_sequence),确保查找索引对于ch的每个值都不同。 indexMap将这些查找索引映射到您想要的结果。

另请注意,在@ rici的评论之后,indexMap非常小,您可以将其打包到单个int中。

答案 2 :(得分:2)

类型char可以是有符号或无符号(实现定义的行为)。为了安全地操作值0x80,我们应该使用unsigned char数据明确操作。

我假设没有可用的特殊函数或多或少地直接给出位位置,例如ffs()(查找第一组),clz()(计数前导零)或{{ 1}}(人口数),我们只使用标准ISO C确定位位置。

一种方法是将popcount()中的每个位位置扩展到单独的半字节(四位组),然后执行寄存器表查找,其中每个表元素包含一个32位的半字节ch

可以通过将输入平方两次来实现扩展,这将位[i]移动到位[4 * i]。然后,下面的代码使用特殊技巧来允许使用乘法和右移提取表元素,其中乘法将所需的表条目移动到中间结果的位[31:28]。请注意,表以可读的方式指定,等同于常量int,每个合理的编译器都会替换。

编译器资源管理器(Godbolt)shows 0x01234567的大部分执行时间成本是三个相关的整数乘法加上一些其他指令。

此代码假定为8位uchar_bitpos()和32位char。为了获得更好的可移植性,可以将int个变量转换为unsigned char个变量,将uint8_t个变量转换为unsigned int个变量。

uint32_t

上述程序的输出应如下所示:

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

int uchar_bitpos (unsigned char ch)
{
    unsigned int ch_pow2, ch_pow4;
    const unsigned int table =
        ((0 << 28) | (1 << 24) | (2 << 20) | (3 << 16) | 
         (4 << 12) | (5 <<  8) | (6 <<  4) | (7 <<  0));
    ch_pow2 = ch * ch;
    ch_pow4 = ch_pow2 * ch_pow2;
    return (ch_pow4 * table) >> 28;
}

int main (void)
{
    unsigned char a = 0x80;
    do {
        printf ("a = %2x   bitpos=%d\n", a, uchar_bitpos (a));
        a = a / 2;
    } while (a);
    return EXIT_SUCCESS;
}

答案 3 :(得分:2)

  

编写最有效的代码来获取该'1'位的索引。

最有效的代码是将ch的值映射到其位索引,即:

0x01 -> 0
0x02 -> 1
0x04 -> 2
0x08 -> 3
...

朴素地图表

最简单和天真的解决方案需要在映射表中查找所有可能的ch值。对于8位数字(char),我们需要一个包含2 8 = 256个元素的表:

char naive_table[256];

naive_table[0x01] = 0;
naive_table[0x02] = 1;
naive_table[0x04] = 2;
naive_table[0x08] = 3;
naive_table[0x10] = 4;
naive_table[0x20] = 5;
naive_table[0x40] = 6;
naive_table[0x80] = 7;

此表中的查找也很简单:

index = naive_table[ch];

哈希函数+映射表

之前的解决方案简单而快速,但naive_table的大部分元素都被浪费了。考虑到ch是2的幂,对于任何n - 位数,只有n个可能的索引。

因此,我们可以使用一个只有8个元素的表和一个将ch的值映射到唯一的哈希函数,而不是使用带有2个 8 元素的映射表。映射表的索引。

这种散列函数的完美候选者将是使用de Bruijn序列的函数。有一篇论文"Using de Bruijn Sequences to Index a 1 in a Computer Word"声明:

  

length-n de Bruijn序列,其中n是2的精确幂,是n 0和1的循环序列,使得每个长度为lg n的0-1序列发生恰好是一个连续的子串。

     

例如,长度为8 de Bruijn的序列为00011101.每个3位数字恰好作为一个连续的子字符串出现一次:从最左边的3位开始,一次向右移动一个3位窗口,我们有000,001,011,111,110,101,010(环绕),100(也缠绕)。

     

哈希函数的计算公式为:h(x)=(x * deBruijn)&gt;&gt;(n - lg n)

因此,让我们尝试使用此哈希函数在紧凑查找表中获取唯一索引:

h(ch) = ((ch * 00011101b) >> (8 - 3)) & 0x7
h(ch) = ((ch * 29) >> 5) & 0x7

让我们计算ch的所有值的哈希值,并确保哈希函数按预期工作,即所有哈希值都是唯一的:

ch    h(ch)
0x01  ((1 * 29) >> 5) & 0x7 = 0
0x02  ((2 * 29) >> 5) & 0x7 = 1
0x04  ((4 * 29) >> 5) & 0x7 = 3
0x08  ((8 * 29) >> 5) & 0x7 = 7
0x10  ((16 * 29) >> 5) & 0x7 = 6
0x20  ((32 * 29) >> 5) & 0x7 = 5
0x40  ((64 * 29) >> 5) & 0x7 = 2
0x80  ((64 * 29) >> 5) & 0x7 = 4

因此散列函数工作正常,并为ch的两个值的每个幂产生唯一的散列。

现在让我们使用上表中的哈希值创建一个紧凑的映射表:

char compact_table[8];

compact_table[0] = 0;
compact_table[1] = 1;
compact_table[3] = 2;
compact_table[7] = 3;
compact_table[6] = 4;
compact_table[5] = 5;
compact_table[2] = 6;
compact_table[4] = 7;

现在查找我们使用哈希值作为索引:

h = ((ch * 29) >> 5) & 0x7;
index = compact_table[h];

哈希函数+位串

以前的版本几乎是完美的:映射表中不再有浪费的元素。但由于所有索引都在0-7之内(即只有3位值),因此仍有改进的余地。让我们使用位字符串而不是映射表,这样就不会浪费每个元素的最高位。

首先,让我们使用ch的所有值和先前版本的哈希值创建这样的位字符串:

ch    h(sh)  index
0x01  0      0 (000b)
0x02  1      1 (001b)
0x04  3      2 (010b)
0x08  7      3 (011b)
0x10  6      4 (100b)
0x20  5      5 (101b)
0x40  2      6 (110b)
0x80  4      7 (111b)

现在让我们按哈希值命令此表:

ch    h(sh)  index
0x01  0      0 (000b)
0x02  1      1 (001b)
0x40  2      6 (110b)
0x04  3      2 (010b)
0x80  4      7 (111b)
0x20  5      5 (101b)
0x10  6      4 (100b)
0x08  7      3 (011b)

因此位串将是这些3位索引的反向串联:

011 100 101 111 010 110 001 000 = 0x72f588

现在让我们像以前一样在这个位字符串中查找。请注意,我们的索引是3位的,因此我们需要将哈希值乘以3:

h = ((ch * 29) >> 5) & 0x7; // just like before
bit_string = 0x72f588;
index = (bit_string >> (h * 3)) & 0x7;

或简而言之:

index = (0x72f588 >> ((((ch * 29) >> 5) & 0x7) * 3)) & 0x7;

代码中没有分区/模/条件,所以它应该在任何CPU上快速执行。

概念代码的证明:

unsigned char ch;
for (ch = 1; ch; ch <<= 1) {
        int index = (0x72f588 >> ((((ch * 29) >> 5) & 7) * 3)) & 7;
        printf("ch = 0x%02x index = %d\n", ch, index);
}
return 0;

答案 4 :(得分:1)

快速且便携的解决方案是:

int charindex(unsigned char c){
    union {   /* Assume both float and int are 32 bits, assume IEEE 754 floating point. */
        int i;
        float f;
    } x;
    x.f = (float)c;
    return (x.i >> 23) - 127;
}

请注意,许多处理器都具有硬件支持,可用于计算前导零或尾随零的数量 一个整数。使用gcc可以轻松访问这些特定指令:gcc具有内置函数__builtin_ctz(),在具有适当硬件支持的平台上可能比charindex更高效。

答案 5 :(得分:0)

有效的代码行数可以是通过位的线性搜索。

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Tooltip, Overlay } from 'react-bootstrap';

const TooltipExample extends Component {
  constructor(props) {
    super(props);

    this.state = {
      showTooltip: false,
    };
  }

  render() {
    let myTooltip = (
      <Tooltip 
        onMouseEnter={() => this.setState({ showTaskTooltip: true })}
        onMouseLeave={() => this.setState({ showTaskTooltip: false })}
      >
        I'm a tooltip and I'll stay open when you leave the target element and hover over me!
      </Tooltip>
    );

    return(
      <div>
        <h3
          ref="target"
          onMouseEnter={() => this.setState({ showTooltip: true })}
          onMouseLeave={() => this.setState({ showTooltip: false })}
        >
          Hover over me!
        </h3>

        <Overlay
          show={this.state.showTooltip}
          onHide={() => this.setState({ showTooltip: false })}
          placement="bottom"
          target={() => ReactDOM.findDOMNode(this.refs.target)}
        >
          {myTooltip}
        </Overlay>
      </div>
    );
  }  
}

export default TooltipExample;

当然,错误检查可能是一个好主意,所以你也可以添加一个检查以确保你仍然有效。

short bit=0;
const char one=1;
while(!((ch >> bit) & one)) ++bit;

它绝对没有计算效率,并且无法检测何时设置了多个位,因此开关盒仍然可能是正确的方法。

这个人在装配中的跳跃比开关盒少,所以可能在计算钻头方面效率更高。

short bit=0;
const char one=1;
while(++bit < 8 && !((ch >> bit) & one)) {}

你也可以跳过检查最后一位,并假设没有其他任何东西匹配它的第7位被设置,这可以保存一个比较。

short bit=
    ch&0x2?1:
    (ch&0x4?2:
    (ch&0x8?3:
    (ch&0x10?4:
    (ch&0x20?5:
    (ch&0x40?6:
    (ch&0x80?7:8))))));

答案 6 :(得分:0)

一些不太高效的方法(取决于您对效率的定义)。

循环和移位方法。

int ch = 32
int i;
for ( i=1;ch >>i ; i++) 
  printf("%i %i \n",i, ch>>i);
printf("Final index:%i\n",i-1);

调用math.h log2

int l=log2((double)ch);
printf("math log2:%i\n",l);

更高效:对于单个查找,可能难以击败AnT的版本。但对于重复查找,查找表可能表现更好。

int ltable[256]= { -1 };

void initTable()
{
  ltable[0x01]=0;
  ltable[0x02]=1;
  ltable[0x04]=2;
  ltable[0x08]=3;
  ltable[0x10]=4;
  ltable[0x20]=5;
  ltable[0x40]=6;
  ltable[0x80]=7;
}

int lookup(size_t ch)
{
  return  ltable[ch];
}

表初始化ASM

init():
  push rbp
  mov rbp, rsp
  mov DWORD PTR ltable[rip+4], 0
  mov DWORD PTR ltable[rip+8], 1
  mov DWORD PTR ltable[rip+16], 2
  mov DWORD PTR ltable[rip+32], 3
  mov DWORD PTR ltable[rip+64], 4
  mov DWORD PTR ltable[rip+128], 5
  mov DWORD PTR ltable[rip+256], 6
  mov DWORD PTR ltable[rip+512], 7
  nop
  pop rbp
  ret

表查找ASM

lookup(unsigned long):
  push rbp
  mov rbp, rsp
  mov QWORD PTR [rbp-8], rdi
  mov rax, QWORD PTR [rbp-8]
  mov eax, DWORD PTR ltable[0+rax*4]
  pop rbp
  ret

输出

 1 16 
 2 8 
 3 4 
 4 2 
 5 1 
 Final index:5
 math log2:5
 Lookup[32]=>5

答案 7 :(得分:0)

您可以在此处使用二进制搜索技术将比较次数从7减少到3.

assert((n & n-1) == 0);
if(n & 0x0F) {
    if(n & 0x03){
        if(n & 0x01){
            idx = 0;
        }
        else{
            idx = 1;
        }
    }else{
        if(n & 0x04){
            idx = 2;
        }
        else{
            idx = 4;
        }
    }
}else{
    if(n & 0x30){
        if(n & 0x10){
            idx = 3;
        }
        else{
            idx = 4;
        }
    }else{
        if(n & 0x40){
            idx = 5;
        }
        else{
            idx = 6;
        }
    }
}

答案 8 :(得分:0)

某些体系结构包含popcount的高效(单指令)实现,可通过内在函数或__builtin_popcount()在C编译器中使用。

如果是这种情况,将很难击败popcount(x - 1),它将首先将单个设置位(1&lt;&lt; n)转换为来自(1&lt;&lt; n-1))1,或当x == 1时为0,然后计算1的数量,这是原始 n 的索引。

有些评论指出“比特扫描转发”,但至少在x86体系结构中不如popcount。永远都知道你的硬...

答案 9 :(得分:0)

如果您只有一位设置为1,则表示它是2的幂。您可以通过log的{​​{1}}直接获取索引。当然,你必须使用2基日志。

答案 10 :(得分:0)

最简单的解决方案可能不是最快的,但只有针对其他解决方案的分析才能确定,并且仅针对给定的体系结构和编译器。

这是一个非常简单的解决方案:

#include <math.h>

int leadingbit(unsigned char c) {
    return log2(c);
}

这是一个带有查找表的解决方案:

int leadingbit(unsigned char c) {
#define N(x) ((076543210 / (x) / (x) / (x)) & 7)
#define N8(x) N(x), N(x+1), N(x+2), N(x+3), N(x+4), N(x+5), N(x+6), N(x+7)
#define N32(x) N8(x), N8(x+8), N8(x+16), N8(x+24)
    static unsigned char table[256] = {
        N32(0), N32(32), N32(64), N32(96), N32(128), N32(160), N32(192), N32(224),
    };
#undef N
#undef N8
#undef N32
    return table[c];
}

这是一个受Matt Timmermans启发而没有记忆参考的人:

int leadingbit(unsigned char c) {
    int n = c - 1;
    n = ((n & 0xAA) >> 1) + (n & 0x55);  //sums of pairs of bits
    n = ((n & 0xCC) >> 2) + (n & 0x33);  //sums of 4s of bits
    return ((n >> 4) + n) & 7;
}

这是一个使用非可移植builtin_clz()函数(计数前导零):

#include <limits.h>

int leadingbit(unsigned char c) {
    return CHAR_BIT * sizeof(unsigned) - 1 - builtin_clz((unsigned)c);
}

请注意,以上所有假设c都是2的幂,其他值的行为可能未定义。您可以使用简单的表达式检查c2的强大功能:

if (c && !(c & (c - 1))) {
    /* c is a power of 2 */
}