假如你为自己开发一个简单的股票投资软件,该软件系统中的核心类自然就是“股票”类(仅用于问题分析,不具有现实意义):
struct Stock
{
int code; // 股票代码
int id; // 行业id
float price; // 股价
float pe; // 市盈率
int budget; // 预算(即打算在这支股票投入多少钱)
}
由于预算有限(比如100万吧),你制作了一个表,为“行业”、“股价”和“市盈率”这3个股票属性的各个区段配置一个预算(表中仅列出部分配置):
行业id | 股价下限 | 股价上限 | 市盈率下限 | 市盈率上限 | 预算 |
---|---|---|---|---|---|
1 | ¥5.00 | ¥10.00 | 10 | 15 | ¥10万 |
1 | ¥5.00 | ¥10.00 | 16 | 20 | ¥8万 |
1 | ¥10.01 | ¥15.00 | 10 | 15 | ¥6万 |
1 | ¥10.01 | ¥15.00 | 16 | 20 | ¥4万 |
2 | ¥5.00 | ¥10.00 | 15 | 20 | ¥15万 |
2 | ¥5.00 | ¥10.00 | 21 | 25 | ¥12万 |
2 | ¥10.01 | ¥15.00 | 15 | 20 | ¥10万 |
2 | ¥10.01 | ¥15.00 | 20 | 25 | ¥8万 |
行业用一个整数值表示(行业id),id越小的行业,越是你优先考虑的投资行业;当然,股价越便宜,估值越低,越优先考虑。比如行业1的股票,如果:
● 股价在[5, 10]区间,市盈率在[10, 15]之间,则投资预算是10万
● 股价在[5, 10]区间,市盈率在[16, 20]之间,则投资预算是8万
你经过大量研究在市场上选中某支股票后,以这支股票所属行业、股价和市盈率为依据,查表来确定应该为这支股票投入多少资金。
注意:配置之间不允许有冲突,比如下表中的后2条配置就和第1条冲突,因为假如行业1有一支股票,其股价是8块,市盈率是15,那么根据配置,你无法决定是投入8万,9万,还是10万:
行业id | 股价下限 | 股价上限 | 市盈率下限 | 市盈率上限 | 预算 |
---|---|---|---|---|---|
1 | ¥5.00 | ¥10.00 | 10 | 15 | ¥10万 |
1 | ¥5.00 | ¥10.00 | 15 | 20 | ¥8万 |
1 | ¥5.00 | ¥10.00 | 14 | 20 | ¥9万 |
现在,你面临两个需求(或者说两个问题):
1. 读取配置时,如何判断有冲突的配置记录?
2. 准备投资一支股票时,如何确定投资预算?
第一种方案,你可以选择std::multimap表来存储这些配置:
struct StockCfg
{
float min_price; // 股票价格下限
float max_price; // 股票价格上限
float min_pe; // 市盈率下限
float max_pe; // 市盈率上限
int budget; // 预算
}
std::multimap<int, StockCfg> stock_cfg; // key为行业id,value为StockCfg
你假设不会遇到冲突的配置,因此忽略问题1,对于问题2,你这样解决:
int GetBudget(const Stock& stock)
{
for (auto it = stock_cfg.equal_range(stock.id); it.first != it.second; ++it.first)
{
if (stock.price >= it.first->second.min_price &&
stock.price <= it.first->second.max_price &&
stock.pe >= it.first->second.min_pe &&
stock.pe >= it.first->second.max_pe)
{
return itr.first->second.budget;
}
}
return 0; // 预算为0,不建议投资
}
这也是我见过的一种方案,显然这不是一个很好的方案。该方案有两个隐患:
1. 你不能真的忽略可能的冲突配置,这个表扩展起来可能有成千上万行!没有人能保证这么多配置不会出错,你应该对冲突进行检测!
2. 以行业id为key用std::multimap存储,同一个行业的不同股价和不同市盈率的配置顺序可能就是配置文件中的原始顺序,也就是说没有按照你想要的优先级排列,如果有冲突的记录,你GetBudget到的投资预算有可能并不准确,那么,你离财务自由的目标就可能会越来越远了。
我们来解决第1个问题,在加载配置时检测冲突:
bool AddCfg(int id, const StockCfg& cfg)
{
for (auto it = stock_cfg.equal_range(id); it.first != it.second; ++it.first)
{
if (HasConflict(cfg, itr.first->second))
{
return false; // 有冲突
}
}
return stock_cfg.insert(make_pair(id, cfg)).second;
}
检测冲突的HasConflict函数就是比较两个配置的min_price、max_price、min_pe和max_pe这两对属性是否有重叠,代码从略。
问题1解决了,问题2自然就不存在了,但是因为没有排序,采用遍历的方式,有那么点性能损耗。当然,作为一个优秀的程序员,你可能无法接受这个方案。
抱歉,本文的主题现在才正式开始!
其实,我们只需要为StockCfg类提供一个合适的operator <
即可,这两个需求实现起来,干净利落,而且也不会有性能损耗。
struct StockCfg
{
int id; // 行业id
float min_price; // 股票价格下限
float max_price; // 股票价格上限
float min_pe; // 市盈率下限
float max_pe; // 市盈率上限
StockCfg(int xid, float minprice, float maxprice, float minpe, float maxpe)
: id(xid), min_price(minprice), max_price(maxprice), min_pe(minpe), max_pe(maxpe)
{}
}
bool operator < (const StockCfg& left, const StockCfg& right)
{
if (left.id != right.id)
{
return left.id < right.id;
}
if (left.max_price < right.min_price)
{
return true; // left的股价上限比right的股价下限都小,那就小
}
else if (left.min_price > right.max_price)
{
return false; // left的股价下限比right的股价上限都大,那就大
}
// 两者的[min_price, max_price]区间有重叠,就判定为相等。下同:
if (left.max_pe < right.min_pe)
{
return true;
}
else if (left.min_pe > right.max_pe)
{
return false;
}
return false;
}
以图示例更明了:
只要有重叠,就视为相等:
提供了上述的operator <
之后,StockCfg结构体即可作为std::map的key:
std::map<StockCfg, int> stock_cfg; // key为StockCfg,value为预算
那么解决第1个问题,最自然不过的了(很容易就检测出第一张表中的最后一条记录非法):
bool AddCfg(const StockCfg& cfg, int budget)
{
return stock_cfg.insert(make_pair(cfg, budget)).second;
}
对于第2个问题,需要一点技巧:
int GetBudget(const Stock& stock)
{
// 构造一个临时的StockCfg对象作为key,key的min_price和max_price都赋值为真实股价(stock.price)
// min_pe和max_pe都赋值为真实的市盈率(stock.pe),以此key在map表中查找:
auto it = stock_cfg.find(StockCfg(stock.id, stock.price, stock.price, stock.pe, stock.pe));
if (it != stock_cfg.end())
{
return it->second;
}
return 0; // 预算为0,不建议投资
}
如果有第3个需求:
3. 为一支股票没有找到对应的配置,则以最接近的配置为参考。
仍然很简单,用临时key调用stock_cfg.lower_bound(...)
,然后对返回的迭代器进行必要的检查,比如,如果行业都不符合要求,则直接返回0。但是,如果你像前面的那个方案一样,手工去做这个lower_bound的工作,那么,冗余、复杂、潜在bug、代码丑陋,总之各种坏味道扑面而来!
好了,正如前一篇结语,本文其实仍不值一提。