JS快速排序详解——递归与非递归

版权声明:转载请注明出处 https://blog.csdn.net/xuyangxinlei/article/details/81062015

基本介绍

快速排序(quickSort)是基于冒泡排序的一种改进,该排序算法采用了分治的思想,将待排序数组逐步划分两个部分,其中左半部分都要小于右半部分,再将左右部分分别进行快速排序,整个过程可采用递归进行,直到排成一个有序数列。

整体步骤:

① 选取待排序数组中其中一个数作为基数(建议选取第一个数),使flag等于基数的下标,left等于待排序数组第一个数的下标,right等于待排序数组最后一个数的下标。

② 将数组中比基数小的数放到它的左边,比基数大的数放到它的右边。

③ 将基数左边的数组作为待排序数组,重复①步骤

④ 将基数右边的数组作为待排序数组,重复①步骤

⑤ 直到left >= right,代表数组已经划分为最小单位(待排序数组长度小于等于1),即该部分排序完毕,无需继续分割数组。

详细过程:

括号内为待排序数组

① 待排序数组num:{ 9,17,0,6,10,5 }  其中基数为 9 ,即flag = 0,left = 0,right = 5;

定义i = left,j = right,先使 j 不断左移直到num[j]<num[flag],交换这两个数,并使flag = j;

此时 num:{ 5,17,0,6,10,};

再使 i 不断右移直到num[i]>num[flag],交换这两个数 ,并使flag = i;

此时 num:{ 5,9,0,6,10,17 };

不断重复以上步骤,直到i==j时。

此时 num:{ 5,6,0,9,10,17 };

将基数左右划分为两部分,分别作为待排序数组进行此步骤;

② 即待排序数组num:{ 5,6,0 },9,10,17,执行步骤①后num:{ 0,5,6 },9,10,17 。

接下来排序另一部分num:0,5,6,{ 9,10,17 } ,不需要移动。

③  接下来排num:0,5,6,9,{ 10,17 },也不需要移动( ̄▽ ̄)"

此时数组已经全部排序完成。

特别说明:数据交换步骤可进行优化,如下图所示。

首先num[flag]与num[j]交换,并使flag = j

然后num[flag]与num[i]交换,并使flag = i

两步交换可合二为一,将flag位置赋值成num[j],将j位置赋值成num[i],将i位置赋值成num[flag],再使flag = i,最后结果也如上图所示。

JS代码:

/**
 * 快速排序
 * @param  num 待排序数组
 */
function quickSort(num) {
    _quickSort(num, 0, num.length - 1); // 将整个num数组快速排序,left和right分别指向数组左右两端。
}
/**
 * 快速排序(递归)
 * @param num 待排序数组
 * @param left 左指针
 * @param right 右指针
 */
function _quickSort(num, left, right) {
    if (left >= right) return; // 若左右指针相遇,待排序数组长度小宇1,即递归的终点,return(注意不能写成left==right,这里left是有可能大于right的)。
    var i = left, j = right, flag = left; // 定义可移动的左右指针 i,j,定义flag为基数下标。
    while (i < j) { // 在i<j时不断循环,i一旦与j碰头,则跳出循环。
        while (num[j] >= num[flag] && j > flag) j--; // j不断左移,找到在num[flag]右侧且比它大的数。
        if (i >= j) {
            break; // 由于j可能已被改变,需再次判断i与j是否碰头。
        }
        while (num[i] <= num[flag] && i < j) i++; // i不断右移,找到且比基数小的数,且i不能与j碰头。(由于两次交换已合并,此处不需要使得i在flag左侧)
        // num[flag] num[j] num[i]三者换位,可用ES6语法糖[num[flag],num[j],num[i]] = [num[j],num[i],num[flag]];
        let temp = num[flag]; 
        num[flag] = num[j];
        num[j] = num[i];
        num[i] = temp
        flag = i; // 基数已经在原num[i]的位置,flag同时也要赋值成i。
    }
    _quickSort(num, left, flag - 1); // 将flag左边数组作为待排序数组,递归调用。
    _quickSort(num, flag + 1, right); // 将flag右边数组作为待排序数组,递归调用。
}

当数据量很大的时候,递归快排会造成栈溢出,为解决此问题,我们可使用js数组来模拟栈,将待排序数组的[left,right]保存到数组中,循环取出进行快排,代码如下。

function quickSort(num) {
    _quickSort(num, 0, num.length - 1); // 将整个num数组快速排序,left和right分别指向数组左右两端。
}
/**
 * 快速排序(非递归)
 */

function _quickSort(num, left, right) {
    var list = [[left, right]]; // 将[left,right]存入数组中,类似于递归入栈
    while (list.length > 0) { // 若list不为空,循环弹出list最后一个数组进行快排
        var now = list.pop(); // 弹出list末尾。(也可用list.shift()取出list第一个数组,但在数据量较大时,这种方式效率较低)
        if (now[0] >= now[1]) { // 若左右指针相遇,待排序数组长度小宇1,则无需进行快排(注意不能写成now[0]==now[1],这里now[0]是有可能大于now[1]的
            continue;
        }
        var i = now[0], j = now[1], flag = now[0]; // 以下与递归方法相同,请参考上面的递归详解
        while (i < j) {
            while (num[j] >= num[flag] && j > flag) j--;
            if (i >= j) {
                break;
            }
            while (num[i] <= num[flag] && i < j) i++;
            let temp = num[flag];
            num[flag] = num[j];
            num[j] = num[i];
            num[i] = temp;
            flag = i;
        }
        list.push([now[0], flag - 1]); // 将flag左边数组作为待排序数组,只需将左右指针放入list即可。
        list.push([flag + 1, now[1]]); // 将flag右边数组作为待排序数组,只需将左右指针放入list即可。
    }
}

时间复杂度:

快速排序的时间复杂度并不固定,最理想情况下,当我们每次选取的基数都能平分整个数组,递归迭代的时间复杂度为O(logn),左右指针向基数移动的时间复杂度为O(n),

  1. T(n)≤2T(n/2) +n,T(1)=0  
  2. T(n)≤2(2T(n/4)+n/2) +n=4T(n/4)+2n  
  3. T(n)≤4(2T(n/8)+n/4) +2n=8T(n/8)+3n  
  4. ……  
  5. T(n)≤nT(1)+(log2n)×n= O(nlogn) 

故快排最优情况下的时间复杂度为O(nlogn)。

而最差情况下,每次选取的基数都是数组中的最大或最小值,整个排序过程退化为插入排序,递归迭代时间复杂度为O(n),左右指针向基数移动的时间复杂度为O(n),故快排最差情况下的时间复杂度为O(n^2)。

平均复杂度:快速排序的平均时间复杂度也是:O(nlogn)

空间复杂度:

快排的空间复杂度其实并不好计算,有人使用辅助数组,有人使用就地排序。

在这里我们仅分析占用空间最少的就地排序(递归快排),首先就地快速排序使用的空间是O(1)的,而真正消耗空间的就是递归调用了。

 最优的情况下空间复杂度为:O(logn) 

 最差的情况下空间复杂度为:O( n )  

猜你喜欢

转载自blog.csdn.net/xuyangxinlei/article/details/81062015