You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

连续存储不规则数组的原地随机洗牌实现方案咨询

嘿,针对你提到的这种大尺寸不规则数组(ragged array)洗牌的问题,我刚好有几个实用的方案,既可以节省内存,又能满足洗牌需求,我按实现难度和内存开销给你梳理一下:

方案1:间接索引表(最低实现复杂度,低内存开销)

这绝对是最实用的方案——完全不用修改原始的values、shape和offsets数组,只需要维护一个行索引数组就行。你对这个索引数组执行Fisher-Yates洗牌,后续访问数组时通过索引映射到原始行即可。

为啥推荐它?

  • 内存开销极小:仅需要一个长度等于行数的Vec<usize>,就拿1亿行来说,8字节/元素的话也就800MB,远小于复制整个values数组的开销(按每行平均10个4字节浮点数算,1亿行就是40GB,根本没法比)。
  • 实现超简单,完全不碰原始数据,避免了内存移动带来的性能损耗。
  • 还能随时恢复原始顺序,只要保留好初始的索引数组就行。

Rust示例代码:

use rand::Rng;
use rand::seq::SliceRandom; // 嫌手动写麻烦的话,用这个库方法也行

// 生成洗牌后的行索引数组
fn shuffle_rows_with_index(rows_count: usize) -> Vec<usize> {
    // 先创建初始索引:0,1,2,...,rows_count-1
    let mut indices: Vec<usize> = (0..rows_count).collect();
    
    // 手动实现Fisher-Yates洗牌(也可以直接用indices.shuffle(&mut rng))
    let mut rng = rand::thread_rng();
    for i in (1..rows_count).rev() {
        let j = rng.gen_range(0..=i);
        indices.swap(i, j);
    }
    
    indices
}

// 通过索引数组访问洗牌后的行
fn access_shuffled_row(
    indices: &[usize],
    row_idx: usize,
    values: &[f32],
    offsets: &[usize],
    shape: &[usize]
) -> &[f32] {
    let original_row = indices[row_idx];
    let start = offsets[original_row];
    let len = shape[original_row];
    &values[start..start+len]
}

方案2:原地行交换(低额外内存,修改原始数据)

如果你必须修改原始数组的存储顺序(比如后续需要直接遍历values就能拿到洗牌后的行),那可以基于Fisher-Yates的思路,直接交换两行的内存块,同时更新shape和offsets数组。这里只需要一个临时缓冲区,大小等于最长行的长度——你说每行最多20个4字节浮点数,也就是80字节,这点内存完全可以忽略不计。

核心步骤:

  1. 从最后一行往前遍历(Fisher-Yates的逆序遍历逻辑)。
  2. 随机选一个j(0≤j≤当前行索引i),交换行i和行j。
  3. 交换的时候要注意:
    • 先把两行的长度存下来,因为后续要调整内存和偏移量。
    • 如果两行长度不一样,得先移动中间的内存块腾出空间,再交换数据。
    • 最后更新shape数组,还要调整受影响的后续行的offsets值。

Rust示例代码片段(核心交换逻辑):

use rand::Rng;

// 交换两行的核心逻辑
fn swap_rows(
    values: &mut [f32],
    shape: &mut [usize],
    offsets: &mut [usize],
    i: usize,
    j: usize,
) {
    if i == j {
        return;
    }
    // 统一让i大于j,方便后续处理内存块移动
    let (i, j) = if i < j { (j, i) } else { (i, j) };
    
    let len_i = shape[i];
    let len_j = shape[j];
    let start_i = offsets[i];
    let start_j = offsets[j];
    
    // 先把行i的数据存到临时缓冲区
    let mut temp = vec![0.0; len_i];
    temp.copy_from_slice(&values[start_i..start_i+len_i]);
    
    // 处理长度不同的情况,移动中间的内存块
    if len_i > len_j {
        // 行i更长,需要把中间的块后移(腾出空间给行j的数据)
        let shift = len_i - len_j;
        values.copy_within(start_j+len_j..start_i, start_j+len_j+shift);
    } else if len_i < len_j {
        // 行i更短,需要把中间的块前移(填补行j数据移走后的空隙)
        let shift = len_j - len_i;
        values.copy_within(start_i..start_j+len_j, start_i-shift);
    }
    
    // 把行j的数据移到行i的位置
    values.copy_within(start_j..start_j+len_j, start_i);
    // 把临时存储的行i数据移到行j的位置
    values[start_j..start_j+len_i].copy_from_slice(&temp);
    
    // 更新shape数组
    shape.swap(i, j);
    
    // 更新offsets数组中受影响的部分
    let delta = len_i as isize - len_j as isize;
    // 先更新j+1到i的偏移量
    for k in j+1..=i {
        offsets[k] = (offsets[k] as isize + delta) as usize;
    }
    // 如果i不是最后一行,还要更新i之后的所有偏移量
    if i < offsets.len() - 1 {
        for k in i+1..offsets.len() {
            offsets[k] = (offsets[k] as isize + delta) as usize;
        }
    }
}

// 原地洗牌整个数组
fn shuffle_rows_in_place(
    values: &mut [f32],
    shape: &mut [usize],
    offsets: &mut [usize],
) {
    let rows_count = shape.len();
    let mut rng = rand::thread_rng();
    
    for i in (1..rows_count).rev() {
        let j = rng.gen_range(0..=i);
        swap_rows(values, shape, offsets, i, j);
    }
}

方案3:原地重排(零额外内存,极高实现复杂度)

如果你的内存极端受限,连索引数组的几百MB都拿不出来,那可以试试类似原地排序的“循环置换”思路:把每个行的位置看作一个置换环,沿着环把行移动到目标位置,全程只用少量临时空间存当前行的数据。不过这个方案实现起来特别复杂,调试难度也大,除非真的走投无路,不然不推荐用。

核心思路:

  1. 生成随机置换,但不用存储整个置换数组,而是用一个标记数组记录哪些行已经处理过。
  2. 对每个未处理的行,沿着置换环依次移动行到目标位置,每次移动都调整values、shape和offsets。
  3. 标记数组用Vec<bool>就行,内存开销是1字节/元素,1亿行也就100MB,比索引数组更小。

内容的提问来源于stack exchange,提问作者MSR

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:12:27