BUPT · 数据结构课程项目 · 2026

个性化旅游系统

基于 Rust 自定义数据结构与算法,覆盖推荐、路线规划、
设施查询、旅游日记、社区搜索与美食推荐的全旅程平台

谢佳恒
王逸晨
蔡芝林
Rust Axum SQLite React Tauri
扫码访问 dscp.meltyland.dev
dscp.meltyland.dev 扫码在线访问

六大核心功能

满足数据结构课程全部要求,覆盖完整旅行生命周期

01

旅游推荐

基于 Top-K 堆算法的个性化景点推荐,评分可解释,支持类别与关键词检索

02

路线规划

Dijkstra 最短路径 + 最近邻启发式多点旅游路线,2-opt 局部优化

03

地点查询

有界 Dijkstra 扩展,基于图路径距离(非欧氏)的周边设施分类搜索

04

旅游日记

日记创建与管理,支持 Huffman 无损压缩存储,降低磁盘占用

05

日记社区

2-gram 倒排索引全文搜索,综合 gram 命中、评分与热度的复合排名

06

美食推荐

菜系偏好 × 图路径距离 × 热度融合的 Top-K 美食排名推荐

系统架构

五层分层设计,职责边界清晰,算法层完全自实现

展示层
React 页面 地图可视化 算法基准测试UI 日记编辑器
Tauri IPC / HTTP
桌面容器层
Tauri Shell 跨平台打包
REST API
服务层
Rust + Axum 11 个 REST 端点 SQLite 持久化 Tokio 异步运行时
函数调用
算法层 核心
MinHeap<T> AdjacencyListGraph Top-K 引擎 2-gram 搜索引擎 Huffman 编码树
读写
数据管道层
OpenStreetMap 提取 数据归一化脚本 种子数据加载
200+
目的地节点
200+
道路边
11
API 端点
5
自定义数据结构

核心数据结构

全部以 Rust 自定义实现,不依赖标准库等价物

MinHeap<T: Ord>

Dijkstra 优先队列与 Top-K 选择的核心支撑。维护最小元素在根部,push/pop 均 O(log n)。

时间复杂度
push O(log n) · pop O(log n) · peek O(1)
核心操作
sift_up · sift_down
泛型约束
T: Ord(支持任意可比较类型)
应用场景
Dijkstra 优先队列 · Top-K 排名 · Huffman 树构建
min_heap.rs
struct MinHeap<T: Ord> { data: Vec<T> }

impl<T: Ord> MinHeap<T> {
    fn push(&mut self, val: T) {
        self.data.push(val);
        self.sift_up(self.data.len() - 1);
    }

    fn pop(&mut self) -> Option<T> {
        if self.data.is_empty() { return None; }
        let last = self.data.pop().unwrap();
        if self.data.is_empty() { return Some(last); }
        let min = std::mem::replace(&mut self.data[0], last);
        self.sift_down(0);
        Some(min)
    }

    fn sift_up(&mut self, mut i: usize) {
        while i > 0 {
            let p = (i - 1) / 2;
            if self.data[p] <= self.data[i] { break; }
            self.data.swap(p, i);
            i = p;
        }
    }
}
堆当前为空,点击「插入随机数」开始

邻接表有权图

支持有向/无向加权边,Dijkstra 最短路径与有界扩展均基于此图实现。

存储方式
Vec<Vec<(NodeId, f64)>>
节点操作
add_node · add_directed/undirected_edge
最短路径
Dijkstra + 自定义 MinHeap,O((V+E) log V)
有界扩展
shortest_distances_within(start, max_dist)
graph.rs
struct Graph {
    adj: Vec<Vec<(usize, f64)>>,
    node_count: usize,
}

fn shortest_path(&self, start: usize, goal: usize)
    -> Option<PathResult>
{
    let mut dist = vec![f64::INFINITY; self.node_count];
    let mut prev = vec![None; self.node_count];
    let mut heap = MinHeap::new();
    dist[start] = 0.0;
    heap.push((OrderedFloat(0.0), start));

    while let Some((d, u)) = heap.pop() {
        if u == goal { return Some(reconstruct(prev, u)); }
        if d.0 > dist[u] { continue; }
        for &(v, w) in &self.adj[u] {
            let nd = dist[u] + w;
            if nd < dist[v] {
                dist[v] = nd;
                prev[v] = Some(u);
                heap.push((OrderedFloat(nd), v));
            }
        }
    }
    None
}

邻接表结构示意 — 每个节点维护一个 (邻居, 权重) 列表

2-gram 倒排索引

将文本按滑动窗口切分为字符对,建立 gram → 文档列表的倒排映射,支持模糊全文检索。

索引结构
HashMap<String, Vec<DocId>>
分词方式
滑动窗口,连续字符对(中英均支持)
评分公式
hits×1.5 + title_boost + rating×2 + ln(views+1)
查询复杂度
O(gram_count × posting_list_len)
search.rs
fn build_index(docs: &[DiaryEntry])
    -> HashMap<String, Vec<usize>>
{
    let mut idx: HashMap<String, Vec<usize>>
        = HashMap::new();
    for (id, doc) in docs.iter().enumerate() {
        let text = format!("{} {}", doc.title, doc.content);
        let chars: Vec<char> = text.chars().collect();
        for w in chars.windows(2) {
            let gram: String = w.iter().collect();
            idx.entry(gram).or_default().push(id);
        }
    }
    idx
}

// 评分:融合相关性 + 热度 + 评分
score = gram_hits * 1.5
      + if title_match { 2.0 } else { 0.0 }
      + diary.rating * 2.0
      + (diary.views as f64 + 1.0).ln();

搜索「旅行」的倒排检索与评分过程

Huffman 编码树

对日记内容按字符频率进行无损压缩,高频字符编码短,低频字符编码长,最优前缀编码。

构建算法
贪心合并,频率最小优先,O(n log n)
编码方式
左边 0,右边 1;变长前缀编码
存储格式
EncodedPayload { bytes, bit_len, code_table }
验证
解压后与原文完全一致(可逆验证)
search.rs — build_huffman_tree
fn build_huffman_tree(
    freq: &HashMap<char, usize>
) -> HuffmanNode {
    // 用 MinHeap 以频率为键构建优先队列
    let mut heap = MinHeap::new();
    for (&ch, &f) in freq {
        heap.push(HuffmanNode::leaf(ch, f));
    }
    // 贪心合并:每次取两个最小节点合并
    while heap.len() > 1 {
        let a = heap.pop().unwrap();
        let b = heap.pop().unwrap();
        heap.push(HuffmanNode::merge(a, b));
    }
    heap.pop().unwrap()  // 根节点即完整编码树
}

字符串 "TRAVEL" 的 Huffman 编码树(叶节点显示编码)

算法演示

交互式逐步动画,直观感受核心算法执行过程

Dijkstra 最短路径

单源最短路径,使用自定义 MinHeap 作优先队列。从节点 A 出发,寻找到 G 的最短路径。

时间 O((V+E) log V) 空间 O(V)
点击「开始演示」观察算法执行过程
节点ABCDEFG
dist
未访问
队列中
当前节点
已访问
最短路径

Top-K 堆选择

维护大小为 k 的最小堆,流式处理数据时保留最大的 k 个元素。O(n log k) 优于完整排序 O(n log n)。

时间 O(n log k) 空间 O(k)
数据流 (k=3)
堆内部 (最小堆)
当前 Top-3
点击「下一个元素」开始流入数据

REST API 接口

Rust + Axum 实现,11 个端点,JSON 统一响应格式

端点路径 方法 功能描述 核心算法
/api/recommend/destinations POST 个性化景点推荐(请求驱动) Top-K 堆
/api/recommend/destinations/live GET Top-K 推荐(数据库驱动) Top-K 堆
/api/routes/shortest POST 单源最短路径查询 Dijkstra
/api/facilities/nearby POST 周边设施分类搜索 有界 Dijkstra
/api/diaries POST 创建新旅游日记
/api/diaries/search POST 全文日记检索(请求驱动) 2-gram 倒排索引
/api/diaries/search/live POST 全文检索(数据库驱动) 2-gram 倒排索引
/api/diaries/compress POST Huffman 压缩演示 Huffman 编码
/api/benchmarks GET 算法性能对比报告 堆选择 vs 全排序
/healthz GET 健康检查

技术栈与性能目标

后端核心

  • Rust — 内存安全的系统语言
  • Axum — 异步 Web 框架
  • SQLite / rusqlite — 嵌入式持久化
  • Tokio — 异步运行时

前端与桌面

  • React + Vite — 前端框架
  • Tailwind CSS — 原子化样式
  • Tauri — 跨平台桌面封装
  • D3.js — 数据可视化

数据与工具链

  • OpenStreetMap — 地理数据源
  • Cargo Workspace — 2 个 crate
  • pnpm — 前端包管理
  • rusqlite — 种子数据入库

自定义数据结构

  • MinHeap<T> — 通用最小堆
  • AdjacencyListGraph — 有权图
  • TopKEngine — 排名引擎
  • DiarySearchEngine — 2-gram 搜索
  • HuffmanTree — 压缩编码树
< 300ms
推荐 / 美食查询
< 200ms
单源最短路径
< 300ms
全文日记搜索
< 10s
桌面冷启动

开发团队

北京邮电大学 2026 届 · 数据结构课程项目组

谢佳恒

后端架构 · 核心算法实现

RustAxum算法设计

王逸晨

前端开发 · API 接口设计

ReactTauriUI设计

蔡芝林

数据处理 · 测试验证

数据工程测试OSM