经典算法的再优化
学习过嵌入式系统的人,应该大都学习过一个嵌入式操作系统UCOS,在这个系统中,有一个经典的O(1)复杂度的调度算法。该算法用于从就绪的任务中找到优先级最高的就绪任务,然后把执行的权利交给这个线程。关于该算法的细节有兴趣的可以去阅读嵌入式操作系统的相关知识.下面我们把这个问题抽象成一个简单的算法问题来描述。
问题的本质:求一个8bit的BYTE形变量的bit位中,从低到高第一个Bit值为1的位置。
简单的介绍一个样例:
一个 8 Bit的 二进制数表示为:0 1 0 1 0 1 0 0
其第一个值为1的Bit位置为2,计数方式从右往左(从低位到高位),位置从0到7
这就是UCOS调度算法的核心调度本质问题,在UCOS的调度算法中,当其中一个Bit为1的时候,代表其Bit代表的优先级任务在就绪中,当其Bit为0的时候,代表该优先级的任务未就绪。UCOS的调度算法,就是要找到就绪任务中优先级最高的任务。
在UCOS的解决方案中,其采用了一种逆向表的方法,在一个表中,存储了从1到255的数对应的每个数的从最低位开始的第一个出现1的Bit的位置。
#这只是简单的示例代码,与源代码不同,逆向表未完全写出。
static unsigned char reverse_list[256]={255, 0, 1, 0, 2, .........} #第一个为255是为了标识值为0的输入无法得到第一个Bit为1的位置,因为0对应的2进制中没有一个Bit为1
unsigned char getAns(unsigned char input) {
return reverse_list[input];
}
逆向表的方法会耗费256个BYTE的内存,而且这种方法不能推广到16Bit变量的求解中,因为对16Bit的变量采用逆向表的方法求解的话,会浪费极大的内存空间。
下面介绍一种优化的求解方法,可以同样保证O(1)的复杂度同时不会浪费内存空间,而且可以推广到16位,32位等的同类问题求解。
个人的经验告诉我,任何一个算法,如果要高效的求解某些问题,必须充分的利用信息,如果无法充分的利用信息,其效率不太可能是最高。
对于该问题而言,求解 0 1 0 1 0 1 0 0 这个数的从低位到高位,第一个出现1的位置这个问题,逆向表没有充分的利用数字中的信息。就以 0 1 0 1 0 1 0 0
这个数而言,其第2位左边的所有bit的值,其实都不能对求解的结果造成影响,但是在逆向表中却利用了这些信息,我们需要想办法把这些冗余的信息去除掉。
无用信息 → 【0 1 0 1 0】【1 0 0】← 有用信息
考虑这样一种运算:
X ⊕ (X - 1)
⊕指异或运算,以 0 1 0 1 0 1 0 0 为例:
0 1 0 1 0 1 0 0
⊕ 0 1 0 1 0 0 1 1
________________
0 0 0 0 0 1 1 1
你应该可以从上面的计算出看出其巧妙之处,X ⊕ (X - 1) 运算将X第一位为1的左边的位置都变成了0,右边变成了1,相当于去掉了很多的冗余信息,做了一次
简单的映射,所以对于从1到255的每个数都会将他们转换成【00000001,00000011,00000111,00001111,00011111,00111111,01111111,11111111】这8个
数中的某一个。
将这8个数对13取余(13这个数也是用程序搜索出来的,这是一种散列的方式), 其结果为
【1, 3, 7, 2, 5, 11, 10, 8】
通过对13取余这种方法,把这8个数散列到了 0 到 13 中 8 个互不重叠的位置,这样就可以用14个BYTE的逆向表来存储信息。
#这只是简单的示例代码,与源代码不同。
static unsigned char reverse_list[14]={255, 0, 3, 1, 255, 4, 255, 2, 7, 255, 6, 5, 255, 255} #255代表该位为无效值,不使用。
unsigned char getAns(unsigned char input) {
unsigned char tmp = input ^ (input - 1);
return reverse_list[tmp % 13];
}