[前端]_重学js中的快速排序

158 阅读6分钟

前言

上个月我贡献了掘金中的第一篇文章,小白也能看懂的五个排序算法解析, 给大家讲解了几个常用排序算法的白话文,然后有小伙伴向我提出建议,如果有代码附带一起说感觉效果会更好,于是我又来了。让每篇文章专注做一件事,总结文是让大家明白每个排序算法的思路和区别,单篇文章负责带领大家入门某个排序算法并进行实战。

正题

首先,跟大家说明白,快速排序的原理: 就是通过建立一个基准值(可以直接用数组第一个元素),把大于基准值的元素放到左半部分,小于基准值的元素放到右半部分。然后再对左右两个部分,分别进行快速排序,直到左右部分里的元素小于或者等于1个的时候终止。

暴力写法

思路

开辟左右两个部分的数组,把小于基准值的元素放到左边数组,大于的元素放到右边数组,然后如果左右数组的长度大于1,则递归执行。

实现

function quickSort(arr) {
    // left存放比arr[0]小的, right存放比arr[0]大的
    let left = [], right = [];
    for (let i = 1; i < arr.length; i++) {
        if (arr[i] < arr[0]) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    
    // 如果长度大于1,递归进行快排
    if (left.length > 1) {
        left = quickSort(left);
    }
    if (right.length > 1) {
        right = quickSort(right);
    }

    return [ ...left, arr[0], ...right ];
};

这样子一个简单的快速排序就实现了,不过这里用到了额外的空间,每次需要开辟左右两个数组去存储元素,这样子数据量大的时候,特别耗空间。

优雅写法

思路

于是我们对上面的代码进行一次优化,我们每次不开辟空间了,只记录当前的基准值的索引,然后把比基准值小的放到前面去,比基准值大的放到后面。但是这样子每次我们操作的时候,就需要执行三个动作:

  1. 删除节点, 从原本位置删掉;
  2. 插入节点, 插入最前面或者最后面;
  3. 变更索引, 如果前面插入了节点,基准值所在位置要加一,后面插入则减一。

这样子代码写完心智负担挺重的,因为索引不断被打乱,导致我们循环过程中本来该进行到的某个元素,会被意外变成另一个元素,那么如何在不变更原来的元素时进行呢?

首先我们把数据分成三部分:

基准值、 [比基准值小的]、 [比基准值大的]

我们把第一个元素作为基准值,然后只要去维护比基准值小的部分,剩余的自然是比基准值大的了。最终我们只需要把基准值调个位置,换到中间去就实现了一轮快速排序,然后递归进行即可。

[比基准值小的]、基准值、 [比基准值大的]

我们要做的是统计当前一共有几个比基准值小的元素,把他们放在前半段即可。最终数量就是我们基准值所在的索引。

实现

function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left < right) {
        let index = partition(arr, left, right);
        quickSort(arr, left, index - 1);
        quickSort(arr, index + 1, right);
    }

    return arr;
};

// 寻找基准值的位置
// 通过left和right去记录从哪排到哪
function partition(arr, left, right) {
    // 记录有多少个比基准值小的元素, 记得索引值 + left
    let index = left;

    // 记得是从left开始,不是从0开始
    for (let i = left + 1; i <= right; i++) {
        if (arr[i] < arr[left]) {
            index++;
            // 交换位置,把小于的都放前面来
            swap(arr, i, index);
            
        }
    }

    // 最终把基准值放到两部分数据中间
    swap(arr, left, index);

    return index;
}

// 交换节点的值
function swap(arr, i, j) {
    if (i !== j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }  
}

实战

215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

 

示例 1:

输入: [3,2,1,5,6,4]k = 2
输出: 5

示例 2:

输入: [3,2,3,1,2,4,5,5,6]k = 4
输出: 4

 

提示:

  • 1 <= k <= nums.length <= 104
  • -104 <= nums[i] <= 104

思路

先给整个数组做个快速排序,然后返回索引值为k - 1的元素即可。刚刚我们一直是从小到达排序的,现在要改成从大到小排序了,其实只需要在我们刚刚的代码改一个符号即可。

实现

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    let reuslt = quickSort(nums);
    return reuslt[k - 1];
};

function quickSort(arr, left = 0, right = arr.length - 1) {
    if (left < right) {
        let index = partition(arr, left, right);
        quickSort(arr, left, index - 1);
        quickSort(arr, index + 1, right);
    }

    return arr;
};

function partition(arr, left, right) {
    let index = left;

    for (let i = left + 1; i <= right; i++) {
        // 从大到小和从小到大改这个符号即可
        if (arr[i] > arr[left]) {
            index++;
            swap(arr, i, index);
            
        }
    }

    swap(arr, left, index);

    return index;
}

function swap(arr, i, j) {
    if (i !== j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }  
}

优化

这道题目人家只要求出第k位,所以我们只需要求出相应的值即可,至于前半部分和后半部分的顺序我们并不关心。于是这道题目可以做如下优化:

  1. 确定我们要找的值所在区间,超出区间的我们不去排序;
  2. 每一轮找到基准值的位置,判断一下我们要找的元素是再基准值的左边还是右边,每次只需要对两者其一进行排序即可。

优化代码

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
var findKthLargest = function(nums, k) {
    return quickSort(nums, k);
};

function quickSort(arr, k, left = 0, right = arr.length - 1) {
    if (left < right) {
        let index = partition(arr, left, right);

        // 确定我们要找的值所在区间,超出区间的我们不去排序
        if (index === k - 1) {
            return arr[index];
        } else if (index > k - 1) {
            return quickSort(arr, k, left, index - 1);
        } else {
            return quickSort(arr, k, index + 1, right);
        }
    }
    
    return arr[left];
};

function partition(arr, left, right) {
    let index = left;

    for (let i = left + 1; i <= right; i++) {
        // 从大到小和从小到大改这个符号即可
        if (arr[i] > arr[left]) {
            index++;
            swap(arr, i, index);
            
        }
    }

    swap(arr, left, index);

    return index;
}

function swap(arr, i, j) {
    if (i !== j) {
        const temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }  
}

总结

学习排序主要学的是排序的思路,解决问题的思想。而不单单只是为了学习这固定的十几行代码。只有当你武器库足够丰富,你才可以选择用什么武器去杀敌,而不是有什么就拿什么。