<?php /** * author: selfimpr * blog: http://blog.csdn.net/lgg201 * mail: [email protected] * copyright: 原创作品, 转载请附原始连接. * * 根据某种比较算法读取数据集中的最大N条数据, 并且, 数据有特征字段, 结果集中每条数据的特征字段必须唯一 * 应用场景举例: * 某班要发放单科成绩优秀奖, 共有N条单科成绩数据, 现要选取M名同学作为嘉奖对象, 但每人最多只能获奖一次. * 这里提供两种实现方式, 并加以比较其时间复杂度, 以便优选算法 * 算法1: 先对所有成绩排序, 然后从中选择 * 算法2: 不经过排序, 一遍扫描数据集选择数据 * 时间复杂度比较结果: * 算法1时间消耗 : 算法2时间消耗 = log N : M */ define('WEIGHT', 'weight'); define('TIME', 'time'); define('UID', 'uid'); define('FEED_ID', 'feed_id'); define('FEED_ID_MIN', 10000); define('WEIGHT_MIN', 20); define('TIME_MIN', 1000); define('UID_MIN', 1000); #产生测试数据 function generate_datas() { #定义最大值 if ( !defined(FEED_ID_MAX) ) { define('FEED_ID_MAX', FEED_ID_MIN + FEED_ID_COUNT); define('WEIGHT_MAX', WEIGHT_MIN + WEIGHT_COUNT); define('TIME_MAX', TIME_MIN + TIME_COUNT); define('UID_MAX', UID_MIN + UID_COUNT); } $feed_id = FEED_ID_MIN; $datas = array(); while ( $feed_id ++ < FEED_ID_MAX ) { array_push($datas, array( FEED_ID => $feed_id, TIME => rand(TIME_MIN, TIME_MAX), UID => rand(UID_MIN, UID_MAX), WEIGHT => rand(WEIGHT_MIN, WEIGHT_MAX), )); } return $datas; } #输出测试数据 function output_datas($datas, $fp) { foreach ( $datas as $data ) { fprintf($fp, "%d %d %d %d\n", $data[FEED_ID], $data[WEIGHT], $data[UID], $data[TIME]); } } #权重排序(先权重, 后时间) function weight_sort($a, $b) { if ( $a[WEIGHT] != $b[WEIGHT] ) return $a[WEIGHT] - $b[WEIGHT]; return $a[TIME] - $b[TIME]; } #权重排序(先权重, 后时间), 倒序, 供先排序后选择算法使用 function weight_sort_reverse($a, $b) { if ( $b[WEIGHT] != $a[WEIGHT] ) return $b[WEIGHT] - $a[WEIGHT]; return $b[TIME] - $a[TIME]; } #权重选择(基于已排序数据集) function weight_select($datas, $uniq_key, $count) { $result = array(); foreach ( $datas as $data ) { if ( array_key_exists($data[$uniq_key], $result) ) continue; $result[$data[$uniq_key]] = $data; if ( -- $count <= 0 ) break; } return $result; } /** * compare_select_uniq_n * 读取数据集$datas中的数据, 应用$compare比较函数, 选取$uniq_key数据项不重复的最大前$count条数据 * 复杂度计算: * 假设: * 1. 数据集$datas长度为N * 2. 要获取的记录数$count为M * 计算: * 复杂度 = O( M * 2 ) + O( M * M * 1 ) + O( (N - M) * 1 ) + O( (N - M) * M * 2 ) + O( (N - M) * 1 ) * = O( 2 * M * (M + 1) ) + O( 2 * (N - M) * (M + 1) ) * = O( 2 * (M + N - M) * (M + 1) ) * = O( 2 * N * (M + 1) ) * = O( N * M ) #忽略常数 * @param mixed $datas * @param mixed $uniq_key * @param mixed $compare * @param mixed $count * @access public * @return void */ function compare_select_uniq_n($datas, $uniq_key, $compare, $count) { $tmp_datas = array(); #结果集空间 $tmp_length = 0; #结果集空间当前长度 $index = 0; #遍历目标数据集$datas的下标 $length = count($datas); #目标数据集$datas的长度 #step 1: 构造初始的$count个临时数据 #复杂度: O(M * 2) while ( $count -- > 0 && $index < $length ) { $data = $datas[$index]; $index ++; #寻找唯一键相同的已有数据, 比较替换 #复杂度: O(M * M * 2) foreach ( $tmp_datas as $i => $ele ) { if ( $ele[$uniq_key] == $data[$uniq_key] ) { if ( $compare($data, $ele) > 0 ) $tmp_datas[$i] = $data; break; } } $tmp_datas[$count] = $data; } #step2: 遍历剩余数据, 比较替换 #复杂度: O( (N - M) * 1 ) while ( $index < $length ) { $data = $datas[$index]; $index ++; #寻找结果集中要替换的数据下标 $cmp_i = 0; #复杂度: O( (N - M) * M * 2 ) foreach ( $tmp_datas as $i => $ele ) { #优先选择唯一键相同的数据 if ( $ele[$uniq_key] == $data[$uniq_key] ) { $cmp_i = $i; break; #否则查找当前结果集中的最小记录 } else if ( $compare($tmp_datas[$cmp_i], $ele) > 0 ) { $cmp_i = $i; } } #检查要替换的目标数据, 如果符合替换条件则替换 #复杂度: O( (N - M) * 1 ) if ( $compare($data, $tmp_datas[$cmp_i]) > 0 ) $tmp_datas[$cmp_i] = $data; } return $tmp_datas; } #基于排序然后选择的算法 function compare_select_uniq_n_sort($datas, $uniq_key, $compare, $count) { usort($datas, $compare); return weight_select($datas, $uniq_key, $count); } #测试入口封装 function test_wrap($datas, $uniq_key, $compare, $count, $func, $timefp, $outfp = NULL) { $begin = microtime(true); $datas = $func($datas, $uniq_key, $compare, $count); $end = microtime(true); fprintf($timefp, "%s: %ss\n", strpos($func, 'sort') === FALSE ? '自建算法' : '排序算法', $end - $begin); if ( $outfp ) { #对结果数据排序以方便价差 usort($datas, 'weight_sort'); output_datas($datas, $outfp); } } define('FEED_ID_COUNT', 100000); #产生的测试数据中FEED_ID个数 define('WEIGHT_COUNT', 100); #产生的测试数据中的WEIGHT个数上限 define('TIME_COUNT', 5000); #产生的测试数据中的TIME个数上限 define('UID_COUNT', 5000); #产生的测试数据中的UID个数上限 #输出句柄 $stdout = fopen('php://stdout', 'w'); $stderr = fopen('php://stderr', 'w'); #产生测试数据 $datas = generate_datas(); #输出排序的测试数据集供检查正确性 #$tmp_datas = $datas; #usort($tmp_datas, 'weight_sort'); #output_datas($tmp_datas, $stdout); define('RETRIEVE_COUNT', 50); #要取回的个数 test_wrap($datas, UID, 'weight_sort', RETRIEVE_COUNT, 'compare_select_uniq_n_sort', $stdout); test_wrap($datas, UID, 'weight_sort_reverse', RETRIEVE_COUNT, 'compare_select_uniq_n', $stdout); /* +------------------+--------------------------------------------------------------+ | 测试结果 | +------------------+--------------------------------------------------------------+ | 测试条件 | 结果数据 | +------------------+--------------------------------------------------------------+ | N: 1000 | 排序算法: 0.030486106872559s | | M: 50 | 自建算法: 0.18151593208313s | +------------------+--------------------------------------------------------------+ | N: 100000 | 排序算法: 6.3918650150299s | | M: 50 | 自建算法: 19.431954860687s | +------------------+--------------------------------------------------------------+ | N: 1000 | 排序算法: 0.029729843139648s | | M: 10 | 自建算法: 0.044672012329102s | +------------------+--------------------------------------------------------------+ | N: 100000 | 排序算法: 5.9424071311951s | | M: 10 | 自建算法: 4.2767288684845s | +------------------+--------------------------------------------------------------+ | 测试结果分析 | +------------------+--------------------------------------------------------------+ | 1. 快速排序法时间复杂度为O( N * log N ), 权重选择时间忽略 | | 2. 自建选择算法时间复杂度为O( N * M ) | | 根据以上两条, 期望为两种算法时间消耗比例为M : log N | | 1. 50 : log 1000 = 5 : 1, 测试结果符合期望 | | 2. 50 : log 100000 = 5 : 1.3, 测试结果符合期望 | | 3. 10 : log 1000 = 1 : 1, 测试结果略有偏差, 但整体数据量小, 此误差可接受 | | 4. 10 : log 100000 = 1 : 1.3, 测试结果符合期望 | +------------------+--------------------------------------------------------------+ */