算法题:洗牌算法

一杯绿茶
English

以前面试的时候考过洗牌算法,当时写的比较简陋,核心思路如下:

  • 创建一个空的结果数组;
  • 依次从原数组中随机取样,然后按顺序 push 到结果数组中;
  • 为防止出现碰撞,每次取样后,将该元素从原数组删除;
其实还有一种思路,按顺序从原数组中获取元素,然后放到新数组的随机位置,但是这样无法消除碰撞的问题
function shuffle(arr) {
  let len = arr.length;
  let res = [];
  for (let i=0; i<len; i++) {
    res.push(sample(arr));
  }
  return res;
}

function sample(arr) {
  // 数组随机取样
  let randIndex = Math.floor(Math.random() * arr.length);
  // 获取元素并从原数组中删除
  let [ele] = arr.splice(randIndex, 1);
  return ele;
}

可以看到上面的代码存在两个问题:

  • 创建新数组,导致使用了额外空间;
  • 从数组中删除元素需要移动其他元素下标,导致总的时间复杂度为 O(n^2)

那么有没有一种方法可以不创建新数组,直接在原数组上修改呢?今天看到一种 Fisher-Yates 洗牌算法,思路与上面代码类似,但没有创建新数组,而是采用了与快排类似的元素交换方法:

  • 从数组最后一个元素开始倒序遍历;
  • 从包含当前元素在内的数组中,随机抽取元素,与当前元素交换;
  • 不断往前遍历,直到所有元素都被交换;
function randomIndex(index: number): number {
  return Math.floor(Math.random() * (index + 1));
}

function swap<T>(arr: T[], a: number, b: number) {
  let temp = arr[a];
  arr[a] = arr[b];
  arr[b] = temp;
}

export function shuffle<T>(arr: T[]): T[] {
  for (let i=arr.length-1; i>=0; i--) {
    let r = randomIndex(i);
    swap<T>(arr, r, i);
  }
  return arr;
}

const res = shuffle<number>([1, 2, 3, 4, 5, 6]);
console.log(res);

从代码中可以看出,每一步都是从数组中随机抽取元素,放到数组的最后,然后指针前进一位,继续从剩下数组中抽取元素,放到数组的最后。按这个过程依此类推,最终整个数组的元素就被打乱了。实现的思路其实和本人之前的代码是一样的,但是有两点改进:

  • 没有创建新数组,空间复杂度为 O(n)
  • 通过元素交换的方法,避免了从数组中删除元素导致需要移动其他元素下标的问题,时间复杂度为 O(n)
阅读 116

人在一起就是过节,心在一起就是团圆

140 声望
13 粉丝
0 条评论
你知道吗?

人在一起就是过节,心在一起就是团圆

140 声望
13 粉丝
宣传栏