快速排序算法原来这么简单

作者: pujiaxun | 来源:发表于2017-09-14 13:26 被阅读2076次

    原理简述

    快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

    步骤为(From Wikipedia):

    1. 从数列中挑出一个元素,称为"基准"(pivot)
    2. 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
    3. 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。

    简单实现

    先上一个看起来巨简单的实现:

    const quickSort = (arr) => {
      if (arr.length <= 1) {
        return arr;
      }
      const left = arr.filter((e, i) => e < arr[0] && i !== 0);
      const right = arr.filter((e, i) => e >= arr[0] && i !== 0);
      return [...quickSort(left), arr[0], ...quickSort(right)];
      // return quickSort(left).concat([arr[0]], quickSort(right));
    };
    
    const arr = [15, 10, 6, 34, 21, 66, 32];
    console.log(quickSort(arr));
    

    解释起来就很方便,从数组里取出第0个元素作为基准数,然后过滤数组里的元素,比基准数小的,放到left,剩下的放right。当然,要排除第0个元素本身。最后将它们连接起来,两边各自递归下去。

    作为算法呢,用了两次filter其实不太划算,但比这更重要的是,这个实现占用了额外的内存空间。

    原地排序(in-place)

    其实原理也不太难,每次递归,都是将我们的基准数放到它最终应该在的位置

    比如对于arr = [8, 10, 6, 34, 21, 66, 32]这样的数组,我们还是每次取第0个元素作为基准数。

    初始状态:

    NO. Pivot 0 1 2 3 4 5 6
    0 void 8 10 6 34 21 66 32

    将第0个元素8作为基准数,并且把0号的位置挖出来,等待其他元素填入:

    NO. Pivot 0 1 2 3 4 5 6
    0 void 8 10 6 34 21 66 32
    1 8 void 10 6 34 21 66 32

    从右边开始遍历,直到找到一个小于基准数8的6,将其放入0号坑位:

    NO. Pivot 0 1 2 3 4 5 6
    0 void 8 10 6 34 21 66 32
    1 8 void 10 6 34 21 66 32
    2 8 6 10 void 34 21 66 32

    这时再从左边开始遍历,直到找到一个大于基准数8的10,将其放入2号坑位:

    NO. Pivot 0 1 2 3 4 5 6
    0 void 8 10 6 34 21 66 32
    1 8 void 10 6 34 21 66 32
    2 8 6 10 void 34 21 66 32
    3 8 6 void 10 34 21 66 32

    这时再从右边开始遍历,直到找到一个大于基准数8的……没了,那就说明1号坑位就是基准数8的窝了,将它填入:

    NO. Pivot 0 1 2 3 4 5 6
    0 void 8 10 6 34 21 66 32
    1 8 void 10 6 34 21 66 32
    2 8 6 10 void 34 21 66 32
    3 8 6 void 10 34 21 66 32
    4 void 6 8 10 34 21 66 32

    这时候要进入到递归了,我们已经将本次的基准数归位到1号位置了,那么接下来就是要排序arr[0]和arr[2..-1],左边的就一个元素,刚好符合递归终止条件,直接return掉就可以了。右边还有五个元素,按照上述步骤继续递归下去就OK啦~

    JavaScript(ES6) 代码实现:

    const quickSort = (arr, left = 0, right = arr.length - 1) => {
      if (left >= right) {
        return;
      }
    
      let i = left;
      let j = right;
    
      // 取第一个值作为基准值
      let pivotVal = arr[left];
    
      // 取第一个位置作为坑位
      let blankIndex = left;
    
      while (i < j) {
        while (i < j && arr[j] >= pivotVal) {
          j -= 1;
        }
        if (i < j) {
          arr[blankIndex] = arr[j];
          blankIndex = j;
        }
    
        while (i < j && arr[i] <= pivotVal) {
          i += 1;
        }
        if (i < j) {
          arr[blankIndex] = arr[i];
          blankIndex = i;
        }
      }
      arr[blankIndex] = pivotVal;
    
      quickSort(arr, left, blankIndex - 1);
      quickSort(arr, blankIndex + 1, right);
    };
    
    const arr = [15, 10, 6, 34, 21, 66, 32];
    quickSort(arr);
    console.log(arr);
    

    其中blankIndexpivotVal是为了可读性添加的,为了节省点代码,也是可以省略掉的。比如两次判断,可以简化成下面这样,最后i和j相等,所以取哪个都可以。

    while (i < j) {
      while (i < j && arr[j] >= arr[left]) {
        j -= 1;
      }
      if (i < j) {
        arr[i] = arr[j];
      }
    
      while (i < j && arr[i] <= arr[left]) {
        i += 1;
      }
      if (i < j) {
        arr[j] = arr[i];
      }
    }
    arr[i] = arr[left];
    // arr[j] = arr[left];
    

    相关文章

      网友评论

        本文标题:快速排序算法原来这么简单

        本文链接:https://www.haomeiwen.com/subject/nlhdsxtx.html