[路飞]_leetcode刷题_快速排序

151 阅读4分钟

写法一

不改变原数组,创建新数组来处理,这里我指针取的是长度1/2位置,建议取随机位置,这样对于升序和降序几乎已经排序好的数组效率更高。

代码如下

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var sortArray = function(nums) {
    // 数组长度
    let len = nums.length;
    // 如果数组长度为0,则返回空数组
    if(len==0) return [];
    // 确定指针,为长度1/2位置
    let pivot = Math.floor(len/2);
    let left=[],right=[];
    // 遍历数组,如果大于等于指针,则放到右边,小于指针放到左边,指针元素不动
    for(let i=0;i<nums.length;i++){
        if(i==pivot) continue;
        if(nums[i]<nums[pivot]){
            left.push(nums[i])
        }else if(nums[i]>=nums[pivot]){
            right.push(nums[i])
        }
    }
    // 递归去处理左区间和右区间
    let leftSort = sortArray(left);
    let righSort = sortArray(right);
    // 将左区间、指针位置的值、右区间合并
    return [...leftSort,nums[pivot],...righSort]
};

写法二

单指针基础版

相比于写法一

优点:原数组上排序,减少空间复杂度。

缺点:把等于pivot的所有元素分到了数组的右侧,可能会造成递归树倾斜;

代码如下

/**
 * 该版本会在原数组上处理,空间复杂度会更小
 * 把等于切分元素的所有元素分到了数组的同一侧,可能会造成递归树倾斜;
 * @param {number[]} nums
 * @return {number[]}
 */
 var sortArray = function (nums) {
    let len = nums.length;
    // 调用快速排序递归算法
    quickSort(nums, 0, len - 1);
    return nums;
};

function quickSort(nums, left, right) {
    // 递归终止条件为left>=right
    // 因为如果是这种情况[1,2,3,4,5,6,7,8],这里只是举一个数组的例子,里面的值无关紧要
    // pivot如果是0,那左侧没有值了,无需排序,这种情况就是left>right
    // pivot如果是1,那左侧只有一个值,也无需排序,这种情况就是left==right
    // 同理,pivot是7和6,也不用排序,这种情况也是left>right和left==right
    if(left < right){
        // 计算隔板pIndex,并根据pIndex分好左右区间数组
        let pIndex = partition(nums, left, right);
        quickSort(nums, left, pIndex - 1);
        quickSort(nums, pIndex + 1, right);
    }
}

function partition(nums, left, right) {
    // 在left和right区间内,随机选取一个值当做pivot
    // 这里Math.floor(Math.random()*(right-left+1))计算的是随机区间长度
    // +left是加了一个偏移量,如果假设要求[3,6]区间的随机数,随机长度为3,偏移量为3
    let randomIndex = Math.floor(Math.random()*(right - left + 1)) + left;
    // 交换左边界和随机index
    swap(nums, left, randomIndex);
    // 将左边界当做基准值,其实这时候的左边界值就是刚才随机生成的randomIndex对应的值
    let pivot = nums[left];

    let lt = left;//lt 的含义就是,在lt及lt的左边都是小于pivot的值
    // 循环不变量:
    // left+1为pivot左区间的左边界
    // lt为pivot左区间的右边界
    // lt+1为pivot右区间的左边界,注意lt+1位置是留给pivot的
    // i为右区间的右边界
    // 即
    // all in [left + 1, lt] < pivot
    // all in [lt + 1, i) >= pivot
    // 这里要注意循环区间为[left+1,right],因为left项为pivot,不用管它
    for (let i = left + 1; i <= right; i++) {
        // 当前项小于pivot,那么我们先要将lt++;因为当前lt及lt左侧代表的是小于pivot的值
        // 那lt右侧第一个位置就是我们可以用来存放下一个小于pivot值的位置
        // 所以我们先把[left+1,lt]这个区间扩容
        // 然后再将应该进入这个区域的nums[i]与新扩容的位置值交换
        if (nums[i] < pivot) {
            lt++;
            swap(nums, i, lt);
        }
    }
    // 此时,我们需要将pivot放到它应该在的位置
    // 分析可得,此时lt位置的值是小于pivot的,lt+1位置的值是大于等于pivot的
    // 那么我们将pivot和lt交换后,即可保证,pivot左侧都小于pivot,右侧都大于等于pivot
    swap(nums, left, lt);
    // 交换之后,这个lt就是pivot的下标了
    return lt;
}
function swap(nums, index1, index2) {
    let temp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = temp;
}

写法三

双指针。

相较于写法二

优点:把等于pivot的所有元素等概率地分到了左右两区间,避免了递归树倾斜,递归树相对平衡;

代码如下

/**
 * 原位交换双指针法
 * @param {number[]} nums
 * @return {number[]}
 */
 var sortArray = function(nums) {
    quickSort(nums,0,nums.length-1);
    return nums;
};

function quickSort(nums,left,right){
    if(left<right){
        let pIndex = partition(nums,left,right);
        quickSort(nums,left,pIndex-1)
        quickSort(nums,pIndex+1,right)
    }
}

function partition(nums,left,right){
    // 这里Math.floor(Math.random()*(right-left+1))计算的是随机区间长度
    // +left是加了一个偏移量,如果假设要求[3,6]区间的随机数,随机长度为3,偏移量为3
    let randomIndex = Math.floor((right-left+1)/2+left);
    // let randomIndex = Math.floor(Math.random()*(right-left+1))+left;
    swap(nums,left,randomIndex)
    let pivot = nums[left];
    let lt = left+1;
    let gt = right;
    // 循环不变量:
    // all in [left + 1, lt) <= pivot
    // all in (gt, right] >= pivot
        while (true) {
            // lt从左到右移动,只要值小于pivot,则将左区间扩容
            while (lt <= right && nums[lt] < pivot) {
                lt++;
            }
            // gt从右到左移动,只要值大于pivot,则将右区间扩容
            while (gt > left && nums[gt] > pivot) {
                gt--;
            }
            // 正常结束循环是lt > gt
            // 当最后lt和gt夹逼到中间,只剩最后一个值且等于pivot的时候,此时lt会等于gt,就不用再交换了,直接退出
            if (lt >= gt) {
                break;
            }

            // 细节:相等的元素通过交换,等概率分到数组的两边
            // 这里交换的情况有几种情况需要明白
            // 1. lt对应的值大于或等于pivot
            // 2. gt对应的值小于或等于pivot
            // 那么除了会将lt对应的大值丢到右边,gt对应的小值丢到左边外
            // 还会把等于pivot的值往两边丢,但这不影响,两边还会继续递归排序的
            swap(nums, lt, gt);
            lt++;
            gt--;
        }
        // 这里为什么是pivot和gt交换,而不是和lt交换呢?
        // 因为pivot所在的位置是左区间,将pivot换到中间位置后,应该将属于左区间的值换过来才对
        // 而结束循环后,gt对应的值一定小于等于pivot,lt对应的值一定大于等于pivot
        // 所以我们应该跟gt交换
        swap(nums, left, gt);
        return gt;
}

function swap(nums,i,j){
    let temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}

写法四

三指针。

相较于写法三

优点:把等于pivot的所有元素挤到了中间区间,分治的时候,只用去处理左区间和右区间,在有很多元素和pivot相等的情况下,递归区间大大减少。

代码如下

/**
 * 三指针。把等于切分元素的所有元素挤到了数组的中间,在有很多元素和切分元素相等的情况下,递归区间大大减少。
 * @param {number[]} nums
 * @return {number[]}
 */
 var sortArray = function(nums) {
    quickSort(nums,0,nums.length-1);
    return nums;
};

function quickSort(nums,left,right){
    if(left<right){
        let [lt,gt] = partition(nums,left,right);
        quickSort(nums,left,lt);
        quickSort(nums,gt,right);
    }
}

function partition(nums,left,right){
    let randomIndex = Math.floor(Math.random()*(right-left+1))+left;
    swap(nums,left,randomIndex);
    let pivot = nums[left];
    // 左区间右边界
    let lt = left;
    // 右区间左边界
    let gt = right+1;
    // 循环下标,从left+1开始循环
    let i = left+1;
    // all in [left+1,lt] <pivot
    // all in [lt+1,i) == pivot,
    // all in [gt,right] < pivot
    // 循环在i>=gt时结束,即最后遍历的值是gt左边的那个值,因为gt都是大于pivot的,没必要再往后遍历了,否则既浪费时间,循环内判断还得大改
    while(i<gt){
        // 当前值小于pivot,则左区间扩容,交换当前值到左区间,下标++
        if(nums[i]<pivot){
            lt++;
            swap(nums,i,lt);
            i++;
        }else if(nums[i] == pivot){
            // 当前值等于pivot,那么直接往前进,后面如果遇到了小于pivot的值,会把这个相等的值再丢回中间
            // 如果后面不会再遇到小于pivot的值,那么这个相等的值等于是已经在中间了
            // 这个就能保证最终会将等于pivot的值夹逼到中间区间,为我们后序分治节省时间埋下伏笔
            i++;
        }else{
            // 当前值大于pivot,则右区间扩容,交换当前值到右区间
            // 由于交换过来的值是多少不知道,还需要判断交换过来的值,所以i不能够++
            gt--;
            swap(nums,i,gt);
        }
    }
    // 最后由于lt对应的值是左区间的右边界,所以将pivot和它交换可以保证pivot左区间仍然都是小于pivot的
    swap(nums, left, lt);
    // 返回左区间和右区间的边界下标
    return [lt,gt];
}

function swap(nums,i,j){
    if(i==j)return;
    let temp = nums[i];
    nums[i] = nums[j];
    nums[j] = temp;
}