让我们一起用js实现快排吧

731 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

困难总是存在的,但依然要勇往直前,直面困难,打碎它。

虽然快排是固定的算法,有其固定的思路,但是我还是想实现一下。尝试使用splice、slice和不使用二者来分别写一下,看一下这其中的差别和性能上是否存在差异。

解题思路

  1. 找到中间位置midVal;
  2. 遍历整个数组,小于midVal的放在left,大于midVal的放在right;
  3. 继续递归;
  4. 使用concat将左中右链接起来。

splice

代码

function quickSortSplice(arr: number[]): number[]{
  const length = arr.length;
  if(length == 0) return arr

  const left: number[] = [];
  const right: number[] = [];

  const midIndex = Math.floor( length / 2 );
  const midVal = arr.splice(midIndex, 1);

  for(let i = 0; i < arr.length; i++){
    if(arr[i] < midVal[0]){
      left.push(arr[i])
    }else{
      right.push(arr[i])
    }
  }
  return quickSortSplice(left).concat(midVal).concat(quickSortSplice(right))
}

功能测试

const quickSortSpliceArr = quickSortSplice([1,3,5,4,2,8,7,6,5,9,0])
console.log(quickSortSpliceArr)//[0, 1, 2, 3, 4, 5, 5, 6, 7, 8, 9]

功能测试符合预期;说明我们写的方法是正确的。

注意事项

  1. 注意splice的使用方法
  2. 在for循环中,注意写i < arr.length,而不是i < length;因为splice会改变原数组,这样就会导致arr.length是一个动态变化的值,这里要注意

slice

代码

function quickSortSlice(arr: number[]): number[]{
  const length = arr.length;
  if(length == 0) return arr

  const left: number[] = [];
  const right: number[] = [];

  const midIndex = Math.floor( length / 2 );
  const midVal = arr.slice(midIndex, midIndex + 1);

  for(let i = 0; i < length; i++){
    if(i != midIndex){
      if(arr[i] < midVal[0]){
        left.push(arr[i])
      }else{
        right.push(arr[i])
      }
    }
  }
  return quickSortSlice(left).concat(midVal).concat(quickSortSlice(right))
}

功能测试

const quickSortSliceArr = quickSortSlice([9,5,4,2,33,4,6,8,4,1,0])
console.info(quickSortSliceArr)

功能测试符合预期;说明我们写的方法是正确的。

注意事项

  1. 注意slice是不会改变原数组的,所以在for循环中可以直接使用length,当然使用arr.length也是可以的
  2. 注意slice和splice第二个参数的区别
  3. 在for循环中需要加入if(i != midIndex)这是为了剔除midIndex,这样在最后拼接的时候才能正常返回

不使用splice和slice

使用splce和slice更主要的是为了对二者进行对比,这里不使用二者,是为了看一下性能问题

代码

function quickSort(arr: number[]): number[]{
  const length = arr.length;
  if(length == 0) return arr

  const left: number[] = [];
  const right: number[] = [];

  const midIndex = Math.floor( length / 2 );
  const midVal = arr[midIndex];

  for(let i = 0; i < length; i++){
    if(i != midIndex){
      if(arr[i] < midVal){
        left.push(arr[i])
      }else{
        right.push(arr[i])
      }
    }
  }
  return quickSortSlice(left).concat([midVal]).concat(quickSortSlice(right))
}

功能测试

const quickSortArr = quickSort([9,5,4,2,33,4,6,8,4,1,0])
console.info(quickSortArr)

功能测试符合预期;说明我们写的方法是正确的。

注意事项

这种写法和slice很像,因为都没有改变原数组,区别就是midVal的获取和最后的拼接,这种写法主要是为了下方进行的性能测试。

时间复杂度分析

在三种写法中,都是一个循环内嵌一个二分查找,所以时间复杂度是O(nlogn),虽然时间复杂度也很高,但是在排序这种动辄就是O(n^2)的时间复杂度的情况下,O(nlogn)已经很不错了

性能分析

  • splice和slice的性能分析
const compareThreeArr1 = [];
for(let i = 0; i < 100 * 10000; i++){
  compareThreeArr1.push(Math.random() * 1000)
}
console.time('splice')
compareThreeArr1.splice(50 * 1000, 1)
console.timeEnd('splice')//splice: 0.784912109375 ms

const compareThreeArr2 = [];
for(let i = 0; i < 100 * 10000; i++){
  compareThreeArr2.push(Math.random() * 1000)
}
console.time('slice')
compareThreeArr2.slice(50 * 1000, 50 * 1000+1)
console.timeEnd('slice')//slice: 0.0087890625 ms


const compareThreeArr3 = [];
for(let i = 0; i < 100 * 10000; i++){
  compareThreeArr3.push(Math.random() * 1000)
}
console.time('---')
compareThreeArr3[50 * 1000+1]
console.timeEnd('---')// ---: 0.00634765625 ms

通过上方进行性能分析:

  1. slice和用下标直接获取的时间复杂度是在同一个数量级的,
  2. splice的时间复杂度相对较高,和前两者的差距大概在100倍左右,已经不是一个数量级了,
  3. 这是在运行一次的前提下,如果运行次数继续增多,时间倍数虽然不会发生大的变化,但实际的时间差却会大很多
  4. 代码在本地运行了不少于10次,结果都和上方结论相近;
  5. 通过上方的结果猜测,quickSortSplice方法的运行时间应该是最长的,另外两个运行时间应该差不多
  • 使用三种方法分别对长度为100万的数组进行排序,查看排序时间
const quickSortSpliceArr1 = [];
for(let i = 0; i < 100 * 10000; i++){
  quickSortSpliceArr1.push(Math.random() * 1000)
}
console.time('quickSortSplice')
quickSortSplice(quickSortSpliceArr1)
console.timeEnd('quickSortSplice')//quickSortSplice: 646.64501953125 ms


const quickSortSliceArr1 = [];
for(let i = 0; i < 100 * 10000; i++){
  quickSortSliceArr1.push(Math.random() * 1000)
}
console.time('quickSortSlice')
quickSortSplice(quickSortSliceArr1)
console.timeEnd('quickSortSlice')//quickSortSlice: 616.028076171875 ms


const quickSortArr1 = [];
for(let i = 0; i < 100 * 10000; i++){
  quickSortArr1.push(Math.random() * 1000)
}
console.time('quickSort')
quickSortSplice(quickSortArr1)
console.timeEnd('quickSort')//564.06884765625 ms

根据上发进行的性能分析,得到的结果是比较相近的,并且在我本地的多次运行这段性能测试的代码后发现:

  1. 三者的时间相差不大(100万长度的数组排序,时间差在几十ms,不到100ms)
  2. 三者时间长短的排序并不是固定的,也就是说在这三种方法中,使用slice、splice和使用下标直接获取中间值对整体性能或者说对时间复杂度的影响并不大。
  3. 算法本身时间复杂度已经很高了,splice和slice在这里的影响已经很小
  4. splice实在二分之后使用的,这也会降低splice对整体时间复杂度的影响
  5. 具体问题要具体分析,不要太想当然了