概述
问候,
介绍本周的技巧介绍了一些几乎被大多数人遗忘的旧技巧
这里的人。 有时不再需要这些技巧,因为
如今的处理器是如此之快,而且内存也很多。 但是,如果
我们实现了一种比另一种算法更好或更有效的算法,
这些更快的处理器比其他算法运行第一种算法的速度更快。 如果
一个算法需要更少的内存来运行,我们可以运行更大的问题实例
解决我们更大内存的计算机。
更大和更快的计算机仍然不是实现慢速和/或内存的借口
猪算法。 接下来的段落描述了两个小问题,
解决问题的方式。 第一段涉及速度,其次
本段处理内存需求而不牺牲速度需求。
位计数由于多种原因,人们想知道一个字节中设置了多少位
或字。 以下是一种简单的方法来计算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字节隐式表
然后看起来像这样:
- count1:步骤:n
- count2:步数:平均n-1
- count3:步数:平均n / 2
- count4:步骤:n / 8 + 256字节显式表
- count5:步骤:2log(n)+ n / 8 * 2 * 2log(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
最后
以上就是开心吐司为你收集整理的老把戏的全部内容,希望文章能够帮你解决老把戏所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复