【杂记】从孩子口算题开始逐渐离谱

一、 背景

孩子经常需要做口算题,现在需要做的是100以内的加减法以及乘法口诀的题目,本来是想从外面直接买,但是买的口算题基本上有一小半都不符合条件,要么是10以内的,要么是20以内的,要么是100以内没有进退位的加减法,所以做完想要的题之后会空下特别多的页,做也不想做,扔了还可惜

所以想着自己写个程序随机生成想要的加减法,再打印出来给孩子做,这样就不会感觉浪费了

二、初步实现

初步实现算法没什么好说的,直接随机2个100以内的数字,过滤一些能想到的badcase(10以内的加减法,或者没有进退位的),然后写入到文件中

main.cpp

#include "creater.h"

int main(int argc, char* argv[]) {
    kousuanCreater kousuan("C:\\Users\\yuwen.song\\Desktop\\VS test\\self test\\kousuan.txt");
    kousuan.run();
    return 0;
}

creater.h

#include <iostream>
#include <fstream>
#include <random>
#include <time.h>
using namespace std;

// 口算题类型
enum COLUMN_TYPE : int {
    COLUMN_INVALID = -1,
    COLUMN_ADD = 0,
    COLUMN_MINUS = 1,
    COLUMN_MULTI = 2,
    COLUMN_NUM
};

// 每行设置3列
const int COLUMN_PER_ROW = 3;
// 在word中使用Calibri(body)字体,12号字,每页能放27行
const int MAX_NUMBER_IN_ONE_PAGE = COLUMN_PER_ROW * 27;
// 默认输出10页题
const int MAX_KOUSUAN_NUM = MAX_NUMBER_IN_ONE_PAGE * 10;

class kousuanCreater {
public:
    kousuanCreater();
    kousuanCreater(const string& filename, int max_num = MAX_KOUSUAN_NUM);
    ~kousuanCreater();

public:
    void run();

private:
    int createAdd(int i, int j);
    int createMinus(int i, int j);
    int createMulti(int i, int j);

private:
    ofstream _file;
    int _max_num;
};

creater.cpp

#include <assert.h>
#include "creater.h"

kousuanCreater::kousuanCreater() {
    _file.open("C:\\Users\\yuwen.song\\Desktop\\kousuan.txt", ios::out);
    assert(_file.is_open());
    _max_num = MAX_KOUSUAN_NUM;
}

kousuanCreater::kousuanCreater(const string& filename, int max_num) {
    _file.open(filename, ios::out);
    assert(_file.is_open());
    _max_num = MAX_KOUSUAN_NUM;
}

kousuanCreater::~kousuanCreater() {
    _file.close();
}

void kousuanCreater::run() {
    // 随机初始化
    default_random_engine randEngine(time(0));
    uniform_int_distribution<unsigned > u(5, 100);
    int count = 0;
    int lastCount = 0;
    int num1 = 0;
    int num2 = 0;
    int type = COLUMN_INVALID;
    // 主循环
    while (count < _max_num) {
        num1 = u(randEngine);
        num2 = u(randEngine);
        type = randEngine() % COLUMN_NUM;
        switch (type) {
            case COLUMN_ADD:
                count += createAdd(num1, num2);
                break;
            case COLUMN_MINUS:
                count += createMinus(num1, num2);
                break;
            case COLUMN_MULTI:
                count += createMulti(num1, num2);
                break;
            default:
                fprintf(stderr, "impossible!\n");
                break;
        }

        // 每COLUMN_PER_ROW列换一次行
        if (count % COLUMN_PER_ROW == 0 && count > lastCount) {
            _file << endl;
        }

        // 为了避免出现连续的无效题,导致出现连续的空行,记录之前的个数进行比较
        lastCount = count;
    }
    
    return;
}

// 生成加法
int kousuanCreater::createAdd(int num1, int num2) {
    if (num1 + num2 <= 10) {
        return 0;
    } else if (num1 + num2 > 100) {
        return 0;
    } else if (num1 <= 10 || num2 <= 10 || num1 % 10 == 0 || num2 % 10 == 0) {
        return 0;
    }

    _file << num1 << " + " << num2 << "\t= \t\t\t";
    return 1;
}

// 生成减法
int kousuanCreater::createMinus(int num1, int num2) {
    // 固定将num1设置为更大的数,可以避免后续判断哪个更大放前面
    if (num1 < num2) {
        swap(num1, num2);
    }

    if (num1 < 10 || num2 < 10 || num1 >= 100 || num2 >= 100) {
        return 0;
    } else if (num1 - num2 < 10 || (num1 - num2) % 10 == 0) {
        return 0;
    }

    _file << num1 << " - " << num2 << "\t= \t\t\t";
    return 1;
}

// 生成乘法
int kousuanCreater::createMulti(int num1, int num2) {
    // 将数字限定在1 ~ 9之间
    num1 %= 9 + 1;
    num2 %= 9 + 1;
    if (num1 <= 1 || num2 <= 1) {
        return 0;
    }

    // 固定将num1设置为更大的数,可以避免后续判断哪个更大放前面
    // 为了更好的匹配乘法口诀
    if (num1 > num2) {
        swap(num1, num2);
    }

    _file << num1 << " × " << num2 << "\t= \t\t\t";
    return 1;
}

三、优化

上面的程序写出来后,生成算式时,发现经常同一页上会有重复的题目,所以需要加一个去重的算法。

算法设计是在程序启动时,创建一个MAX_NUMBER_IN_ONE_PAGE大小的数组和hash表来记录,hash表用于快速查找,数组用于记录顺序。在hash表中查找到,则过滤;如果没查找到,则记录到数组中;如果数组已满,则删除数组和hash表中最旧的一个,把当前的插入其中。

修改内容:

creater.h

class kousuanCreater {
...

private:
    int writeStrIntoFile(const string& str);

private:
...
    // 记录过去MAX_NUMBER_IN_ONE_PAGE个数的口算题
    size_t _str_list[MAX_NUMBER_IN_ONE_PAGE] = {0};
    // 记录当前使用到的_str_list下标
    int _list_idx = 0;
    // 记录当前已有的算式,key使用字符串hash后的结果,速度更快
    unordered_set<size_t> _rec;
};

creater.cpp

// 生成加法
int kousuanCreater::createAdd(int num1, int num2) {
	...

    return writeStrIntoFile(to_string(num1) + " + " + to_string(num2) + "\t= \t\t\t");
}

// 生成减法
int kousuanCreater::createMinus(int num1, int num2) {
    ...

    return writeStrIntoFile(to_string(num1) + " - " + to_string(num2) + "\t= \t\t\t");
}

// 生成乘法
int kousuanCreater::createMulti(int num1, int num2) {
    ...

    return writeStrIntoFile(to_string(num1) + " × " + to_string(num2) + "\t= \t\t\t");
}

int kousuanCreater::writeStrIntoFile(const string& str) {
    hash<string> str_hash;
    size_t hash_num = str_hash(str);
    if (_rec.count(hash_num) > 0) {
        return 0;
    }

    _file << str;
    _rec.insert(hash_num);
    if (_str_list[_list_idx] != 0) {
        _rec.erase(_str_list[_list_idx]);
    }

    _str_list[_list_idx] = move(hash_num);
    _list_idx = (_list_idx + 1) % MAX_NUMBER_IN_ONE_PAGE;
    return 1;
}

四、复杂化

在上面的优化中提到,其实是有个类似时序的概念存在的,在MAX_NUMBER_IN_ONE_PAGE个数内,按照出现先后进行数据的淘汰,这样就有点类似LRU的,所以继续使用LRU算法进行优化和剔重
LRU的实现思路为:

数据结构使用一个hash表,一个双向链表,双向链表有一个不保存数据的head和tail节点,方便后续操作
hash表用于快速查询,双向链表用于保存时序,具体的操作为:
数据插入 : 先判断当前数据个数是否到达最大值。如果已经到达最大值,删除tail节点前一个元素,并删除hash表中保存的此节点数据;之后在head节点后插入数据
判断是否存在 :使用hash表判断当前数据是否存在,如果不存在,返回false;如果存在,使用hash表取出此节点,并将此节点移动到head节点后

小tips:

hash表初始化时reserve足够的空间,避免后续重复申请空间导致的性能损耗
hash表的key用算式hash过后的size_t作为key进行保存,因为size_t的比较比string的比较性能快很多,所以虽然增加了hash值计算的逻辑,但是整体性能会有所提升
较短的函数设置为inline函数,能够避免重复创建和销毁函数栈的性能损耗

根据此思路,最终修改的代码为:

main.cpp

#include "creater.h"

int main(int argc, char* argv[]) {
    int start_time = time(0);
    kousuanCreater kousuan("C:\\Users\\yuwen.song\\Desktop\\VS test\\kousuan\\kousuan.txt");
    kousuan.run();
    int end_time = time(0);
    fprintf(stderr, "cost time : [%d]\n", end_time - start_time);
    return 0;
}

creater.h

#include <iostream>
#include <fstream>
#include <random>
#include <time.h>
#include <unordered_set>
#include <assert.h>
#include "lru.h"
using namespace std;

// 口算题类型
enum COLUMN_TYPE : int {
    // 无效值
    COLUMN_INVALID = -1,
    // 加法
    COLUMN_ADD = 0,
    // 减法
    COLUMN_MINUS = 1,
    // 乘法
    COLUMN_MULTI = 2,
    COLUMN_NUM
};

// 每行设置3列
const int COLUMN_PER_ROW = 3;
// 在word中使用Calibri(body)字体,12号字,每页能放27行
const int MAX_NUMBER_IN_ONE_PAGE = COLUMN_PER_ROW * 27;
// 默认输出10页题
const int MAX_KOUSUAN_NUM = MAX_NUMBER_IN_ONE_PAGE * 10;

class kousuanCreater {
public:
    kousuanCreater(const string& filename = "C:\\Users\\yuwen.song\\Desktop\\kousuan.txt", int max_num = MAX_KOUSUAN_NUM) : 
                                    _file(filename, ios::out), 
                                    _max_num(max_num),
                                    lru(MAX_NUMBER_IN_ONE_PAGE),
                                    _count{0} {
        assert(_file.is_open());
    }

    ~kousuanCreater() {
        _file.close();
        for (int idx = 0; idx < COLUMN_NUM; ++idx) {
            cout << getCalcTypeStr(idx) << "有" << _count[idx] << "道题" << ", 占比" << _count[idx] * 100 / _max_num << "%" << endl;
        }
    }

public:
    void run();

private:
    // 生成加法,返回值为本次生成个数,1 : 有效,0 : 无效
    int createAdd(int i, int j);
    // 生成减法,返回值为本次生成个数,1 : 有效,0 : 无效
    int createMinus(int i, int j);
    // 生成乘法口诀,返回值为本次生成个数,1 : 有效,0 : 无效
    int createMulti(int i, int j);

private:
    // 将口算题剔重后写入文件
    inline int writeStrIntoFile(const string& str) {
        if (lru.is_exist(str)) {
            return 0;
        }

        lru.add_node(str);
        _file << str;
        return 1;
    }

    // 加减法通用过滤
    inline bool commonFilterAddAndMinus(int num1, int num2, int type) {
        if (num1 < 10 || num2 < 10) {
            // 小于10
            return false;
        } else if (num1 % 10 == 0 || num2 % 10 == 0) {
            // 整10
            return false;
        } else if (num1 > 100 || num2 > 100) {
            // 大于100
            return false;
        }

        return true;
    }

private:
    inline string getCalcTypeStr(int type) {
        string ret_str = "";
        switch (type) {
            case COLUMN_INVALID:
                ret_str = "无效";
                break;
            case COLUMN_ADD:
                ret_str = "加法";
                break;
            case COLUMN_MINUS:
                ret_str = "减法";
                break;
            case COLUMN_MULTI:
                ret_str = "乘法";
                break;
            default:
                ret_str = "未知";
                break;
        }

        return ret_str;
    }

private:
    // 文件句柄
    ofstream _file;
    // 最大口算题个数
    int _max_num;
    // 使用LRU去重
    LRU lru;
    // 各种算式计数器
    int _count[COLUMN_NUM];
};

creater.cpp

#include "creater.h"
void kousuanCreater::run() {
    // 随机初始化
    default_random_engine randEngine(time(0));
    uniform_int_distribution<unsigned > u(11, 99);
    int count = 0;
    int last_count = 0;
    int num1 = 0;
    int num2 = 0;
    int type = COLUMN_INVALID;
    // 主循环
    while (count < _max_num) {
        num1 = u(randEngine);
        num2 = u(randEngine);
        type = randEngine() % COLUMN_NUM;
        switch (type) {
            case COLUMN_ADD:
                count += createAdd(num1, num2);
                break;
            case COLUMN_MINUS:
                count += createMinus(num1, num2);
                break;
            case COLUMN_MULTI:
                count += createMulti(num1, num2);
                break;
            default:
                fprintf(stderr, "impossible!\n");
                break;
        }
        _count[type] += count - last_count;

        // 每COLUMN_PER_ROW列换一次行
        if (count % COLUMN_PER_ROW == 0 && count > last_count) {
            _file << endl;
        }

        // 为了避免出现连续的无效题,导致出现连续的空行,记录之前的个数进行比较
        last_count = count;
    }
    
    return;
}

// 生成加法
int kousuanCreater::createAdd(int num1, int num2) {
    if (!commonFilterAddAndMinus(num1, num2, COLUMN_ADD)) {
        return 0;
    } else if (num1 + num2 > 100) {
        return 0;
    }

    return writeStrIntoFile(to_string(num1) + " + " + to_string(num2) + "\t= \t\t\t");
}

// 生成减法
int kousuanCreater::createMinus(int num1, int num2) {
    // 固定将num1设置为更大的数,可以避免后续判断哪个更大放前面
    if (num1 < num2) {
        swap(num1, num2);
    }

    if (!commonFilterAddAndMinus(num1, num2, COLUMN_MINUS)) {
        return 0;
    } else if (num1 - num2 < 10 || (num1 - num2) % 10 == 0) {
        return 0;
    }

    return writeStrIntoFile(to_string(num1) + " - " + to_string(num2) + "\t= \t\t\t");
}

// 生成乘法
int kousuanCreater::createMulti(int num1, int num2) {
    // 将数字限定在1 ~ 9之间
    num1 %= 9 + 1;
    num2 %= 9 + 1;
    if (num1 <= 1 || num2 <= 1) {
        return 0;
    }

    // 固定将num1设置为更大的数,可以避免后续判断哪个更大放前面
    // 为了更好的匹配乘法口诀
    if (num1 > num2) {
        swap(num1, num2);
    }

    return writeStrIntoFile(to_string(num1) + " × " + to_string(num2) + "\t= \t\t\t");
}

lru.h

#include <iostream>
#include <unordered_map>
#include <functional>
using namespace std;

struct myListNode {
    myListNode(const string& str = "") {
        this->str = str;
        this->next = nullptr;
        this->prev = nullptr;
    }

    string str;
    myListNode* next;
    myListNode* prev;
};

class LRU {
public:
    LRU(int max_num) {
        _head = new myListNode();
        _tail = new myListNode();
        _head->next = _tail;
        _tail->prev = _head;
        _max_num = max_num;
        _rec.reserve(_max_num);
    }

    ~LRU() {
        while (_head) {
            myListNode* next = _head->next;
            delete _head;
            _head = nullptr;
            _head = next;
        }
    }

public:
    bool is_exist(const string& str) {
        size_t key = _str_hash(str);
        return is_exist(key);
    }

    bool is_exist(size_t key) {
        auto it = _rec.find(key);
        if (it == _rec.end()) {
            return false;
        }

        update_node(it->second);
        return true;
    }

    bool add_node(const string& str) {
        size_t key = _str_hash(str);
        if (is_exist(key)) {
            update_node(_rec[key]);
            return true;
        } else if (_rec.size() >= static_cast<size_t>(_max_num)) {
            del_tail_node();
        }

        myListNode* node = new myListNode(str);
        insert_head_node(node);
        _rec.insert({key, node});
        return true;
    }

private:
    inline bool update_node(myListNode* node) {
        if (!node) {
            return false;
        }
        node->prev->next = node->next;
        node->next->prev = node->prev;
        insert_head_node(node);
        return true;
    }

    inline bool del_node(myListNode* node) {
        if (!node) {
            return false;
        }

        size_t key = _str_hash(node->str);
        _rec.erase(key);
        node->prev->next = node->next;
        node->next->prev = node->prev;
        delete node;
        node = nullptr;
        return true;
    }

    inline void del_tail_node() {
        if (_tail->prev == _head) {
            return;
        }

        del_node(_tail->prev);
    }

    inline bool insert_head_node(myListNode* node) {
        if (!node) {
            return false;
        }

        _head->next->prev = node;
        node->next = _head->next;
        _head->next = node;
        node->prev = _head;
        return true;
    }

private:
    int _max_num;
    unordered_map<size_t, myListNode*> _rec;
    myListNode* _head = nullptr;
    myListNode* _tail = nullptr;
    hash<string> _str_hash;
};

运行结果

cost time : [0]
加法有259道题, 占比31%
减法有398道题, 占比49%
乘法有153道题, 占比18%

五、后记

从开始最简单的生成算式,到添加数组 + hash表的方式进行去重,然后再修改为使用LRU的方式进行去重,代码量也从刚开始的156行,到第二版的193行,再到最后的336行,代码也越写越复杂。
当然项目中是不推荐这样写的,如果真的在工作上遇上类似问题,应该第二种方法就够用了,只是单纯为了写复杂练习才慢慢修改为第三版代码。
之后还能想到的复杂化方式有:

  1. 修改为消费者模式,把生成算式和写文件分开到不同的线程
  2. 使用装饰者模式进行算式过滤条件的组合添加
  3. 修改算法,使得每种算式占比尽量均匀分布

暂时想不到继续复杂化的地方了,后续再继续慢慢完善

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值