排序(重点介绍快速排序的各种场景, 堆排序的数组空洞)

Posted milaiko

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了排序(重点介绍快速排序的各种场景, 堆排序的数组空洞)相关的知识,希望对你有一定的参考价值。

目录

文章目录

排序算法

十大排序算法总览

快速排序(重点)

基本思路

  • 快速排序每一次都排定一个元素(这个元素被放在了它最终应该呆的位置) , 然后递归地去排它左边的部分和右边的部分。 依次进行下去, 知道数组有序。
  • 算法思想: 分治思想。 和【归并排序】不同, 快排在【分】这个事情上不像【归并排序】一样一分为二, 而是利用partition的方法, 因此就没有【合】的过程。
  • 实现细节(如果是顺序数组或者逆序数组)一定要随机化选择切分元素, 否则在输入数组是有序数组或者是逆序数组的时候, 快速排序就会变得非常慢。

经典题目

数组中的第k个最大元素

颜色分类

快排的基本做法

#include<iostream>
using namespace std;
// 选择基准
int partition(int array[], int low, int high)
    while(low < high)
        // low 位置为bound, low 右边的不小于low左边的
        // 从右往左找一个小于6的数, 再从左往右找一个大于6的数
        while(low < high && array[low] <= array[high])
            high--;
        
        int tmp = array[low];
        array[low] = array[high];
        array[high] = temp;
        
        while(low < high && array[low] <= array[high])
            low++;
        
        tmp = array[low];
        array[low] = array[high];
        array[high] = temp;
    
    return low;


void quickSortHelp(int array[], int low , int high)

    if(low < high)
    
        int location  = parition(array, low, high);
        quickSortHelp(array, low,location - 1);
        quickSortHelp(array, location + 1, high);
    


void quickSort(int array[], int n)
    qickSortHelp(array, 0, n-1);

快排的随机化版本

通过随机化选择基准数, 避免出现O(n^2)的情况。 当然也可以使用STL的三点中值, 取整个序列的头、尾、中央三个位置的元素, 以其中值(median)作为基准数。 这个就是median-of-three partition

class solution
public:
    vector<int> sorrArray(vector<int>& nums)
        srand(time(0));
        quicksort(nums, 0, nums.size());
        return nums;
    
    inline int partition(vector<int>& nums, int left, int right)
        int x = nums[right], i = left - 1;
        for(int j = left;j < right;++j)
            if(nums[j] <= x)
                swap(nums[++i], nums[j]);
            
        
        swap(nums[i+1], nums[right]);
        return i+1;
    
    
    inline int randomParition(vector<int>& nums, int left, int right)
        int i = rand() % (right - left + 1) + left;
        swap(nums[i], nums[right]);
        return partition(nums, left , right);
    
    
    void quicksort(vector<int>& nums, int left, int right)
    
        if(left < right)
        
            int location = randomPartition(nums, left, right);
            quicksort(nums, left, location-1);
            quicksort(nums, location+1, right);
        
    

如果随机化版本也是O(n^2)

在原始快排中, 如果输入的序列是正序的或者是逆序的, 那么它就会到达O(n^2)的时间复杂度, 于是我们是用来随机化算法, 来生成基准数。 但是即便是引入了随机化算法, 依旧有可能会到达O(n^2)。 这时候可以通过检查递归深度, 将之转换为堆排序。

根据数据量大小来使用快排

在STL中的sort算法, 数据量大是采用快速排序 ,分段递归排序;

一旦分段后的数据量小于某个门槛, 为避免快排的递归调用带来过大的额外负荷(会有额外空间消耗), 就改用插入排序

如果递归层次过深, 还会改用堆排序, 因为快排不稳定, 在数据量大的时候很有可能退化成O(n^2)的复杂度。

怎么看这个递归深度(怎么自我检测)

在SGI中, 是通过IntroSort来实现。 而Introsort可以通过判断递归深度来决定转化为堆排序

template <class RandomAccessIterator, class T, class Size>
    void __introsort_loop(RandomAccessIterator first, 
                         RandomAccessIterator last, T*,
                         Size depth_limit)

    while(last - first > __stl_threshold)
    
        if(depth_limit == 0) //这时候, 可用的递归深度已经被用完了
        
            partial_sort(first, last, last);
            return ;
        
        --depth_limit;
        // 接下来是 median-of-3 partition , 选择一个够好的枢轴并决定分裂点
        RandomAccessIterator cut = __upguarded_partition(first, last, T(__median(*first, *(first + (last - first)/2), *(last- 1))));
        // 对右半边递归进行sort
        __introsort_loop(cut, last, value_type(first), depth_limit);
        last = cut;
        //回到while循环, 准备对左半段递归进行sort
    


// 其中的depth_limit的参数
template<class RandomAccessIterator>
    inline void sort(RandomAccessIterator first, 
                    RandomAccessIterator last)

    if(first != last)
    
        __introsort_loop(first, last, value_type(first), __lg(last-first)*2);
        __final_insertion_sort(first, last);
    


其中控制分割恶化情况的就是

template <class Size>
    inline Size __lg(Size n)

    Size k;
    for(k = 0;n > 1;n >>= 1)
        ++k;
    return k;

// 当Size 为40, 2^5 <= 40; 所以当数据量为40的时候, 递归深度不能够超过5*2 = 10;

多小的数据量使用插入排序

在STL中,其实5-20的区间内差别不大。 通常在这个区间内转化位插入排序

而插入排序在小数据量的时候相比于快排

  • 没有额外的内存的申请和释放开销
  • 没有递归栈的开销

为什么快排是不稳定的

稳定性指的是:两个值相同的元素在排序前后是否有位置变化。 如果前后位置变化, 排序算法是不稳定的, 否则是稳定的。

那为什么快排不稳定?为什么相同数值的元素会发送前后位置的变化。

原因:

1、 如果我们设置到bound(选定号的基数)它有相同的数值。 这就可以打乱顺序if(nums[i] <= x)。 但是并不是以相同的数值直接交换, 而是和比它大的值交换swap(nums[i+1], nums[rihgt]) 。而这个bound放到了相同数值的最后面。 (如果不使用随机化便可以避免这个问题)

2、 即便我们不是设置的bound没有相同的数值。 |lower|higher|unvisted| bound。 也就是说待排序的范围为(|lower|higher|unvisted| ) lower就是比bound小的值, higher是比bound大的值。 ‘

实例: |3 1 2|9 7 8 9 | 4 6 3 | 5, 当这时候遍历到了4。 那么9和4会进行交换变为 |3 1 2|4 7 8 9 9| 6 3 | 5 。这时候就发生了交换(4和9)之间。

// 在随机化后的快排, 我们把选中的数字放到排序区间的外面, 待排序区间为[left, right)
inline int partition(vector<int>& nums, int left, int right)
    int x = nums[right], i = left - 1;
    for(int j = left;j < right;++j)
        if(nums[j] <= x)
            swap(nums[++i], nums[j]);
        
    
    swap(nums[i+1], nums[right]);
    return i+1;

快排的稳定版本

常规的快速排序算法是一个不稳定的算法, 也就是两个相等的数排序之后的顺序可能在原序列中的顺序不同。 那要实现一个稳定版本的快速排序可能吗?

思路:

  1. 第一遍先把小于pivot的元素按照先后顺序放在tmp中。 然后把pivot放到它的正确位置tmp[k]。
  2. 第二遍把大于pivot的元素按先后顺序放到tmp里。 这样处理pivot以前的其他元素, 都保持了和原序列中一样的顺序。
  3. 第三遍把tmp赋值给原数组A
int stable_partition(vector<int>& nums, int left, int right)

    vector<int> tmp(right - left + 1);
    int pivot = nums[left];
    int index = 0;
    for(int i = left+1;i < right+1;++i)
    
        if(nums[i] < pivot)
        
            tmp[index] = nums[i];
            index = index + 1;
        
    

    tmp[index] = pivot;
    int position = index + left;
    index = index + 1;
    for(int i = left+1;i<right+1;++i)
    
        if(nums[i] >= pivot)
            tmp[index] = nums[i];
            index = index + 1;
        
    
    index = 0;
    for(int i = left;i < right+1;++i)
    
        nums[i] = tmp[index];
        ++index;
    
    return position;

void quickSort(vector<int>& nums, int left, int right)

    if(left < right)
        int location = stable_partition(nums, left, right);
        quickSort(nums, left, location-1);
        quickSort(nums, location+1, right);
    


vector<int> sortArray(vector<int>& nums)

    quickSort(nums, 0, nums.size()-1);
    return nums;


int main()

    vector<int> arr = 3,1,2,9,7,8,9,4,6,3,5;
    cout << arr.size() << endl;
    sortArray(arr);
    for(int i = 0; i <arr.size();++i)
    
        cout << arr[i];
    
    cout << endl;

当数据较小, 却又有较多的重复元素

**面对有大量重复元素的场景, 使用原始的快速排序, 会让时间复杂度退化成O(n^2) 。不管是当条件是大于等于还是小于等于v,**当数组中重复元素非常多的时候,等于v的元素太多,那么就将数组分成了极度不平衡的两个部分,因为等于v的部分总是集中在数组的某一边。


一种优化方式就是双路快排

由代码可以得知, 当i的元素小于v的时候继续向后扫描, 直到碰到某个元素大于等于v。 同理, 直到碰到某个元素小于等于v。

而后,只要交换i和j的位置就可以了, 然后i++, j–就行了。

/*
 * @Date: 2021-12-07 17:24:33
 * @LastEditors: kafier
 * @LastEditTime: 2021-12-07 17:46:26
 */


#include<iostream>
#include <algorithm>
#include <vector>
using namespace std;

template<typename T>
int partition(vector<T>& arr, int left, int right)

    T pivot = arr[left];
    int i , j;
    i = left + 1, j = right;

    while(true)
    
        while(arr[i] < pivot && i <= right) 
            ++i;
        while(j>=left+1 && arr[j] > pivot)
            --j;
        if(i > j)
        
            break;
        
    
        swap(arr[i], arr[j]);
        ++i;
        --j;
    
    swap(arr[left], arr[j]);

    return j;


template <typename  T>
void __quickSort2(vector<T>& arr, int left, int right)

    if(left >= right)
    
        return;
    

    int position = partition(arr, left, right);
    __quickSort2(arr, left, position-1);
    __quickSort2(arr, position+1, right);


template<typename T>
void quickSort(vector<T>& arr, int n)

    __quickSort2(arr, 0, n-1);


int main()

    vector<int> arr = 3,1,2,9,7,8,9,4,6,3,5;
    cout << arr.size() << endl;
    // sortArray(arr);
    quickSort(arr, arr.size());
    for(int i = 0; i <arr.size();++i)
    
        cout << arr[i];
    
    cout << endl;


三路快排

双路快排讲整个数组分成了小于v, 大于v的两部分, 而三路快排则是将数组分成了小于v, 等于v,大于v的三个部分。 当递归处理的时候, 遇到等于v的元素直接不用管, 只需处理小于v, 大于v的元素就好了

#include<iostream>
#include<algorithm>
using namespace std;

t

归并排序(重点)

基本思路

借助额外空间,合并两个有序数组, 得到更长的有序数组。(怎么合并两个有序数组, 去看合并两个有序数组

而后利用递归逐渐分解这个问题。

常规代码

排序数组

class MergeSolution 
public:
    vector<int> tmp; // 需要一个额外的数组来存储原数组
    vector<int> sortArray(vector<int>& nums) 
        tmp.resize((int)nums.size(), 0);
        mergeSort(nums, 0, (int)nums.size() - 1);
        return nums;
    

    void mergeSort(vector<int>& nums, int left, int right)
    
        if(left >= right)
            return ;
        
        int mid = left + (right - left)/2;
        mergeSort(nums, left, mid);
        mergeSort(nums, mid+1, right);

        int i = left, j = mid + 1;
        int cnt = 0;
        while(i <= mid && j <= right)
        
            if(nums[i] <= nums[j])
            
                tmp[cnt++] = nums[i++];
            
            else
                tmp[cnt++] = nums[j++];
            
        
        // 如果右边的区间先被放完了
        while(i <= mid)
            tmp[cnt++] = nums以上是关于排序(重点介绍快速排序的各种场景, 堆排序的数组空洞)的主要内容,如果未能解决你的问题,请参考以下文章

排序(重点介绍快速排序的各种场景, 堆排序的数组空洞)

数组各种排序算法和复杂度分析

Python八大算法的实现,插入排序希尔排序冒泡排序快速排序直接选择排序堆排序归并排序基数排序。

堆排序

C# 各种内部排序方法的实现(直接插入排序希尔排序冒泡排序快速排序直接选择排序堆排序归并排序基数排序)

C# 各种内部排序方法的实现(直接插入排序希尔排序冒泡排序快速排序直接选择排序堆排序归并排序基数排序)