跳转至内容
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组
折叠
品牌标识

D2Learn Forums

sky-littlestarS

sky-littlestar

@sky-littlestar
关于
帖子
11
主题
4
群组
0
粉丝
0
关注
1

帖子

最新 最佳 有争议的

  • 分享一种通过二分法快速查找数组中缺失数字的方法
    sky-littlestarS sky-littlestar

    最近在看《编程珠玑》提升自己的C语言编程能力,书中的第二章提出了一个有趣的问题。
    该问题的描述如下:给定一个最多包含40亿个随机排列的32位整数的顺序文件,找出一个不在文件中的32位整数(在文件中至少缺失一个这样的数--为什么?)。在具有足够内存的情况下,如何解决该问题?如果有几个外部的“临时”文件可用,但是仅有几百字节,又该如何解决问题。
    对于为啥文件会确实一个32位的数,这个是显而易见的,我们知道2^32次方是4,294,967,296这个数大于40亿,所以40亿个数是无法完全包含32位整数的全部的。至于,这里有40亿个数,我们怎么找到究竟缺失了哪几个数,这是这个章节考察的重点。我们能不能通过遍历的办法,通过一个基准值然后对比40亿个数,如果能比对成功就证明存在,如果比对不成功,就证明这个数是当前文件所确实的,这个方法当然是可以的,但是在实施是完全不现实的。每比对一个数,就要遍历40亿次(注意,文中指出数据是无序的),所以基本上都要大范围遍历,还要遍历40亿次,这对计算机的性能消耗是巨大的,计算时间也是不可想象的。能不能缩少遍历次数呢,书本提到了位图的方法,通过创建一个大的数组,每个数组的每个位,1代表该数存在,0代表该数不存在,这样,遍历一次即可把位图填满,然后我们再遍历一次位图,看看哪个是0,即可知道究竟是缺失了哪一个数,那这个方法确实可以大大提升计算效率,但是,我们的问题还有一个限制条件,即是内存是有限的,我们很难一次创建一个很大的位图,快速完成遍历。通过理论计算可知,4,294,967,296/8=536,870,912,创建一个完整位图,需要超过500MB的内存,书中最后提出了一个二分搜索的方法。
    位图法的原理如下:
    1、首先我们先创建一个位图(本文以32位为例子说明),随后将其整个位图初始化为0:
    代码如下,比较简单
    首先宏定义,
    #define N 31
    #define BITWORD 8
    uint8_t test_array[N / BITWORD + 1] = { 0 };
    b151ec93-4a90-4a7d-bcb9-8853d330babd-image.png

    随后,我们可以设计位图操作函数,分别是:
    void set_bit(uint8_t* array, uint8_t num);//将位图相关位置置1
    void set_bit(uint8_t* array, uint8_t num)
    {
    array[num >> 3] |= (1 << (num & 0x7));
    }
    void clear_bit(uint8_t* array, uint8_t num);//将位图相关位置置0
    void clear_bit(uint8_t* array, uint8_t num)
    {
    array[num >>3] &= ~(1 << (num & 0x7));
    }
    其中右移动三位是指,将要填入的数字除以8(毕竟我们是8位数组),我们先看看把该数字安放在哪个数组,随后我们num&0x7,这个意思是指求他的余数,即是num%0x7,我们可以计算出,该数字在数组应该右移多少位,为了程序设计方便,我们可以通过宏定义替换。
    #define SHIFT 3
    #define Mask 0x7
    void set_bit(uint8_t* array, uint8_t num)
    {
    array[num >> SHIFT] |= (1 << (num & Mask));
    }
    void clear_bit(uint8_t* array, uint8_t num)
    {
    array[num >> SHIFT] &= ~(1 << (num & Mask));
    }
    我建议做这种除法或取模运算时,尽量运用移位的方法,按位与的方法,这样计算效率会高一些。
    假设我们将数字22所在的位置值换为1,
    360af902-d5cb-4f79-a1b3-7e6e3751b23a-image.png
    首先,我们通过array[num >> SHIFT],求出我们是将array[2]中的某一位置为1,那究竟是哪一位呢?我们通过num&Mask可以得出是array[2]右移动6位置一,因此我们的置一成功。
    接下来,进入到验证部分,
    我设计了一个函数:
    bool test_bit(uint8_t* array, uint8_t num);//通过判断数字的相应位是否为1,判断该数字是否存在,返回值为布尔量
    bool test_bit(uint8_t* array, uint8_t num)
    {
    return array[num >> SHIFT] & (1 << (num & Mask));
    }
    当我们输入22时,该函数会判断array[2]的第6位是否为1,进而判断我们是否写入过该数字。下面我们演示一下运行结果:
    9a15d53c-0915-4ed4-99bb-667e5f6fcdf3-image.png
    5dc8ebc3-ea0c-4109-a8e3-0e8014589167-image.png
    假设我们改为23,
    c3d64efa-2ac7-43ac-9ca6-f1fb303a58eb-image.png
    649ef332-9490-4859-aaf8-575a5d577fc8-image.png
    输出符合预期。源代码如下:
    #define BITWORD 8
    #define SHIFT 3
    #define N 32
    #define Mask 0x7
    void set_bit(uint8_t* array, uint8_t num);
    void clear_bit(uint8_t* array, uint8_t num);
    bool test_bit(uint8_t* array, uint8_t num);
    uint8_t test_array[N / BITWORD + 1] = { 0 };

    int main()
    {
    set_bit(test_array, 22);

    printf("%d\n", test_bit(test_array, 22));
    

    }

    void set_bit(uint8_t* array, uint8_t num)
    {
    array[num >> SHIFT] |= (1 << (num & Mask));
    }

    void clear_bit(uint8_t* array, uint8_t num)
    {
    array[num >> SHIFT] &= ~(1 << (num & Mask));
    }

    bool test_bit(uint8_t* array, uint8_t num)
    {
    return array[num >> SHIFT] & (1 << (num & Mask));
    }
    我们可以通过位图法,读取文件中的数字,将位图相应位置置一,随后通过我们设计的验证函数,看看该数是否存在,如不存在,就可以输出该数据,但正如之前分析,该方法需要的内存呢非常大。
    下面我们介绍一下,该问题的解决方法,二分查找法,为了演示方便,我们假设存在一个数组,里面的数据在0-16之内,即是4位的数据。那么我们应该如何找到4位数据中的,缺失的数字呢?这里简要介绍一下思路,示意图图下
    95c83b61-1c16-48ac-99f7-43411fffd328-image.png
    假设我们有16个数,0000,0001,0010,0011,0100,0101,0110,0111,1000,1001,1010,1011,1100,1101,1110,1111,我们发现,这四位数,高位不是1,就是0,通过划分高位0和1,我们可以产生以上的树状图,比如说,第四位为1时,可以分为1000,1001,1010,1011,1100,1101,1110,1111,第四位为0时,可以分为0000,0001,0010,0011,0100,0101,0110,0111,细心观察,我们可以发现,第四位为1时,在0-15的范围内,可以分为0-7,8-15两个区间,此时,我们可以进一步细分,在8-15的区间内,第三位为1时,区间为12-15,第三位为0时,区间为8-11,在0-7区间内,第三位为1时,区间为4-7,第三位为0时,区间为0-3,依次类推,通过这种二分法,可以快速得到各个区间的数字所在区域。同时,我们可以利用原理,快速排查区间中缺少的数字,我们利用递归法,从原始数据开始进行分类,0-15中,假如第四位为1,没能在数组得到数字时,证明,8-15的区间的数字全部缺失,我们可以快速得到数组中缺失的数字,假如第四位为0没能在数组得到数字时,证明0-7的数字全部缺失,当然,有时候,缺失的数字没有那么多,高四位为1和高四位为0都能找到相应的数字,### (需要注意的是,经过上一次的分类,这时候,我们已经分别得到第四位为1和第四位为0的数字分别构成的两个数组,我们需要在此基础进一步细分查找)###,在0-7的区间内,分别查找第3位为1和第3位为0的数字,假如第三位为1的数字没有找到,证明缺失4-7的数字,假如第3位的数字没有找到,证明缺失0-3的数字,假如他们都有,我们可以将搜索到的数字,分别归为4-7和0-3两个数组,进入下一轮搜索,直到搜索到没有相应的数据,进行返回,依次类推,我们从判断第四位为1或零,一直搜索到第一位是1或0,总能把缺失的数据找出来。
    我们以0-15的数组中,缺少5和缺少11,10为例
    376751ab-e7f6-4998-8bbc-8b1e55f62dff-image.png
    当我们通过二分法,不断细分划分区间时,在{4,5}区间内通过划分第四位为0或1,我们可以找到,我们的数字5是缺失的,通过{8,9,10,11}通过划分第二位为0或1,我们可以找到,10和11是缺失的,一次找出了两个缺失数字,这就是二分法的高明之处。下面,我们演示一下代码运行的结果:
    81212c99-98f7-4d74-b012-ed44d78d1bce-image.png
    创建数组,源数组src_data有{0,1,4,2,6,9,12}7个数字,我们同时创建一个数组,src_data记录缺失的数字,并打印出来,运行结果如下:
    4048e1d2-b55a-44fd-8490-0e87bf2d6b26-image.png
    结果运行正确。下面我们贴出实现代码过程:
    #define MAX_COUNT 16
    #define Mask 0xF
    uint8_t dst_data[MAX_COUNT] = { 0 }; //目标数据(缺失的数字)
    uint8_t src_data[MAX_COUNT] = { 0,1,4,2,6,9 ,12}; //源数据
    uint8_t dst_index = 0;
    void binarysort_array(uint8_t move_bit, uint8_t* src_data, uint8_t start, uint8_t end, uint8_t count);//二分法实现函数

    int main()
    {
    binarysort_array(3, src_data, 0,16,4);
    for (int i = 0; i < dst_index; i++)
    {
    printf("%d ", dst_data[i]);
    }
    }

    void binarysort_array(uint8_t move_bit, uint8_t* src_data, uint8_t start,uint8_t end,uint8_t count)
    {
    uint8_t Bit_zero_array[MAX_COUNT] = { 0 }; //存储Bit位为0的数组
    uint8_t Bit_one_array[MAX_COUNT] = { 0 }; //存储Bit位为1的数组
    uint8_t Bit_zero_index = 0;
    uint8_t Bit_one_index = 0;

    for (int i = 0; i < count; i++)
    {
    	if (((src_data[i] >> move_bit) & 0x1) == 1)
    	{
    		Bit_one_array[Bit_one_index++] = src_data[i];   //将源数据分为两个区间,此为Bit为1的区间,并用Bit_one_index记录其数量
    	}
    	else
    	{
    		Bit_zero_array[Bit_zero_index++] = src_data[i]; //将源数据分为两个区间,此为Bit为0的区间,并用Bit_zero_index记录其数量
    	}
    }
    
    if (Bit_one_index == 0)            //假如Bit为1的数目等于0,证明区间相应的数字缺失
    {
    	for (int i = (start+end)/2; i < end; i++)
    	{
    		dst_data[dst_index++] = i;
    	}
    }
    else if (Bit_one_index != 0 && move_bit > 0)  //将Bit为0的数组送入下一轮筛选
    {
    	binarysort_array( move_bit-1, Bit_one_array, (start+end)/2,end, Bit_one_index);//不断细分区间
    	//8-15        (start+end)/2,end
    	//12-15  //4-7 
    	//14-15  //10-11 //6-7 //2-3
    	
    }
    
    if (Bit_zero_index == 0)              //假如Bit为0的数目等于0,证明区间相应的数字缺失
    {
    	for (int i = start; i < (start+end)/2; i++)
    	{
    		dst_data[dst_index++] = i;
    	}
    }
    else if (Bit_zero_index != 0 && move_bit > 0) //将Bit为1的数组送入下一轮筛选
    {
    	binarysort_array(move_bit - 1, Bit_zero_array, start,(start+end)/2, Bit_zero_index); //不断细分区间
    	//0-7     start,(start+end)/2
    	//0-4  //8-11
    	//0-1  //4-5  //8-9 //12-13  
    }
    

    }
    以上就是我关于该问题的一些个人心得,欢迎各位大佬提出建议,不吝赐教!


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar

    @sunrisepeak UP主可以看看乐鑫董办那个公众号,它的视频号里面有乐鑫科技参加字节火山引擎发布会的视频,算是对AI+潮玩理解比较深的,可能这个是一个切入点,以后这方面的延伸应该很多。


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar

    @sunrisepeak 在 在STM32启动文件中加入版本号 中说:

    @sky-littlestar OTA固件下载是和实际app分开的, OTA固件本身应该只能通过手动升级吧 还是说也能OTA呢 或者 一般app和ota程序都统一视为 app 方式的进行升级更新

    OTA程序放在APP里面,也是可以进行更新的


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar

    最近嵌入式+AI这个领域比较火,UP对这个领域感兴趣吗?有研究过这方面吗?


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar

    @sunrisepeak 但是如果将版本号,直接放在中断向量表的好处是,操作比较简单,而且是在启动文件里,一开机上电,就可以立刻知道版本号,我看过一些案例,由于电路设计的时候运行不稳定,如果开机要初始化太多外设,就很容易死机重启,这时候直接放版本好在这里,对于功耗的需求,也没有那么高


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar

    实际使用中一般会在ROM里面专门划分一个区域存放固件参数,包括版本号,配置特性等等,而且会对参数做一个备份,毕竟Flash的擦除,更新不是原子操作,如果在FLASH的刚好擦除,更新版本号的过程中断电了,那就没有办法知道版本号了,影响到校验工作,设备就变成砖头了,目前比较主流的OTA方法是,一个BOOTLOADER,两个或着3个APP区域,bootloader用于校验或者引导,1个APP区代表当前的运行域,1个APP区代表旧的APP,算是备份,用于安全作用或者回退,还有1个APP区域用于下载新的APP固件,在受到OTA升级指令后,会引导新的APP下载到APP新区域,然后,重启机器,bootloader会检查是否有新的版本号APP区域,如果有,会直接将新的APP区域覆写到APP运行域,覆写完成后,bootloader跳转到APP运行域,然后就运行新的APP程序了


  • 在STM32启动文件中加入版本号
    sky-littlestarS sky-littlestar
     我们平时在单片机上编写程序,生成固件的时候,一般都会在固件里面添加版本号信息,方便下一次修改或者进行OTA升级。目前,写入固件版本号信息主要有以下两种方法,一种是通过使用关键字attribute,在编译阶段,把固件版本号固定到flash中(注意:attribute是GNU编译器特有的,我在MDK上能实现,但是在微软的Visual stido发现是实现不了的)。另外一种方法是,通过在开机后,读写flash,对flash解锁,随后写入相关信息,然后再给flash上锁,完成一次固件信息的固化。但是,今天我想要介绍第三种方法,即是通过在STM32启动文件中,直接在保留的字节里加入版本号信息,方便又快捷,下面我们详细来对比以下四种方法的优劣之处。
    首先,第一种方法,通过关键字attribute在固件中添加相关信息,其具体语法如下,const uint32_t fw_version __attribute((at(address))=XX; 
    由于信息是固化在flash里面,其属于非易失性的ROM,而且我们在程序运行过程中也不会去修改固件标记,所以加上const关键字,fw_version代表你要写进去的版本号,address代表你要写入的地址位置,例如,我要在0x08000010处写入我的版本号位1.0.0,我们可以这样写,const uint32_t fw_version __attribute((at(address))=100;下面我们可以看看运行效果, 我的芯片是STM32-F103ZET6,首先,先看看不烧录固件地址的程序运行状况,方便对比。
    

    9af77c4c-2462-415a-9cda-cf1d5959c921-image.png
    98470f23-0787-4a89-910f-c4afecedb4a6-image.png
    此时程序运行正常,在右侧的flash中,0x08000000是栈顶指针,0x08000004是PC指针,后面依次是启动文件中规定的中断地址,程序没有问题。flash中的中断地址比函数的地址+1是由于Thumb-2指令集要求跳转地址的最低有效位(LSB)必须是1,所以其记录的地址会加1,程序烧录后,能正常运行。此时,我们尝试使用attribute将相关固件信息,固定在指定地址,我们再打开map表和查看flash上的信息,发现上面的地址信息会乱掉,程序启动后,进入死机状态。
    3d1b84c5-3393-494d-8006-673b286d9c3e-image.png
    观察FLASH上的数据我们可以发现,0x08000010处成功地写入100,证明我们的写入是有效的,但是,我们细心观察可以发现,在0x8000000和0x08000000处已经不是程序PC指针跳转地址和栈顶地址了,程序无法正常启动,观察左侧LR寄存器,0xFFFFFFF9进入硬件错误,证明通过这种写法去写入固件信息是不可行的。我们再看看map表,固件信息前面都是一些C的库函数,而中断信息已经不见了,反而放在0x08001000的后面,9792abc0-28fa-42c3-a005-5f78d68e500a-image.png
    由于,在地址的前面处加入,会导致flash上烧录的信息错乱,所以我们只能尝试在flash的后面加入,以致于其不影响其他函数烧录到相应的flash地址,我尝试在0x0800FFF0处写入,
    const uint32_t fw_version __attribute((at(0x0800FFF0)))=100; 此时,程序能正常运行,也成功在固定地址写入,但是我们观察flash可以发现,写入地址前面的数据也受到影响,单片机上的flash在没写入的状态下,一律初始化为FF,现在,都是00,证明使用这个方法,会导致一个扇区的浪费,所以使用attribute+固定地址并不是一个好的方法。
    887d4d5e-9c2b-4b9a-a77f-8f8d7290c1f6-image.png
    我们还可以使用另外一种方法写入固件信息,STM32单片机内部是norflash,所以我们可以按字节去寻址写入,非常方便,而不必像NANDFLASH那样去,先进行页擦除,再进行页写入,整个过程比较耗费时间,对系统的实时性有一定的影响。但是由于FLASH具有只能从1写到0的特性,而不能从1写道0,所以,我们写入相应的区域地址时,依然需要校验当前写入区域是否全为0XFF,如果不是全为0XFF,我们依然需要进行扇区擦除,为之后的写入作准备。下面我们进行简单的演示。
    首先,先进行Flash上面的数据读取,函数实现如下:
    d04305d0-db17-4cc0-889c-b3e88f6efba5-image.png
    将两个8位数据凑成一个半字的数据,然后写入,其中STM32_PAGE_SIZE宏定义为2048,这个根据芯片的FLASH每一页大小确定
    1ae99ffa-d5c8-4771-ac52-e716272257a0-image.png
    d1a1159a-db4a-47d5-b6e2-3b1505fb9baa-image.png
    8112b773-70f2-4955-8fe7-913854edfa9b-image.png
    fd29c9e2-a26a-4cfe-a118-5e1aab8a3ef8-image.png
    最后发现,FLASH上成功写入数据。
    262226dc-a9fa-49ef-98fb-924cb4cb3a4c-image.png
    但是使用该办法,无法利用中断区域没有利用的保留字空间,因为程序运行中,会发生各种中断,如果对中断向量表的区域进行擦写,这是不被允许的,所以虽然使用Flash擦写的方式能够精确到每一个地址,不用造成扇区浪费,但还是无法利用中断向量表的保留字空间,而且,此办法,需要编写相应的FLASH擦写程序,也比较费时费力。
    于是,我们可以利用分散加载技术,通过将我们定义的段固定到指定的地址,指定其大小,随后我们再往段里面写入的相应的数据,我们即可在编译的时候,将数据擦写到固定的位置。这样的方法,比通过程序进行写Flash要简单,也是通过编译时的烧录算法,直接将变量烧写到指定位置,但是由于单片机一开始就已经指定了头部存放的中断变量,跳转地址等信息,没办法在这些空间加入新的段,所以也只能在没有使用过的空间指定地址,但是分段后有一个好处,就是分段后再使用attribute与直接使用attribute相比,可以精准地在相关地址写入变量信息,而不必对扇区的其他位置造成影响,节约了空间。我们可以看一看运行效果。
    86527e46-7527-4b2f-be40-47d0ac3f52ca-image.png
    3bcec958-b8de-4845-9de7-cee3b0abfd99-image.png
    但是以上三种方法依然无法实现我们使用启动文件保留字空间的办法,最后,我请教了硬汉嵌入式论坛的大佬,通过总结发现,中断向量表中,其位置存储的就是函数的执行地址,也即是函数名直接被翻译为地址存储了,那我们不用通过翻译,直接把数字写进去,可不可以呢,我们实践了一下,最后惊喜的发现,是可以的。下面我们可以看一下效果。
    d14bd81c-df5b-45a4-a4fa-df3dcf9a1e6b-image.png
    踏破铁鞋无觅处,得来全不费功夫。虽然大费周章,最后发现仅需要通过在启动文件直接添加相关变量即可,但是这次的研究过程也带来不少收获,希望各位大佬共同研究。


  • 如何使用ImGui中的DrawList制动一个动画控件? - Loading动画控件
    sky-littlestarS sky-littlestar

    @sunrisepeak 在 如何使用ImGui中的DrawList制动一个动画控件? - Loading动画控件 中说:

    根据每一帧到lambda表达式的插值动画数据实现动态的更新滑动块的坐标

    请问这个具体的计算过程的怎样的呢?有相关的参考资料吗?


  • 如何使用ImGui中的DrawList制动一个动画控件? - Loading动画控件
    sky-littlestarS sky-littlestar

    请问在滑块移动的过程中是先把滑块,通过父对象渲染的方式,把父对象还原,然后再通过计算滑块的坐标,重新在父对象上面绘制滑块来实现的吗?(还原父对象->计算滑块坐标->重新绘制滑块形成新的一帧),是这种方法吗?


  • 关于rt-thread task_struct结构体的疑惑
    sky-littlestarS sky-littlestar

    最近在研究rt_thread的源码,发现struct task_struct结构体没有找到定义,但是一些函数却以此作为类型传参数,task_struct的定义应该怎么找,或者说,这是一种什么用法吗?
    dc9862e8d15b83680a6fb04183a292f.png

  • 登录

  • 没有帐号? 注册

  • 登录或注册以进行搜索。
d2learn forums Powered by NodeBB
  • 第一个帖子
    最后一个帖子
0
  • 版块
  • 最新
  • 标签
  • 热门
  • Online Tools
  • 用户
  • 群组