目录
一:插入排序
1.1直接插入排序
直接插入排序,可以有很多种写法,写法也比较简单,在这里,我主要介绍一种和希尔排序十分相似的思想,方便后续讲解希尔排序
在这里我们定义一个变量end,它用来记录下标,在这里我们认为[0,end]范围内的数组是有序的,然后将下标end+1所在的数组,与[0,end]范围内的数组比大小(所有排序讲解的均为升序),放在合适的位置 。
如果,a[end] > a[end+1],我们即将a[end]的数字,放在end+1处,这是又会产生一个问题,我们是每次比较都要两个数字交换,还是让a[end]直接将a[end+1]覆盖呢?
直接覆盖肯定是首先就排除的,因为覆盖会使数据缺失,那就选每次比价,每次交换吗?
但是,如果数组长度很长,可能依次交换,并不能让a[end+1]放在合适的位置,这样看每次交换可能不如直接覆盖来的快,这时我们可以取一个折中的方法:在新建一个变量tmp,将a[end+1]的值存在tmp,让后直接覆盖就可以了,但是tmp不着急让在end处,我们将继续比较,找到正确的位置直接将tmp放入即可
由上图,可以写出代码:
//插入排序
//直接插入排序
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
//单趟,[0,end]为有序数组,将a[end+1]插入数组
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
1.2希尔排序
希尔排序通过名字可以看出是一个名叫希尔的大佬创造的方法,而当时这位大佬在写这个代码的时候正是想通过自己的方法,改良直接插入排序,所以希尔排序的思想和上面的直接插入排序很相似。
希尔排序和直接插入排序相比所做出的优化就是,先进行预排序,让数组十分接近有序,在进行一次插入排序即可。
预排序,就是将间隔gap的数分为一组,在这一小组中先进行排序,排完这几个小组后,再将gap变小,继续进行进行预排序,随着gap的减少,每一小组的成员不断增加,知道gap减少到1,也就是直接插入排序,之后数组有序
控制一组颜色排序:
for (int i = 0; i < n - gap; i+= gap)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
排完一种颜色,再排下一组颜色,知道所有组排完,一趟排序结束
for(int j = 0; j< gap; j++)
{
for (int i = j; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
此时此刻,我们排完一趟排序,已经用了三个循环,这样想,你是否觉得时间复杂度已经爆了呢?但是在数据十分庞大时,希尔排序也即是说比快排慢一点而已,所以大佬之所被称为大佬不是没有原因的。各位,请继续往下看,静等希尔排序的奇迹!
其实,前面排完一趟是,希尔当时的原版本,在这之后还有大佬,对这一段代码,进行了优化,时间复杂度并没有变化,只是代码更简洁一点用了两个循环。
思路,排每一组的第一个,然后第一个都排完以后,再同时排第二个
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
排完一趟以后,在最外面,再套上一层循环控制gap即可
//希尔排序
//预排序:接近有序
//直接排序:gap == 1,插入排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;//最后一次是1
//先排每一组的第一个,排完以后依次类推
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
对于gap的取值,希尔的初始版本是gap /= 2,这样就可以保证gap最后一次是1
后来,又有人经过大量实验数据得出,gap模3时,效率最高,但是/3的最后结果可能是0,所以为了保证最后一次是0,gap = gap/3+1
二:选择排序
2.1选择排序
选择排序就十分简单粗暴,即使遍历一遍数组,选出最小的,然后让最小的与下标为0的数字互换,然后重复
再这里可以做一点小小的优化就是一次选出最大和最小
//选择排序
//选择排序
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[mini])
mini = i;
if (a[i] > a[maxi])
maxi = i;
}
Swap(&a[begin], &a[mini]);
//如果maxi和bgein重合,前面begin和mini换了值以后,最大的数应该在mini下标
if (begin == maxi)
maxi = mini;
Swap(&a[end], &a[maxi]);
end--;
begin++;
}
2.2堆排序
堆排序分为两个部分就是建堆和排序
建堆:由于升序建大堆
//向下调整
void AdjustDown(int* a, int parent, int n)
{
int child = parent * 2 + 1;
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent *2+ 1;
}
else
{
break;
}
}
}
//堆排序
void HeapSort(int* a, int n)
{
//建堆:升序见大堆
for (int i = (n - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(a, i , n);
}
//排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, 0, end);
end--;
}
}
三:交换排序
3.1冒泡排序
冒泡排序就是将数组遍历一遍,建最大的数字冒到最后,然后最后一个位置不参与遍历,在从头开始遍历,找到次大的
代码如下:
//交换排序
//冒泡排序
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++)
{
for (int i = 0; i < n - 1-j; i++)
{
if (a[i] > a[i + 1])
{
Swap(&a[i], &a[i + 1]);
}
}
}
}
3.2快速排序
3.2.1Hoare版本
对于单趟排序,根据上面位置,很多人会写出这样的单趟排序
int key = a[left];
while (left < right)
{
//右边先走,找小
//还要保证在数组范围内
while (left < right && a[right] > key)
{
right--;
}
//左边找大
while (left < right && a[left] < key)
{
left++;
}
//此时left所在的位置大于key,right所在的位置小于key
Swap(&a[left], &a[right]);
}
//此时left和right相遇
Swap(&key, &a[left]);
此时,如果调试一下,会发现最后交换时,数组第一个值并没有变,因为创建了一个变量key,用来存最左边的值,相当于创建了一个临时变量,所以最后交换时,是临时变量,与数组中的值交换的,所以我们可以创建一个变量,用于存下标
解决了这个问题,循环条件,还有一个隐藏的条件,如果是以下数组,left和right如何呢?
单趟排序的代码:
int keyi = left;
while (left < right)
{
//右边先走,找小
//还要保证在数组范围内
while (left < right && a[right] > a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] < a[keyi])
{
left++;
}
//此时left所在的位置大于key,right所在的位置小于key
Swap(&a[left], &a[right]);
}
//此时left和right相遇
Swap(&a[keyi], &a[left]);
keyi = left;
排完单趟以后,就是排整个数组,也就是递归,递推
完整的排序代码:
if (left >= right)
return;
//单趟
int begin = left, end = right;
int keyi = left;
while (left < right)
{
//右边先走,找小
//还要保证在数组范围内
while (left < right && a[right] > a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] < a[keyi])
{
left++;
}
//此时left所在的位置大于key,right所在的位置小于key
Swap(&a[left], &a[right]);
}
//此时left和right相遇
Swap(&a[keyi], &a[left]);
keyi = left;
//[begin,keyi -1] keyi [keyi+1,end]
//递归
PartSort1(a, begin, keyi - 1);
PartSort1(a, keyi + 1, end);
以上是最基础的快排,但是在厉害的排序也是有自己的不足的,基础版的快排有一个不足就是,如果数组是顺序时,时间复杂度就会从n*logn变成n^2
这个问题会出现是因为我们每次都选择最左边的值作为key,为了解决这个问题,大佬们分别有不同的解决方法,比较常用的是随机值和三数取中
随机值法:顾名思义就是让key是个随机数,不再让key固定即使key,而为了让我们单趟写的代码不发生太大的变化,我们可以选出这个之后,再让这个值与left位置的值互换,这样代码处的left就可以继续使用了
//单趟
int begin = left, end = right;
//随机选取keyi
int randi = rand() % (right - left);
randi += left;
Swap(&a[randi], &a[left]);
int midi = GetMid(a, left, right);
Swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
//右边先走,找小
//还要保证在数组范围内
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
//此时left所在的位置大于key,right所在的位置小于key
Swap(&a[left], &a[right]);
}
//此时left和right相遇
Swap(&a[keyi], &a[left]);
keyi = left;
//[begin,keyi -1] keyi [keyi+1,end]
//递归
PartSort1(a, begin, keyi - 1);
PartSort1(a, keyi + 1, end);
三数取中:三数取中并不是取下标的中间值,而是取下标处的数组值的中间值,三数取中应用有序数组可以将复杂度再拉回n*logn
//三数取中
int GetMid(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] > a[midi])
{
if (a[midi] > a[right])
return midi;
//a[midi] 最小
else if (a[left] < a[right])
return left;
else
return right;
}
else//a[left] < a[midi]
{
if (a[midi] < a[right])
return midi;
//a[midi]最大
else if (a[left] > a[right])
return left;
else
return right;
}
}
而有的大佬,为了让快排更快,有做出了一些优化
递归过程,越往下所占总体比例越大 ,和二叉树类似,最后一层就占总体的50%,而且越往下数组区间越小,因此下面区间小的数组就不值得再往下递归了,之间排序了,一般当数组区间小于10时,就用插入排序
// 快速排序hoare版本
//三数取中
int GetMid(int* a, int left, int right)
{
int midi = (left + right) / 2;
if (a[left] > a[midi])
{
if (a[midi] > a[right])
return midi;
//a[midi] 最小
else if (a[left] < a[right])
return left;
else
return right;
}
else//a[left] < a[midi]
{
if (a[midi] < a[right])
return midi;
//a[midi]最大
else if (a[left] > a[right])
return left;
else
return right;
}
}
void PartSort1(int* a, int left, int right)
{
if (left >= right)
return;
if (right - left + 1 < 10)
{
InsertSort(a, right - left + 1);
}
else
{
//单趟
int begin = left, end = right;
//随机选取keyi
//int randi = rand() % (right - left);
//randi += left;
//Swap(&a[randi], &a[left]);
int midi = GetMid(a, left, right);
Swap(&a[midi], &a[left]);
int keyi = left;
while (left < right)
{
//右边先走,找小
//还要保证在数组范围内
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
//此时left所在的位置大于key,right所在的位置小于key
Swap(&a[left], &a[right]);
}
//此时left和right相遇
Swap(&a[keyi], &a[left]);
keyi = left;
//[begin,keyi -1] keyi [keyi+1,end]
//递归
PartSort1(a, begin, keyi - 1);
PartSort1(a, keyi + 1, end);
}
}
3.2.2双指针法
除了Hoare原本的版本以外,还有用双指针法来写快排
代码:
/ 快速排序前后指针法
void PartSort3(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int midi = GetMid(a, left, right);
Swap(&a[midi], &a[left]);
//单趟
int keyi = left;
int prev = left;
int cur = left + 1;
while (cur <= end)
{
if (a[cur] < a[keyi] && ++prev != cur)
Swap(&a[cur], &a[prev]);
cur++;
}
Swap(&a[keyi], &a[prev]);
keyi = prev;
//[begin, keyi-1] keyi [end, keyi+1]
PartSort3(a, begin, keyi - 1);
PartSort3(a, keyi+1, end);
}
3.2.3非递归
快排不一定必须要用递归,也可以借助栈来实现
标签:right,end,大乱,int,gap,排序,left From: https://blog.csdn.net/2301_80096514/article/details/136993084