希尔排序和快速排序的比较
作者:互联网
为什么要写这个?
- 今天重新回顾希尔排序,敲了一下代码。
- 使用希尔排序和标准快速排序对这个数组进行排序,遇到了希尔排序的速度碾压标准快排的情况。
- 之前并没有好好的思考过快排和希尔排序的使用场景,这里来做一下个人记录。
快速排序
- 快速排序(递增)的步骤如下:
- 选取一个pivot,挖出来,腾出来一个A坑(挖坑)
- person1负责:从右往左找第一个比pivot小的元素挖出来填A坑(小的往左放),挖出来的这个元素腾出来一个B坑(需要有一个比pivot大的元素来填)
- person2负责:从左往右找第一个比pivot大的元素填B坑(大的往右放),挖出来的这个元素腾出来一个C坑(需要有一个比pivot小的元素来填)
- 不断重复2和3步骤,直到person1和person2相遇,此时将pivot放入person1所在的坑中。(确定了pivot最终的位置)
- 对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)但是这种情况并不容易出现。
希尔排序
- 希尔排序的步骤如下:
- 确定一个分组数k,开始的时候是n / 2
- 根据分组数,将数组分成{v[0], v[k], v[2k] ...} , {v[1], v[k + 1], v[2k + 1] ...} , ...
- 对每个组的组内元素分别进行插入排序
- 减少分组数,重复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