排序算法快速排序

Posted 程序员思语

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序算法快速排序相关的知识,希望对你有一定的参考价值。

在做前端工作的时候常常会对数组做处理,也经常会用到排序,今天开始将对常用排序算法做总结,顺便复习一些数据结构知识。
友情提示:阅读本文大概需要 35分钟

前言

快速排序,是从冒泡排序演变而来的算法,也属于交换排序,但是比冒泡算法高效很对,因此得名,也是最经典的排序算法之一,它的核心思想就是分治(小伙伴还记不记得前面介绍的Fish-Redux的核心思想也是分治),快速排序拥有良好的时间复杂度,平均为O(nlog2n),最差为O(n2)。

快速排序

快速排序通过元素之间的比较和交换位置来达到排序的目的,不同的是,冒泡排序在每一轮只把一个元素冒泡到数组的一端,而快速排序在每一轮挑选一个基准元素,并让其他比它大的元素移动到数列一边,比它小的元素移动到数列的另一边,从而把数列拆解成了两个部分。这是分治思想的主要体现,原数组在每一轮被拆分成两部分,每一部分在下一轮又分别被拆分成两部分,直到不可再分为止。平均情况下需要logn轮,因此快速排序算法的平均时间复杂度是 O(nlogn)。

题目背景
// 栗子
var array = [294386215473849 , 352]
JS实现
// JS
function quickSort(arr{
    let piovtIndex = Math.floor(arr.length/2);
    if(arr.length<=1return arr;
    let piovt = arr.splice(piovtIndex,1)[0];
    let _left = []
    let _right = []
    for (var i = 0; i < arr.length; i++) {
        let item = arr[i];
        if(item<piovt) {
            _left.push(item)
        } else {
            _right.push(item)
        }
    }
    return quickSort(_left).concat([piovt],quickSort(_right))
}
C++实现
// C++
#include <iostream>
using namespace std;

void quickSort(int* arr, int start, int end) {
    if (start < end) {
        int i = start, j = end;
        while (i < j) {
            while (arr[i] <= arr[j] && i < j) {
                j--;
            }
            if (i < j) {
                arr[i] = arr[i] ^ arr[j];
                arr[j] = arr[i] ^ arr[j];
                arr[i] = arr[i] ^ arr[j];
                i++;
            }
            while (arr[i] < arr[j] && i < j) {
                i++;
            }
            if (i < j) {
                arr[i] = arr[i] ^ arr[j];
                arr[j] = arr[i] ^ arr[j];
                arr[i] = arr[i] ^ arr[j];
                j--;
            }
        }
        quickSort(arr, i + 1, end);
        quickSort(arr, start, i - 1);
    }
}

int main()
{
    int arr[] = {
        294386215473849 , 352
    };
    quickSort(arr, 017);
    for (int i = 0; i < 18; i++) {
        cout << arr[i] << "   ";
    }
    cout << endl;
    return 0;
}

// 或者
#include <iostream>
using namespace std;

void quickSort(int* arr, int start, int end)
{
    if (start < end) {
        int i = start, j = end, store = arr[start];
        while (i < j) {
            while (i < j && arr[j] >= store) {
                j--;
            }
            if (i < j) {
                arr[i] = arr[j];
            }
            while (i < j && arr[i] < store) {
                i++;
            }
            if (i < j) {
                arr[j] = arr[i];
            }
        }
        arr[i] = store;
        quickSort2(arr, i + 1, end);
        quickSort2(arr, start, i - 1);
    }
}

int main() {
    int arr[] = {
        294386215473849 , 352
    };
    quickSort(arr, 017);
    for (int i = 0; i < 18; i++) {
        cout << arr[i] << "   ";
    }
    cout << endl;
    return 0;
}
局限性:

如果出现一种原本逆序的数组,采用最原始的快速排序的画,就无法发挥分治法的优势了,在最坏情况下,快速排序需要进行N轮比较,时间复杂度会退化到 O(N^2);此外,如果随机选取的基准元素是数组中的最大值or最小值也会到导致分治思想瓦解。

为了避免以上的情况,这个基本元素pivot的选取就需要分情况,pivot的选取直接影响排序的优劣。


①.最简单的方式是选择数列的第一个元素;这种选择在绝大多数情况是没有问题的。但是,假如有一个原本逆序的数列,期望排序成顺序数列,这种排序效率将降到最低。


②.随机选择一个元素作为基准元素;这样即使在数列完全逆序的情况下,也可以有效地将数列分成两部分,但是这样基本元素随机性较强,时间复杂度也不稳定,平均时间复杂度是 O(nlogn),如果选中了数组中最大值or最小值,最坏情况下的时间复杂度是 O(n^2)。


③.挖坑法,用法是闲选定基准元素 pivot,并记住这个位置 index,这个位置相当于一个“坑”。并且设置两个指针 left 和 right,指向数列的最左和最右两个元素;接下来,从right 指针开始,把指针所指向的元素和基准元素做比较。如果比 pivot 大,则 right 指针向左移动;如果比 pivot 小,则把right所指向的元素填入坑中,最终实现比基准元素小的元素全部移到 left 区域,比基准元素大的元素的移动到 right 区域,从而实现排序。


    public static void quickSort(int[] arr, int startIndex, int endIndex{
        // 递归结束条件:startIndex大等于endIndex的时候
        if (startIndex >= endIndex) {
            return;
        }
        // 得到基准元素位置
        int pivotIndex = partition(arr, startIndex, endIndex);
        // 用分治法递归数组的两部分
        quickSort(arr, startIndex, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, endIndex);
    }

    private static int partition(int[] arr, int startIndex, int endIndex{
        // 取第一个位置的元素作为基准元素
        int pivot = arr[startIndex];
        int left = startIndex;
        int right = endIndex;
        // 坑的位置,初始化等于 pivot 的位置
        int index = startIndex;

        // 大循环在左右指针重合或者交错时结束
        while(right >= left) {
            // right 指针从右向左进行比较
            while(right >= left) {
                if(arr[right] < pivot) {
                    arr[left] = arr[right];
                    index = right;
                    left++;
                    break;
                }
                right--;
            }
            // left指针从左向右进行比较
            while(right >= left) {
                if(arr[left] > pivot) {
                    arr[right] = arr[left];
                    index = left;
                    right--;
                    break;
                }
                left++;
            }
        }
        arr[index] = pivot;
        return index;
    }

    public static void main(String[] args{
        int[] arr = new int[] {294386215473849 , 352};
        quickSort(arr, 0, arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

④. 指针交换法,定义两个指针,一前一后,前面指针找比基数小的数,后面指针找比基数大的数,前面的指针找到后,将前后指针所指向的数据交换,当前面的指针遍历完整个数组时,将基数值与后指针的后一个位置的数据进行交换,然后以后指针的后一个位置作为分界,然后将数组分开,进行递归排序。

// 实现
    public static void quickSort(int[] arr, int startIndex, int endIndex{
        // 递归结束条件:startIndex大等于 endIndex的时候
        if(startIndex >= endIndex) {
            return;
        }
        // 得到基准元素位置
        int pivotIndex = partition(arr, startIndex, endIndex);
        // 根据基准元素,分成两部分递归排序
        quickSort(arr, startIndex, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1; endIndex);
    }

    private static int partition(int[] arr, int startIndex, int endIndex{
        // 取第一个位置的元素作为基准元素
        int pivot = arr[startIndex];
        int left = startIndex;
        int right = endIndex;

        while(left != right) {
            // 控制 right 指针比较并且左移
            while(left < right && arr[right] > pivot) {
                right--;
            }
            // 控制right指针比较并且右移
            while(left < right && arr[left] <= pivot) {
                left++;
            }
            // 交换left和right指向的元素
            if(left < right) {
                int p = arr[left];
                arr[left] = arr[right];
                arr[right] = p;
            }
        }

        // pivot和指针重合点交换
        int p = arr[left];
        arr[left] = arr[startIndex];
        arr[startIndex] = p;

        return left;
    }

    public static void main(String[] args{
        int[] arr = new int[] {294386215473849 , 352};
        quickSort(arr, 0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

⑤.非递归的快速排序

public static void quickSort(int[] arr, int startIndex, int endIndex) {
        // 用一个集合栈来代替 递归的函数栈
        Stack<Map<String, Integer>> quickSortStack = new Stack<Map<String, Integer>>();
        // 整个数列的起止下标,以哈希的形式入栈
        Map<K, V> rootParam = new HashMap();
        rootParam.put("startIndex", startIndex);
        rootParam.put("endIndex", endIndex);
        quickSortStack.push(rootParam);

        // 循环结束条件:栈为空时结束
        while(!quickSortStack.isEmpty()) {
            // 栈顶元素出栈,得到起止下标
            Map<String, Integer> param = quickSortStack.pop();
            // 得到基准元素位置
            int pivotIndex = partition(arr, param.get("startIndex"), param.get("endIndex"));
            // 根据基准元素分成两部分,把每个部分的起止下标入栈
            if(param.get("startIndex") < pivotIndex - 1) {
                Map<String, Integer> leftParam = new HashMap<String, Integer>();
                leftParam.put("startIndex", param.get("startIndex"));
                leftParam.put("endIndex", pivotIndex - 1);
                quickSortStack.push(leftParam);
            }
            if(pivotIndex + 1 < param.get("endIndex")) {
                Map<String, Integer> rightParam = new HashMap<String, Integer>();
                rightParam.put("startIndex", pivotIndex + 1);
                rightParam.put("endIndex", param.get("endIndex"));
                quickSortStack.push(rightParam);
            }
        }
    }

    private static int partition(int[] arr, int startIndex, int endIndex) {
        // 取第一个位置的元素作为基准元素
        int pivot = arr[startInxdex];
        int left = startIndex;
        int right = endIndex;

        while(left != right) {
            // 控制 right 指针比较并且左移
            while(left < right && arr[right] > pivot) {
                right--;
            }
            // 控制 right 指针比较并且右移
            while(left < right && arr[left] <= pivot) {
                left++;
            }
            // 交换left 和 right 指向的元素
            if(left < right) {
                int p = arr[left];
                arr[left] = arr[right];
                arr[right] = p;
            }
        }

        // pivot和指针重合点交换
        int p = arr[left];
        arr[left] = arr[startIndex];
        arr[startIndex] = p;

        return left;
    }

    public static void main(String[] args) {
        int[] arr = new int[] {294386215473849 , 352};
        quickSort(arr, 0, arr.length-1);
        System.out.println(Arrays.toString(arr));
    }

总结

在平均状况下,排序 n 个项目要Ο(n log n)次比较。在最坏状况下则需要Ο(n^2)次比较,但这种状况并不常见。事实上,快速排序通常明显比其他Ο(n log n) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。

最差时间复杂度: O(n^2)

最优时间复杂度: O(n log n)

平均时间复杂度: O(n log n)

但当元素个数比较少时(10^2的数量级左右),快排的速度跟冒泡相比并没有快很多,还有如果要排序的元素大部分都已经是排好顺序了时,快排效率会下降,但是其最坏情况是N^2(当元素全部是已经排好的顺序时),一般情况(也即平均效率)是N*Log2(N),最好情况是N(当元素全部是逆序时),快排的特点是元素越乱排序速度越快,所以可以看出,虽然元素少时使用快排并没有很大优势,但是在快排的最坏情况跟冒泡、选择排序(冒泡、选择排序其复杂度不受元素顺序影响,永远为N^2)一样,所以快排永远是最快的。

最后

今天的 排序算法之快速排序 就分享到这里,有问题欢迎大家留言,谢谢 ~ 

以上是关于排序算法快速排序的主要内容,如果未能解决你的问题,请参考以下文章

一行Python代码搞定快速排序算法

交换排序(冒泡排序快速排序的算法思想及代码实现)

排序算法 | 快速排序(含C++/Python代码实现)

[leetcode]排序算法(冒泡排序,选择排序,插入排序,快速排序,计数排序)

十大经典排序算法总结(快速排序)

快速排序算法详解及代码实现