需求
在网络游戏中,服务器通常会对全服玩家的某些数据进行排名,如等级、战力、活动获取的积分等等。而对于排行榜,需做到以下要求求:
- 排行榜需要做到实时,如玩家战力提升,立马看到新的排名
- 只关心前N名玩家的排名,通常 N <= 1000
- 排名分前后顺序,不可并列,如同一战力,先达到的排名靠前
实现
源码地址
https://github.com/zifengzwz/sundrycpp/tree/main/rank
目录结构
.
├── CMakeLists.txt
├── main.cpp
└── sorted_set.hpp
sorted_set.hpp
#ifndef SORTED_SET
#define SORTED_SET
#include <memory>
#include <set>
#include <unordered_map>
template <class Key, class T>
class sorted_set {
public:
using key_type = Key;
using value_type = T;
using size_type = std::size_t;
// default: care of top 1000
static constexpr size_type default_top = 1000;
struct key_value_type {
key_type first;
value_type second;
key_value_type(const key_type& k, const value_type& v)
: first(k), second(v) {}
};
using key_value_ptr = std::shared_ptr<key_value_type>;
struct key_value_less {
constexpr bool operator()(const key_value_ptr& left,
const key_value_ptr& right) const {
if (left->second == right->second) {
return left->first < right->first;
}
return left->second < right->second;
}
};
sorted_set() : top_(default_top) {}
explicit sorted_set(size_type top) : top_(top == 0 ? default_top : top) {}
sorted_set(const sorted_set&) = delete;
sorted_set& operator=(const sorted_set&) = delete;
~sorted_set() = default;
sorted_set(sorted_set&& right)
: top_(right.top_),
map_(std::move(right.map_)),
set_(std::move(right.set_)) {}
sorted_set& operator=(sorted_set&& right) noexcept {
top_ = right.top_;
map_ = std::move(right.map_);
set_ = std::move(right.set_);
return *this;
}
// ranged-for: large->small
auto begin() const { return set_.rbegin(); }
auto end() const { return set_.rend(); }
auto find(key_type k) { return map_.find(k); }
auto map_end() { return map_.end(); }
auto set_end() { return set_.end(); }
void insert(const key_type& k, const value_type& v) {
auto add = [&] {
key_value_ptr ptr = std::make_shared<key_value_type>(k, v);
map_.emplace(k, ptr);
set_.insert(ptr);
};
auto it = map_.find(k);
if (it == map_.end()) {
if (set_.size() < top_) {
add();
} else if (set_.size() == top_) {
auto val = v;
if (val < (*set_.begin())->second) return;
map_.erase((*set_.begin())->first);
set_.erase(set_.begin());
add();
}
} else {
set_.erase(it->second);
map_.erase(it);
add();
}
}
void erase(const key_type& k) {
auto it = map_.find(k);
if (it == map_.end()) return;
set_.erase(it->second);
map_.erase(it);
}
void clear() {
set_.clear();
map_.clear();
}
size_type size() { return set_.size(); }
size_type rank(const key_type& k) {
auto it = map_.find(k);
if (it == map_.end() || set_.empty()) return 0;
size_type rank = 0;
for (auto n = set_.rbegin(); n != set_.rend(); ++n) {
++rank;
if ((*n)->first == k) return rank;
}
return 0;
}
private:
size_type top_;
std::unordered_map<key_type, key_value_ptr> map_;
std::set<key_value_ptr, key_value_less> set_;
};
#endif
- 这里使用标准库的set和unordered_map简单地实现sorted_set(有序集合)
- 参与排名的userid与排名数据为一个整体,作为set的元素和unordered_map的值,userid为unordered_map的键
- 利用set的红黑树结构进行自排序,unordered_map哈希特性进行快速索引
- 默认排名为前1000(default_top)
- 通常排名在单线程中进行,无需考虑多线程安全,禁用拷贝构造和赋值操作
- 添加了begin和end方法,支持从大到小的范围for循环
main.cpp
#include <chrono>
#include <cstdint>
#include <iostream>
#include <random>
#include "sorted_set.hpp"
struct rank_item {
int score{0};
int64_t time{0};
bool operator<(const rank_item& r) {
if (score == r.score) {
return time > r.time;
}
return score < r.score;
}
bool operator==(const rank_item& r) {
return score == r.score && time == r.time;
}
};
// [left, right]
int rand_num(int left, int right) {
static std::default_random_engine gen(std::random_device{}());
std::uniform_int_distribution<decltype(right)> dis(left, right);
return dis(gen);
}
int64_t get_time_milli() {
auto now = std::chrono::system_clock::now().time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(now).count();
}
int main(int argc, char* argv[]) {
sorted_set<int, rank_item> rank_info;
for (int i = 1; i <= 10; ++i) {
rank_info.insert(i, rank_item{rand_num(50, 100), get_time_milli()});
}
int rank = 1;
for (auto& n : rank_info) {
std::cout << "rank: " << rank << ", userid: " << n->first
<< ", score: " << n->second.score << std::endl;
++rank;
}
return 0;
}
- 对于排名数据rank_item,需重载<和==运算符
- 随机生成50-100之间的数值作为得分,对userid为1-10的玩家进行排名
CMakeLists.txt
cmake_minimum_required(VERSION 3.19)
project(rank LANGUAGES CXX)
SET(CMAKE_CXX_STANDARD 17)
add_executable(rank main.cpp sorted_set.hpp)
- 由于使用了一些新特性,需要cmake添加对C++17标准的支持
编译运行
# mkdir build
# cd build
# cmake ..
# cmake --build .
# ./rank
rank: 1, userid: 4, score: 100
rank: 2, userid: 8, score: 97
rank: 3, userid: 5, score: 94
rank: 4, userid: 9, score: 91
rank: 5, userid: 2, score: 89
rank: 6, userid: 3, score: 75
rank: 7, userid: 6, score: 70
rank: 8, userid: 7, score: 68
rank: 9, userid: 10, score: 64
rank: 10, userid: 1, score: 55