数据结构——快速排序

1.原理及代码实现

快速排序
快速排序也是一种分治的排序算法。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序,会需要一个额外的数组;而快速排序的排序方式是当两个子数组都有序时,整个数组就自然有序了,快速排序可以不产生额外的数组。

对于小数组(N<=20),快速排序不如插入排序。所以,小数组建议使用其他排序。

快速排序可以由以下几步组成:

1.如果数组S中的元素个数是0或1,则返回。

2.取S中任一元素v,称为枢纽元。

3.将S中其余元素(除枢纽元)分为两部分,一部分是小于v的,放在v的左边,一部分是大于v的,放在v的右边。

4.再将左右两部分继续递归快速排序。
  
  快速排序是冒泡排序的改进版,也是最好的一种内排序,在很多面试题中都会出现,也是作为程序员必须掌握的一种排序方法。

思想:
1.在待排序的元素任取一个元素作为基准(通常选第一个元素,但最的选择方法是从待排序元素中随机选取一个作为基准),称为基准元素;
2.将待排序的元素进行分区,比基准元素大的元素放在它的右边,比其小的放在它的左边;
3.对左右两个分区重复以上步骤直到所有元素都是有序的。

所以我是把快速排序联想成东拆西补或西拆东补,一边拆一边补,直到所有元素达到有序状态。
在这里插入图片描述

public class QuickSort {

    public static void quickSort(int arr[],int _left,int _right){
        int left = _left;
        int right = _right;
        int temp = 0;
        if(left <= right){   //待排序的元素至少有两个的情况
            temp = arr[left];  //待排序的第一个元素作为基准元素
            while(left != right){   //从左右两边交替扫描,直到left = right

                while(right > left && arr[right] >= temp)  
                     right --;        //从右往左扫描,找到第一个比基准元素小的元素
                  arr[left] = arr[right];  //找到这种元素arr[right]后与arr[left]交换

                while(left < right && arr[left] <= temp)
                     left ++;         //从左往右扫描,找到第一个比基准元素大的元素
                  arr[right] = arr[left];  //找到这种元素arr[left]后,与arr[right]交换

            }
            arr[right] = temp;    //基准元素归位
            quickSort(arr,_left,left-1);  //对基准元素左边的元素进行递归排序
            quickSort(arr, right+1,_right);  //对基准元素右边的进行递归排序
        }        
    }
    public static void main(String[] args) {
        int array[] = {10,5,3,1,7,2,8};
        System.out.println("排序之前:");
        for(int element : array){
            System.out.print(element+" ");
        }
        
        quickSort(array,0,array.length-1);

        System.out.println("\n排序之后:");
        for(int element : array){
            System.out.print(element+" ");
        }

    }

}

算法分析

算法复杂度

最坏时间复杂度 O(n2)O(n2)
最优时间复杂度 O(nlogn)O(nlog⁡n)
平均时间复杂度 O(nlogn)O(nlog⁡n)
空间复杂度 根据实现的方式不同而不同

算法使用分析

  • 快速排序是二叉查找树(二叉查找树)的一个空间最优化版本。不是循序地把数据项插入到一个明确的树中,而是由快速排序组织这些数据项到一个由递归调用所隐含的树中。这两个算法完全地产生相同的比较次数,但是顺序不同。对于排序算法的稳定性指标,原地分区版本的快速排序算法是不稳定的。其他变种是可以通过牺牲性能和空间来维护稳定性的。

  • 快速排序的最直接竞争者是堆排序(Heapsort)。堆排序通常比快速排序稍微慢,但是最坏情况的运行时间总是 O(nlogn) 。快速排序是经常比较快,除了introsort变化版本外,仍然有最坏情况性能的机会。如果事先知道堆排序将会是需要使用的,那么直接地使用堆排序比等待introsort再切换到它还要快。堆排序也拥有重要的特点,仅使用固定额外的空间(堆排序是原地排序),而即使是最佳的快速排序变化版本也需要 O(logn)O(log⁡n) 的空间。然而,堆排序需要有效率的随机存取才能变成可行。

  • 快速排序也与归并排序(Mergesort)竞争,这是另外一种递归排序算法,但有坏情况 O(nlogn)O(nlog⁡n) 运行时间的优势。不像快速排序或堆排序,归并排序是一个稳定排序,且可以轻易地被采用在链表(linked list)和存储在慢速访问媒体上像是磁盘存储或网络连接存储的非常巨大数列。尽管快速排序可以被重新改写使用在链串列上,但是它通常会因为无法随机存取而导致差的基准选择。归并排序的主要缺点,是在最佳情况下需要 Ω(n)Ω(n) 额外的空间。

局限性

  • 当序列长度很小时,快排效率低,研究表明长度在5~25的数组,快排表现不如插入排序。
  • 当pivot选择不当是,会导致树的不平衡,这样导致快排的时间复杂度为O(n2)。
  • 当数组中有大量重复的元素,快排效率将非常之低。
    针对上面提出的快排的局限性,我们依次做出优化策略:

优化

  • 当当前序列长度小于特定值时,直接采用插入排序,或者不做处理,等到快排都执行完毕后(大致有序)在执行一次插入排序。
  • 针对pivot的选择,不再选取固定值,而是采用其他选取策略,如随机、三值取中等。
  • 如果数组中重复元素多,就采用三路划分算法:以某个数为基准将一个数组分成三部分:第一部分表示小于该pivot,第二部分等于pivot,第三部分大于pivot,要得到三部分得区间范围。
/*
* 快速排序3优化1:
* 当排序的子序列小于预定的值M时,采用插入排序
*/

template <typename T>
void PartionInsert(T *array, int left, int right) {
    if (left >= right)
        return;

    if (right - left <= M)
        InsertSort(array, left, right);
    else{
        int i = left;
        int j = right;
        T pivot = array[left];                  // 取第一个数为基准
        while (i < j){                          // 循环直至 i,j 相遇
            while (i < j && array[j] >= pivot)
                --j;
            if (i < j)
                array[i++] = array[j];          // 从右向左扫描,将比基准小的数填到左边
            while (i < j && array[i] < pivot)
                ++i;
            if (i < j)
                array[j--] = array[i];          // 从左向右扫描,将比基准大的数填到右边
        }
        array[i] = pivot;                       // 将基准数填回
        PartionInsert(array, left, i - 1);
        PartionInsert(array, i + 1, right);
    }
}

//产生随机数
template <typename T>
void Random(T *array, int left, int right)
{
    int size = right - left + 1;
    int i = left + rand() % size;
    swap(array[i], array[left]);
}

//取中位数移至left
template <typename T>
void Median(T *array, int left, int right)
{
    int mid = left + ((right - left )>> 1);
    int minIndex = right;

    if (array[minIndex] > array[mid])
        minIndex = mid;
    if (array[minIndex] > array[left])
        minIndex = left;
    if (minIndex != right)                      //三个判断,把最小值移到最右侧
        swap(array[minIndex], array[right]);
    if (array[mid] < array[left])               //那么剩下的两个数,最小的那个就是中位数了
        swap(array[left], array[mid]);
}

/*
* 快速排序3优化2:
* 取随机数或者三值取中作为基准值
*/

template <typename T>
void PartionSecond(T *array, int left, int right) {
    if (left >= right)
        return;

//  Random(array, left, right);                 // 优化2-1:取随机数至最左端(基准值)
    Median(array, left, right);                 // 优化2-2:取中位数至最左端(基准值)
    int i = left;
    int j = right;
    T pivot = array[left];                      // 取第一个数为基准
    while (i < j){                              // 循环直至 i,j 相遇
        while (i < j && array[j] >= pivot)
            --j;
        if (i < j)
            array[i++] = array[j];              // 从右向左扫描,将比基准小的数填到左边
        while (i < j && array[i] < pivot)
            ++i;
        if (i < j)
            array[j--] = array[i];              // 从左向右扫描,将比基准大的数填到右边
    }
    array[i] = pivot;                           // 将基准数填回
    PartionSecond(array, left, i - 1);
    PartionSecond(array, i + 1, right);
}

/*
* 快速排序3优化3:
* 重复数据比较多的话,可以分为小于等于大于三段
*/

template <typename T>
void PartionThird(T *array, int left, int right) {
    if (left >= right)
        return;

    int less = left;
    int greater = right;
    int it = left;
    T pivot = array[left];                      // 取第一个数为基准
    while (it <= greater){                      // 循环直至it和greater相遇
        if (array[it] == pivot)                 // 如果等于pivot,it右移
            ++it;
        else if (array[it] < pivot){            // 如果小于pivot,扔左边,it和less右移
            swap(array[less], array[it]);
            ++it;
            ++less;
        }
        else{                                   // 如果大于pivot,扔右边,greater左移
            swap(array[greater], array[it]);
            --greater;
        }
    }
    PartionThird(array, left, less - 1);
    PartionThird(array, greater + 1, right);
}

猜你喜欢

转载自blog.csdn.net/qq_30281559/article/details/88972613