老把戏

问候,

介绍

本周的技巧介绍了一些几乎被大多数人遗忘的旧技巧

这里的人。 有时不再需要这些技巧,因为

如今的处理器是如此之快,而且内存也很多。 但是,如果

我们实现了一种比另一种算法更好或更有效的算法,

这些更快的处理器比其他算法运行第一种算法的速度更快。 如果

一个算法需要更少的内存来运行,我们可以运行更大的问题实例

解决我们更大内存的计算机。

更大和更快的计算机仍然不是实现慢速和/或内存的借口

猪算法。 接下来的段落描述了两个小问题,

解决问题的方式。 第一段涉及速度,其次

本段处理内存需求而不牺牲速度需求。

位计数

由于多种原因,人们想知道一个字节中设置了多少位

或字。 以下是一种简单的方法来计算int中的那些位:


public int count1(int word) {
    int ones= 0; // the number of one-bits in the 'word'
    for (int mask= 0x00000001; mask != 0; mask<<= 1)
        if ((word&mask) != 0) ones++;
    return ones;
} 
这是一种非常简单的方法:我们使用“掩码”检查每一位

变量; 如果单词中的相应位被设置,我们将“ 1”加1

计数器。 循环完成后,该方法将返回“ ones”变量。

无论单词的值如何,循环都会循环32次,因为int包含

32位,都需要检查。

我们可以对此进行一些改进:找到一点后,将其设置为

再次归零。 如果单词变量中没有剩余位,我们就完成了。 这就是

它可以实现:


public int count2(int word) {
    int ones= 0; // the number of one-bits in the 'word'
    for (int mask= 0x00000001; word != 0; mask<<= 1)
        if ((word&mask) != 0) {
            word&= ~mask; // remove the bit
            ones++;
        }
    return ones;
} 
现在,当字中没有更多位设置为1时,循环停止。 计数2

该方法肯定比以前的count1方法更有效。 多少

究竟更有效? 如果我们考虑使用随机的32位模式,则其中的50%

位模式的最左位设置为1(选中)。 因此,在所有

可能的'word'值,count2的运行效率没有方法count1高。

对于2 ^ 31个可能值,count2需要32次迭代,对于2 ^ 30个可能值

count2只需要31次迭代,依此类推,一直到2 ^ 0个可能的值

count2方法需要零次迭代; 这是值0,其中循环体

被完全跳过。

从数学上来说,count2方法平均需要:

sum(i = 0,31:2 ^ i *(i + 1))/ 2 ^ 32 == 31次迭代。

大不了,平均而言,我们只保存一次迭代。 我们必须能够做到

比这更好。 看一下以下概念:

对于每个至少有一位设置为1的位模式,我们可以表示

位模式如下:

xxx ... xxxx1000 ... 0000

1位的左侧为“ xxx”位,右侧为零个或多个0位

1位。

如果从该位模式中减去1,我们将获得位模式:

xxx ... xxxx0111 ... 1111

如果我们对两个位模式进行按位“和”运算,则会得到位模式:

xxx ... xxxx0000 ... 0000

我们已经有效地从原始位模式中删除了最右边的1位。

我们已经做到了:


pattern&= pattern-1; 
我们可以在count3方法中使用此概念:

public int count3(int word) {
    int ones= 0; // the number of one-bits in the 'word'
    for (; word != 0; ones++)
        word&= word-1;
    return ones;
} 
每次循环时,从“单词”中删除最右边的1位,直到“单词”

等于零。 平均而言,在32位字中,有16位设置为1。

count3方法肯定比count1和count2方法要好。 计数2

并没有比count1好得多,但是count3只需要循环遍历的一半。

我们可以做得更好吗? 当然,从数学上来讲,我们可以:我们只需构建

一个具有2 ^ 32个元素的表,如果位模式i包含,则table [i] == j

j位设置为1。当然,数学家是疯子:我们无法建立表或数组

具有2 ^ 32个元素; 即使在今天,我们也需要大量的存储空间。

我们可以为256个字节的值建立这样一个表; 桌子看起来

像这样的东西:


byte[] table= { 0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4 ... 8 }; 
我们可以这样使用该表:

public int count4(int word) {
    int ones;
    for (int byte= 0; byte < 4; byte++) {
        ones+= table[word&0xff];
        word>>>= 8;
    }
    return ones;
} 
此方法仅计算每个字节中的所有一位(有四个字节)

在使用Java的int中),因此该算法只需要进行四次循环遍历。 但

它确实需要一个256字节的表。 如果没有这样的桌子,我们还能做得更好吗?

比count3方法好吗?

我们需要2位来存储2位宽字中的个数。 有

两位宽字中的最多2位等于1,值2需要两位

通过它自己。 我们只需要6位就可以存储[0,32]范围内的值。 最多

32位可以等于32位宽字中的一个。

我们如何找到在两位宽的字中设置为1的位数? 简单:


int word; // just two bits in here, zero or one
int ones= (word&1)+(word>>>1); 
但是我们可以并行使用这个小技巧。 对于32位宽的单词,我们可以

为组成单个32的16个2位字存储16个2位计数器

位词:


int word; // 32 bits wide word interpreted as 16 two bit words;
int ones= (word&0x55555555)+((word&0xaaaaaaaa)>>>1); 
现在,“ ones” int包含代表数字的十六个小的两位值

如果将32位字的值解释为16个2位小字,则设置为1的位。

现在我们要做的就是将这16个2位值加到'ones'int中,

将字int中的位数设置为1。

我们可以使用相同的技巧将16个2位值存储在一个32位字中:

而不是以前的掩码,我们使用0x33333333和0xccccccc掩盖了

16个两位数值,然后将它们再次加在一起; 我们有4位存储和。

但是再一次,我们可以一次又一次地重复这个小程序:


public int count5(int word) {
    word= (word&0x55555555)+((word&0xaaaaaaaa)>>> 1); // sum  1 bits
    word= (word&0x33333333)+((word&0xcccccccc)>>> 2); // sum  2 bits
    word= (word&0x0f0f0f0f)+((word&0xf0f0f0f0)>>> 4); // sum  4 bits;
    word= (word&0x00ff00ff)+((word&0xff00ff00)>>> 8); // sum  8 bits;
    word= (word&0x0000ffff)+((word&oxffff0000)>>>16); // sum 16 bits; 
    return word;
} 
该方法不再使用循环:它使用了五个步骤,并且变相使用了一张桌子; 让我们看看现在有什么:
  • count1:步数:32
  • count2:步骤:平均31
  • count3:步骤:平均16
  • count4:步骤:4 + 256字节显式表
  • count5:步骤:5 + 40字节隐式表
现在,让我们看看当单词为n位宽时会发生什么。 桌子会

然后看起来像这样:

  • count1:步骤:n
  • count2:步数:平均n-1
  • count3:步数:平均n / 2
  • count4:步骤:n / 8 + 256字节显式表
  • count5:步骤:2log(n)+ n / 8 * 2 * 2log(n)字节隐式表
当n越来越大(它将)最后一个算法的内存时

需求与n呈对数增长。 前四种算法不断发展

与n成线性比例; 选择您的选择。

记忆操纵

我们喜欢几乎连续地移动内存; 好像我们做不到

下定决心要将数据存储在什么地方,然后继续拖动这些数据

个字节一遍又一遍。 例如:我写了第一段

我写完第二段之后的这篇文章。

高效地,我将第二段移到记忆的下方,为

第一段。 现在,我将更多数据添加到其余文本中。

假设我有一个存储单元序列; 对于示例,我使用纯文本,但是

为了开发算法,我将使用更抽象的概念:内存

由使用索引号0、1、2,... n索引的单元组成。

我想做的第一件事是交换两个单元格i和j的内容,

因此,此假设内存实现了以下接口:


public interface Swapeable {
    public void swap(int i, int j);
} 
有了这个简单的界面,我可以反转内存块,例如,如果这是

内存内容和两个索引值i和j


+---+---+---+---+---+---+---+---+---+---+---+---+
| H | e | l | l | o |   | W | o | r | l | d | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
              ^                   ^
              |                   |
              i                   j 
下面的方法在索引值i和j之间反转内存的内容:

public void reverse(Swapeable s, int i, int j) { 
    for(; --j > i; i++)
        s.swap(i, j);
} 
然后,内存内容将如下所示:

+---+---+---+---+---+---+---+---+---+---+---+---+
| H | e | l | o | W |   | o | l | r | l | d | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
              ^                   ^
              |                   |
              i                   j 
好吧,大不了; 那就是CS101,(几乎?)每个人都可以做到。 是的,但是有

我们只需使用简单的reverse()方法就可以完成更多操作

意识到这一事实。

假设我反转了这一部分:


+---+---+---+---+---+---+---+---+---+---+---+---+
| H | e | l | l | o |   | W | o | r | l | d | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
  ^                                           ^
  |                                           |
  i                                           j 
结果是:

+---+---+---+---+---+---+---+---+---+---+---+---+
| d | l | r | o | W |   | o | l | l | e | H | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
  ^                                           ^
  |                                           |
  i                                           j 
接下来,我反转这一部分:

+---+---+---+---+---+---+---+---+---+---+---+---+
| d | l | r | o | W |   | o | l | l | e | H | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
  ^                   ^
  |                   |
  i                   j 
结果是:

+---+---+---+---+---+---+---+---+---+---+---+---+
| W | o | r | l | d |   | o | l | l | e | H | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
  ^                   ^
  |                   |
  i                   j 
最后,我反转了这一部分:

+---+---+---+---+---+---+---+---+---+---+---+---+
| W | o | r | l | d |   | o | l | l | e | H | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
                          ^                   ^
                          |                   |
                          i                   j 
这导致:

+---+---+---+---+---+---+---+---+---+---+---+---+
| W | o | r | l | d |   | H | e | l | l | o | ! |
+---+---+---+---+---+---+---+---+---+---+---+---+
                          ^                   ^
                          |                   |
                          i                   j 
我刚刚交换了两个内存块; 这是原始内容

内存的大小:B1 B2 B3其中B1 == Hello,B2 == <space>和B3 == World。 的

结果就是这个B3 B2 B1。 '!' 没有使用或触摸的迹象。 如果区块B2

包含多个元素,我也必须反转该块。

更重要的是:除单个内存外,没有使用其他任何临时内存

swap()方法本身使用的存储单元。 在早期的记忆中

以千字节甚至单字节而不是千兆字节为单位进行测量的小技巧

是一颗珍贵的小宝石,经常使用。 但是,什么使您无法使用它呢?

在下面的伪代码中,我将符号“ b”用于反转的块B。

所需步骤为:

1:反向B1 B2 B3产生b3 b2 b1

2:反向b3产生B3 b2 b1

3:反向b2产生B3 B2 b1

4:反向b1产生B3 B2 B1

这是它的代码:


// assume ibeg <= iend <= jbeg <= jend
public void move(Swapeable s, int ibeg, int iend, int jbeg, int jend) {
    int ilen= iend-ibeg;
    int jlen= jend-jbeg; 
    reverse(s, ibeg, jend);
    reverse(s, ibeg, ibeg+jlen);
    reverse(s, ibeg+jlen, jend-ilen);
    reverse(s, jend-ilen, jend);
} 
我将它留给读者检查该方法中摆弄的所有索引。 这个

方法交换两个内存块; 块不必是

大小相同 第一个区块必须在第二个区块的左侧; 它没有

采用火箭科学来扩展此方法,以便可以定位两个块

内存中的任何位置,即第一个块不必位于左侧

第二块 (提示:写一些包装方法)。

结束语

我在这里描述的这两个技巧是我(旧)技巧包中的骄傲成员

因为它们有时会很好地使用,尽管我确实需要将它们除掉。

也许我会在不久的将来描述更多(老)技巧。 目前,

请了解这两个技巧的细节; 值得的。

我希望我们下周再见面,

亲切的问候,

乔斯

From: https://bytes.com/topic/java/insights/715776-old-tricks

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值