如何通过最少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长度,这是理论上的最小值。
具体步骤:
- 为目标数组建立元素到索引的映射表,方便快速查找元素在目标中的位置。
- 将原数组转换为“目标索引序列”:把原数组每个元素替换成它在目标数组中的索引。
- 找出这个索引序列的最长递增子序列(LIS)——因为目标索引是递增的,所以LIS就对应原数组和目标数组的LCS。
- 确定需要移动的元素:所有不在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
相关产品推荐
相关产品推荐

