ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

希尔排序和快速排序的比较

2022-08-11 12:31:47  阅读:105  来源: 互联网

标签:arr right 用时 希尔 排序 快速 left


为什么要写这个?

  • 今天重新回顾希尔排序,敲了一下代码。
  • 使用希尔排序和标准快速排序对这个数组进行排序,遇到了希尔排序的速度碾压标准快排的情况。
  • 之前并没有好好的思考过快排和希尔排序的使用场景,这里来做一下个人记录。

快速排序

  • 快速排序(递增)的步骤如下:
  1. 选取一个pivot,挖出来,腾出来一个A坑(挖坑)
  2. person1负责:从右往左找第一个比pivot小的元素挖出来填A坑(小的往左放),挖出来的这个元素腾出来一个B坑(需要有一个比pivot大的元素来填)
  3. person2负责:从左往右找第一个比pivot大的元素填B坑(大的往右放),挖出来的这个元素腾出来一个C坑(需要有一个比pivot小的元素来填)
  4. 不断重复2和3步骤,直到person1和person2相遇,此时将pivot放入person1所在的坑中。(确定了pivot最终的位置)
  5. 对pivot左边的子数组和pivot右边的子数组进行1到4步骤的循环。
  • 简单来说,快速排序就是:挖坑,交替填坑,分治的三个操作。

快速排序的代码


int partition(vector<int>& arr, long long left, long long right) {
    // 1. 挖坑
    int tmpPivot = arr[left]; // 在最左边挖了一个坑,并临时保存了坑中的元素,此时left指向这个坑

    // 2. 交替填坑
    while (left < right) {
        while (left < right && arr[right] >= tmpPivot) right--;// 从右往左找第一个小于pivot的元素
        arr[left] = arr[right]; //用这个元素填当前坑(left所在位置),那么right指向新腾出来的一个坑,此时我们让right停止
        while (left < right && arr[left] <= tmpPivot) left++; // 从左往右找第一个大于pivot的元素
        arr[right] = arr[left]; // 用这个元素填当前坑(right所在位置)
    }
    // 当left和right相遇(此时还有一个坑,那么用pivot填充)

    // 3. 用枢轴来埋最后一个坑(确定最终位置)
    arr[left] = tmpPivot;
    return left;
}

void quickSort(vector<int>& arr, long long left, long long right) {
    // 由于left和right由上一步的pivot计算得来,上一步的pivot可能是0,1,arr.size()-1,arr.size() - 2这四种情况
    // 他们分别会导致left == right + 1, left == right, left == right + 1, left == right这几种边缘情况,是不需要继续做快排的
    if (left >= right) {
        return;
    }
    long long pivot = partition(arr, left, right); // 做局部排序并返回枢轴的index
    quickSort(arr, left, pivot - 1);
    quickSort(arr, pivot + 1, right);
}

快速排序时间复杂度分析

  • 快速排序(pivot选取第一个元素)的理论最好时间复杂度是O(nlog2n)级别的。水平来看,每次选取一个pivot都需要对整个数组进行遍历,复杂度为O(n)。垂直来看,分治的最好情况是每一次都分成均等的两份,遍历树的高度为log2n,所以理论最好时间复杂度是O(nlog2n)。
  • 如果数组基本有序,会退化成O(n2)(每一次分治都分成1和n-1)但是这种情况并不容易出现。

希尔排序

  • 希尔排序的步骤如下:
  1. 确定一个分组数k,开始的时候是n / 2
  2. 根据分组数,将数组分成{v[0], v[k], v[2k] ...} , {v[1], v[k + 1], v[2k + 1] ...} , ...
  3. 对每个组的组内元素分别进行插入排序
  4. 减少分组数,重复1、2、3步骤,直到分组数为1,做最后一次插入排序。
  • 希尔排序分组的目的:希尔排序试图解决的是插入排序的一个痛点。试想如果一个长度为n的数组的第n个数据是整个数组中最小的,那么对最后一个数据进行插入排序的时候,比较次数为n-1,元素移动次数为n-1。实际上,希尔排序的分组过程,使得这样的元素可以以组为单位来进行插排的跳跃,大大减少了比较的次数和移动元素的次数。

  • 希尔排序的本质还是插入排序,拥有插入排序的性质。

希尔排序的代码


void shellSort(vector<int> & arr) {
    int size = arr.size();
    if (size == 1 || size == 0) return;
    int groupNum = size; // 分成多少组(等于是同组之间的元素间隔大小)
    int elemNumPerGroup; // 每组中元素个数
    while (groupNum > 2) { // 当分组数变为1的时候,做最后一次插排
        // 计算分组和组内元素个数
        groupNum /= 2;
        elemNumPerGroup = size / groupNum;
        for (int g = 0; g < groupNum; g++) { 
            for (int i = 1; i < elemNumPerGroup; i++) { 
                int now = g + i * groupNum; 
                if (now >= size) { 
                    break;
                }
                int tmp = arr[now];
                int pre = g + (i - 1) * groupNum; 
                while (pre >= 0 && arr[pre] > tmp) {
                    arr[pre + groupNum] = arr[pre]; 
                    pre -= groupNum; 
                }
                arr[pre + groupNum] = tmp; 
            }
        }
    }
}

希尔排序的时间复杂度分析

  • 希尔排序的最佳时间复杂度是O(n1.3)级别,此时数组基本有序。
  • 数组整体逆序的情况下,时间复杂度会达到O(nk)。
  • 如何计算的呢?还需深究。

希尔排序速度碾压快速排序的情况

  • 理论上来说快速排序比希尔排序快很多
  • 使用10w长度的随机数组,随机范围为0~1000000,此时快速排序用时0.026s,希尔排序用时0.073s
  • 使用10w长度的随机数组,随机范围为0~100000,此时快速排序用时0.026s,希尔排序用时0.074s
  • 使用10w长度的随机数组,随机范围为0~10000,此时快速排序用时0.029s,希尔排序用时0.064s
  • 使用10w长度的随机数组,随机范围为0~1000,此时快速排序用时0.041s,希尔排序用时0.055s
  • 使用10w长度的随机数组,随机范围为0~100,此时快速排序用时0.229s,希尔排序用时0.044s
  • 使用10w长度的随机数组,随机范围为0~10,此时快速排序用时2.04s,希尔排序用时0.033s
  • 使用10w长度的随机数组,随机范围为0~5,此时快速排序用时4.803s,希尔排序用时0.032s

数组长度为10w的结果

范围\排序算法 快速排序 希尔排序
0~1000000 0.026 s 0.073 s
0~100000 0.026 s 0.074 s
0~10000 0.029 s 0.064 s
0~1000 0.041 s 0.055 s
0~100 0.229 s 0.044 s
0~10 2.04 s 0.033 s
0~5 4.083 s 0.032 s
  • 第一次排序测试的时候,我使用的就是0~100的数据范围。当时给我愣了一下,希尔排序为什么比快速排序快这么多?仔细想了一下,才反应过来是我数据范围给的太小了。
  • 上表可以看出,随着数据范围的增大,快速排序的速度趋于饱和(0.026 s)的加快,而希尔排序的速度会趋于饱和(0.073s)的变慢。在数据范围特别小的情况下,快速排序的速度大幅下降,甚至被希尔排序虐杀。
  • 分析一下就会知道,当数据范围很小的时候,随机数据基本有序的可能性和范围就会大大增加。希尔排序的本质是插入排序,当数据基本有序的时候,不怎么需要元素比较和移动,节省了很多性能,时间复杂度无限趋近于O(n1.3)。而之前也分析过快速排序,如果数据基本有序,快排的时间复杂度会无限趋近于O(n2)的级别,如此,也就可以理解上述情况发生的原因了。

继续测试

  • 减少数组长度为1w,再做相同的测试,结果如下
  • 使用1w长度的随机数组,随机范围为0~1000000,此时快速排序用时0.002s,希尔排序用时0.005s
  • 使用1w长度的随机数组,随机范围为0~100000,此时快速排序用时0.002s,希尔排序用时0.004s
  • 使用1w长度的随机数组,随机范围为0~10000,此时快速排序用时0.002s,希尔排序用时0.004s
  • 使用1w长度的随机数组,随机范围为0~1000,此时快速排序用时0.002s,希尔排序用时0.004s
  • 使用1w长度的随机数组,随机范围为0~100,此时快速排序用时0.003s,希尔排序用时0.003s
  • 使用1w长度的随机数组,随机范围为0~10,此时快速排序用时0.021s,希尔排序用时0.002s
  • 使用1w长度的随机数组,随机范围为0~5,此时快速排序用时0.047s,希尔排序用时0.002s

数组长度为1w的结果

范围\排序算法 快速排序 希尔排序
0~1000000 0.002 s 0.005 s
0~100000 0.002 s 0.004 s
0~10000 0.002 s 0.004 s
0~1000 0.002 s 0.004 s
0~100 0.003 s 0.003 s
0~10 0.021 s 0.002 s
0~5 0.047 s 0.002 s
  • 可以看到,依然是在范围较小的时候发生希尔比快排快的情况,但n的减小令O(n1.3)和O(n2)的差距已经没有那么明显了。

结论

  • 数据量大且数据范围较小且均匀的时候,可以考虑用希尔排序代替快速排序,来提升排序速度
  • 当然,基本不会这么做,因为数据范围特别小这种情况比较少见(我的测试中,快排在10的范围内效率才会明显下降),而且个人感觉希尔排序的代码不如快排好写。

标签:arr,right,用时,希尔,排序,快速,left
来源: https://www.cnblogs.com/LeisureLak/p/16575619.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有