本文说的排序并不是指「冒泡」之类的技术概念,而是一个业务相关的问题。
举例来说:某个网站,每天都能产生很多数据,需要一个推荐列表页面来展示数据。最初是完全按照时间倒序来排序的,但是这样就产生了一个问题:新鲜的数据不一定是有价值的数据!假设某个时段灌水的数据比较多,那么用户当时在列表页看到的就都是灌水的内容。既然如此,不妨换个思路:给每个数据投票,投票规则可以按业务逻辑自定义,比如:每次评论加一票,每次转发加两票等等。然后按照投票数来倒序是不是就可以了?可惜还有问题:有价值的数据不一定是新鲜的数据!假设历史上曾经产生了一个高票的数据,那么不管过多久,它都会一直占据前排座位。由此可见最好的结果是让用户能看到既新鲜又有价值的数据。
实际上此问题早就有人讨论过了,建议大家阅读相关文章:
- 榜单类应用我所喜欢的算法
- Social Media排序算法的四种模式
- Hacker News与Reddit的算法比较
- Reddit, Stumbleupon, Del.icio.us and Hacker News Algorithms Exposed!
一种方法是让分数随着时间的流逝而降低,比如说一条数据原本的分数是 100 分,第二天衰减到 50 分,第三天衰减到 25 分,以此类推。此方法的优点是简单易懂,缺点是数据的分数随着时间的变化而变化,实际应用中,可能需要用 cron 之类的方法定期更新数据的分数,一旦数据比较多就悲剧了。
另一种方法是让分数本身就包含时间因子:「t / 43200 log10(x)」:
- 「t」表示的是数据生成时的时间戳。
- 「43200」表示的是半天(12 个小时)的秒数。
- 「x」表示的是数据得到的票数。
于是乎可知「t / 43200」得到的就是有多少个半天:
代码语言:javascript复制<?php
$result = [];
$dates = [
'2017-01-01',
'2017-01-02',
'2017-01-03',
'2017-01-04',
'2017-01-05',
];
foreach ($dates as $date) {
$result[$date] = strtotime($date) / 43200;
}
var_export($result);
/*
结果:
array (
'2017-01-01' => 34334,
'2017-01-02' => 34336,
'2017-01-03' => 34338,
'2017-01-04' => 34340,
'2017-01-05' => 34342,
)
*/
?>
实际上「t / 43200」相当于是「时间分」,随着时间的增加而增加,其基数比较大,保证了新鲜的数据会排在前面,同时每天「时间分」的差值为 2,后面会用到这个数字。
那「log10(x)」又是什么意思呢,我们可以把它理解成「价值分」,其基数比较小,之所以做 log10 的对数运算就是为了快速衰减,确保前面投票的权重大于后面投票,如此有利于让有价值的数据尽早被发现:
代码语言:javascript复制<?php
$result = [];
$values = [
10000,
1000,
100,
];
foreach ($values as $value) {
$result[$value] = sprintf('%.5f', log10($value));
}
var_export($result);
/*
结果:
array (
10000 => '4.00000',
1000 => '3.00000',
100 => '2.00000',
)
*/
?>
如上所示:如果一条数据的得票数是 100,那么它的价值分将是 2,最多可以在推荐列表上停留一天;如果一条数据的得票数是 1000,那么它的价值分将是 3,最多可以在推荐列表上停留一天半;如果一条数据的得票数是 10000,那么它的价值分将是 4,最多可以在推荐列表上停留两天,如此保证价值分高的数据不会霸占前排座位太久。
最后要说明的是,与其死记硬背公式,倒不如理解它的意思,并根据客观情况调整,比如说,有的人觉得时间分本身太大了,那么我们可以减去一个参照时间。有的人觉得为什么一定要除以 12 个小时,除以 12.5 个小时可不可以?答案当然是可以的!有的人觉得为什么一定要用以 10 为底的对数,可不可以使用科学对数?答案当然也是可以的!只要你理解了分数中「时间分」和「价值分」的用意,那么怎么实现它们都可以。