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

如何通过最少Move操作将数组重排为目标序列?求最优算法

最小化单元素移动操作次数的数组重排方案

我们需要将一个元素互不相同的数组重排为目标数组的顺序,且仅能使用移动操作——把指定元素从原索引位置删除,插入到目标索引位置。举个例子:

Index 0 1 2 3 4
Array e b a c d
Order a b c d e

对应的移动操作示例:

original e b a c d
0→4 b a c d e
1→0 a b c d e

核心目标是最小化移动操作次数,同时生成具体的操作列表(格式:源索引→目标索引)。

应用场景:用JavaScript从服务器获取ID列表并排序,但只能通过单元素「Move到指定索引」的API命令更新排序结果。


现有方案的局限性

方案1

  • 思路:把目标数组的所有元素依次插入到末尾,或者逆序插入到开头
  • 问题:无论数组初始状态如何,都需要执行N次操作(N为数组元素数量),完全没利用已有顺序的优势。

方案2

  • 思路:遍历目标数组,将元素移至对应位置,同时把被挤开的元素移到它的目标位置
  • 问题:仅在元素互换的场景(如e b c d a)中高效,最优情况0次操作,但多数随机场景下操作次数会超过N,甚至找不到上述示例的最优解(示例中仅需2次操作,但该方案可能执行更多次)。

方案2的代码示例:

function arraymove(arr, fromIndex, toIndex) {
    var element = arr[fromIndex];
    arr.splice(fromIndex, 1);
    arr.splice(toIndex, 0, element);
}

function shuffleArray(array) {
    for (var i = array.length - 1; i > 0; i--) {
         var j = Math.floor(Math.random() * (i + 1));
         var temp = array[i];
         array[i] = array[j];
         array[j] = temp;
    }
}

let newOrder = [...Array(20).keys()];
let oldOrder = [...newOrder];
//oldOrder.reverse() //reverse case
shuffleArray(oldOrder); //random case
//arraymove(oldOrder, 0, 99); //end to beginning case

let checkOrder = [...oldOrder];
let moveOrder = [];
for (ind = newOrder.length - 1; ind >= 0; --ind) {
    let el = newOrder[ind];
    if (checkOrder[ind] != el) {
         let oldind = checkOrder.indexOf(el);
         let altel = checkOrder[ind];
         arraymove(checkOrder, oldind, ind);
         moveOrder.push({ el: el, ind: ind, old: oldind });
         let altind = checkOrder.indexOf(altel);

         if (newOrder[newOrder] != altel) {
              let newind = newOrder.indexOf(altel);
              arraymove(checkOrder, altind, newind);
              moveOrder.push({ el: altel, ind: newind, old: altind });
         }
    }
}
document.write("oldOrder: ", oldOrder.join(" "),"<br> Moves: ", moveOrder.length);

最优解决方案:基于最长公共子序列(LCS)

核心思路

要最小化移动次数,本质是找到原数组和目标数组中最长的、顺序一致的子序列(LCS)——这些元素已经处于符合目标的相对顺序中,不需要移动。剩下的元素则需要通过移动来调整位置,总操作次数为 N - LCS长度,这是理论上的最小值。

具体步骤:

  1. 为目标数组建立元素到索引的映射表,方便快速查找元素在目标中的位置。
  2. 将原数组转换为“目标索引序列”:把原数组每个元素替换成它在目标数组中的索引。
  3. 找出这个索引序列的最长递增子序列(LIS)——因为目标索引是递增的,所以LIS就对应原数组和目标数组的LCS。
  4. 确定需要移动的元素:所有不在LIS中的元素,按目标数组的顺序依次将它们移动到对应位置。

JavaScript实现代码

// 找出最长递增子序列的索引
function findLISIndices(sequence) {
    if (sequence.length === 0) return [];
    
    const tails = [];
    const prevIndices = new Array(sequence.length).fill(-1);
    const indices = [];
    
    for (let i = 0; i < sequence.length; i++) {
        let left = 0, right = tails.length;
        while (left < right) {
            const mid = Math.floor((left + right) / 2);
            if (sequence[tails[mid]] < sequence[i]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        
        if (left === tails.length) {
            tails.push(i);
        } else {
            tails[left] = i;
        }
        
        if (left > 0) {
            prevIndices[i] = tails[left - 1];
        }
    }
    
    // 回溯构建LIS的索引
    let current = tails[tails.length - 1];
    const lisIndices = [];
    while (current !== -1) {
        lisIndices.push(current);
        current = prevIndices[current];
    }
    return lisIndices.reverse();
}

// 生成最小移动操作列表
function generateMinMoves(oldArr, targetArr) {
    // 建立目标元素到索引的映射
    const targetIndexMap = new Map();
    targetArr.forEach((el, idx) => targetIndexMap.set(el, idx));
    
    // 将原数组转换为目标索引序列
    const targetSequence = oldArr.map(el => targetIndexMap.get(el));
    
    // 找出LIS的索引(对应原数组中不需要移动的元素)
    const lisOldIndices = findLISIndices(targetSequence);
    const keepOldIndices = new Set(lisOldIndices);
    
    const moves = [];
    const currentArr = [...oldArr];
    
    // 遍历目标数组,处理需要移动的元素
    for (let targetIdx = 0; targetIdx < targetArr.length; targetIdx++) {
        const targetEl = targetArr[targetIdx];
        const currentIdx = currentArr.indexOf(targetEl);
        
        if (currentIdx !== targetIdx) {
            // 记录移动操作:原索引→目标索引
            moves.push({ from: currentIdx, to: targetIdx });
            // 模拟移动操作,更新当前数组
            const [el] = currentArr.splice(currentIdx, 1);
            currentArr.splice(targetIdx, 0, el);
        }
    }
    
    return {
        moveCount: moves.length,
        moves: moves.map(m => `${m.from}→${m.to}`)
    };
}

// 示例使用
const oldArr = ['e', 'b', 'a', 'c', 'd'];
const targetArr = ['a', 'b', 'c', 'd', 'e'];

const result = generateMinMoves(oldArr, targetArr);
console.log('最小移动次数:', result.moveCount);
console.log('操作列表:', result.moves);
// 输出:
// 最小移动次数: 2
// 操作列表: [ '0→4', '1→0' ]

方案优势

  • 保证操作次数是理论最小值,完全利用原数组中已有的正确顺序
  • 逻辑清晰,适用于所有场景,无论是有序、逆序还是随机排列的数组
  • 时间复杂度主要由LIS算法决定,为O(N log N),效率较高

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 07:40:22