八种排序
排序的稳定性
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,A1=A2,且A1在A2之前,而在排序后的序列中,A1仍在A2之前,则称这种排序算法是稳定的
稳定性本质是维持具有相同属性的数据的插入顺序 ,如果后面需要使用该插入顺序排序,则稳定性排序可以避免这次排序
例如在答题竞赛中,只有三枚奖牌,金银铜,金牌已经选出,如果两者都得99分,在分值上一样但是在提交顺序上有先后之分,这时排序的稳定性就体现出来了
插入排序
直接插入排序
核心思想:将一个数插入一段有序区间,保持这段区间有序,不断的将一个数插入进去,最开始让第一个有序,插入进去后前两个有序,前两个有序了再插入进去,前三个就有序了,以此类推整体就有序了。在实际生活中我们在玩扑克牌时就会进行插入排序。
- 插入排序时间复杂度:排升序时最坏的情况是数据是降序排列,这时时间复杂度为O(N^2),接近有序时时间复杂度最好的情况下为O(N)
void InsertSort(int* a, int n)
{
assert(a);
//end 为 n个数的最后一个数的下标
//n-2是只用比到倒数第二个就已经比完了,超过就越界了
//i每次加一,从头开始,假设只有一个数据,先把他变成前n-1个有序
for (int i = 0; i < n - 2; i++)
{
int end = i;
int tmp = a[end + 1];//保留end之后的数据,防止end+1被end覆盖
while (end >= 0 )
{
if (tmp < a[end])
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;//这里结束的标志有两种
//一种是比最后一个大直接放入到end后面。一种是end到-1,就将要插入的值放到0的位置
}
}
希尔排序
在插入排序上进行优化,当数据是随机或者逆序的排序下,
- 欲排序,先让数据接近有序(接近升序)【对间隔为gap的,分成一组,进行插入排序】
- 直接插入排序
当gap=3,逆序排序时,先进行欲排序使其接近有序。
欲排序一次不是走一步,而是一次走gap步,数据挪动更快,gap越小越接近有序,gap越大越不接近有序,但是gap越小,挪动越慢,gap越大,挪动越快。当gap==1时就是直接插入排序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0tNs5zy-1633948307179)(C:\Users\86181\AppData\Roaming\Typora\typora-user-images\image-20211006174732385.png)]
多组并排,原本希尔排序是要一组一组进行,间隔为gap的数据,进行欲排序,现在只用一层循环控制并行走循环
++i的意思,原本是要排完一组了才去排另一组,现在++i是一组没排完给赋给end,end就直接去排下一组
void ShellSort(int *a, int n)
{
int gap = n;//适用于给的数据很多时,gap要是3就显得很小
while (gap > 1)
{
gap = gap / 3 + 1;
//加一是让gap保证最后一次一定是1,gap大于1就是欲排序,等于1就是直接插入排序
//多组并排,原本希尔排序是要一组一组进行,间隔为gap的数据,进行欲排序,现在只用一层循环控制并行走循环
//++i的意思,原本是要排完一组了才去排另一组,现在++i是一组没排完给赋给end,end就直接去排下一组
for (int i = 0; i < n - gap; i++)//单趟排序
{
int end = i;
int tmp = a[end + gap];//保留end之后的数据,防止end+1被end覆盖
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;//这里结束的标志有两种
//一种是比最后一个大直接放入到end后面。一种是end到0
}
}
}
运行结果的时间差距很大
交换排序
冒泡排序
后一个比前一个大就交换
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; j++)
{
int flag = 0;//最好的情况下顺序有序升序,设立标志位以此来判断是否交换
for (int i = 0; i < n-j; i++)
{
if (a[i - 1]>a[i])
{
Swap(&a[i - 1], &a[i]);
flag = 1;
}
}
if (flag == 0)
{
break;
}
}
}
快速排序
-
hoare版本:左右指针的方式
一般选定左边最开始的位置作为key,左右两个指针Left,Right,让Right先走找比key小的,找到后停下来,再让Left走找比key大的,找到后停下来,交换Left和Right,继续让Right先走找比key小的,Left找比key大的,直到两个相遇,再把key交换到相遇的位置。这样子左边就比key小,右边比key大,让Right先走就保证了相遇位置一定比key小,(让右边做key也是一样的逻辑)
- 相遇有两种情况:
- 右遇左,右边找比key小的,相遇位置的左边一定比key小,
- 左遇右,Right停在了比key小的位置,再一交换左边一定比key小,右边比key大
当数组本身有序的情况下,快速排序的时间复杂度就是O(N^2);,由于排一趟就要选key,第一趟走完,走到左边没有比他大的,再走第二趟,重新选key,再走一遍,逐渐走完,造成时间复杂度这么高的原因主要是选key导致的,这时就又有了新的优化
- 三数取中法,找left,mild,right中不是最大也不是最小的数,这样就保证了不会取值做key,取到有序数组的头的问题
int GetMidIndex(int *a, int left, int right)//三数取中法,通过获取下标来找到中间值
{
int mild = left + (right - left) / 2;
if (a[left] < a[mild])
{
if (a[mild] < right)
{
return mild;
}
else if (a[left] >a[right])
{
return left;
}
else
{
return right;
}
}
else // (a[left] > a[mild])
{
if (a[left] < a[right])
{
return left;
}
else if (a[mild]>a[right])
{
return mild;
}
else
{
return right;
}
}
}
int PartSort1(int *a, int left, int right)
{
int mild = GetMidIndex(a, left, right);
Swap(&a[left], &a[mild]);
int keyi = left;
while (left < right)
{
while (a[right] >= a[keyi] && left<right)//左边做key,右边先走找比key小的//相遇了就停止,没有相遇才继续找
{
--right;
}
while (a[left] <= a[keyi] && left<right)//左边再走找比key大的
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[keyi], &a[right]);//相遇位置和key交换
return left;
}
void QuickSort(int * a, int left,int right)
{
if (left >= right)
{
return;
}
int keyi = PartSort1(a, left, right);//一趟排好了,再将其分成多个子问题去解决
//分成两段,思路类似二叉树的前序遍历,层层递归下去;
//[0-keyi-1][keyi][keyi+1,right]
QuickSort(a, 0, keyi - 1);
QuickSort(a, keyi + 1, right);
}
2.挖坑法
将左边的第一个值作为坑,把值赋给keyi那个位置就形成了一个新的坑位
right从后往前找比原来的坑位的值小的,找到后填到坑里,不用交换,直接覆盖,right找到的值的位置又形成了新的坑位
left从前往后找比key大的,找到后填到坑里
int PartSort2(int* a, int left, int right)//挖坑法
{
int key = a[left];
int hole = left;
while (left<right)
{
if (a[right] >= key && left<right)
{
right--;
}
//找比原来的坑位的值小的,找到后填到坑里
a[hole] = a[right];
hole = right;
while (left < right)
{
if ( a[left] < key && left < right)
{
left++;
}
//找比key大的,找到后填到坑里
a[hole] = a[left];
hole = left;
}
}
a[hole] = key;
}
3.前后指针法
定义prev,cur。
- cur往前走,找比keyi小的数据,
- 找到比keyi小的数据后停下来,++prev
- 交换prev和cur所指向位置的值,直到cur走到数据的结尾
void PartSort3(int* a, int left, int right)
{
int keyi = left;
int prev = left;
int cur = prev + 1;
/*while (cur <= right)
{
if (a[cur]>a[keyi])
{
cur++;
}
if (a[cur]<a[keyi])
{
Swap(&a[prev], &a[cur]);
prev++;
cur++;
}
}*/
while (cur <= right)
{
//prev != cur
//cur碰到比key小的也prev++,实际上就是自己跟自己交换索性不换反正curd都要++
if (a[cur] > a[keyi] && prev != cur)
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
选择排序
简单选择排序:
在一个待排序的数组中,从中找出最大(小)的元素,如果找到的元素不在最后一位,就与最后一(第一)位交换,重复上述动作,直到全部待排序的数据元素排完.
void SelectSort(int * a, int n)//选择排序
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int maxi = begin;
//传统选择一次只找一个最小的或者最大的,优化后同时找最大和最小的
for (int i = begin; i <= end; i++)
{
if (a[i]>a[maxi])
maxi = i;
if (a[i] < a[mini])
mini = i;
}
Swap(&a[begin], &a[mini]);//把一趟中最小的放到左边
if (begin == maxi)
maxi = mini;
Swap(&a[end], &a[maxi]);//一趟中最大的放到右边
++begin;
--end;
}
}
堆排序
堆排序看起来像是交换第一和最后一位最后应该归到交换类排序,但是核心的思路还是选择,建堆向下调整的过程中就会进行选择根节点还是叶子节点。升序建大堆,降序建小堆
排序按照生活中的思维是从小到大排序,应该是建小堆,但是如果是建小堆可以解决第一个数是最小的,那么第二小的怎么找呢,再向下调整一下建小堆找最小的吗,那显然时间复杂度更复杂,如果是建大堆,建好堆后第一个是最大的数,第一个数再跟最后一个数交换一下,左右子树依然保持是个大堆的结构,只需要再向下调整一下就好了,不需要像建小堆选出最小的数再重新建堆。
排降序也是一样的思路
void Swap(int * px, int * py)
{
int tmp = *px;
*px = *py;
*py = tmp;
}
//左右子树都是大堆或者小堆
void AjustDown(int * a, int n, int parent)
{
int child = parent * 2 + 1;//默认child是左孩子
while (child < n)
{
if (child + 1 < n && a[child + 1] > a[child])//默认child是左孩子,如过比左孩子大就认为child为右孩子
{
++child;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void Ajustup(int *a, int child)
//向上调整,当建好堆后如果插入一个数据假设是大堆
//放到最后,比父节点要大,它只会对它所在的父亲节点的路径造成影响,不用管左右子树谁大的问题,,左右子树整体还是一个大堆
{
int parrent = (child - 1) / 2;
while (child > 0)
{
if (a[child] > a[parrent])
{
Swap(&a[child], &a[parrent]);
parrent = child;
parrent = (child - 1) / 2;
}
else
{
break;
}
}
}
void HeapSort(int *a,int n)
{
//排升序建大堆
//排降序建小堆
for (int i = (n - 1 - 1) / 2; i >= 0;--i)//i为parent,要调整几对子树
{
AjustDown(a, n, i);//通过swap函数来控制大堆或者小堆
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AjustDown(a, end, 0 );
--end;
}
}
归并排序
递归版本
一个区间最开始是无序的,如果他的左右区间有序,那么他就有序。 找中间值,把一个整区间划分多个子区间,通过子区间来实现有序,左右区间又可以划分成更小的区间,直到只剩一个值返回
//归并排序
void _MergeSort(int * a, int left, int right, int *tmp)
{
//一个区间最开始是无序的,如果他的左右区间有序,那么他就有序,
//找中间值,把一个整区间划分多个子区间,通过子区间来实现有序
//左右区间又可以划分成更小的区间,直到只剩一个值返回
if (left >= right)
return;
int mid = (left + right) / 2;
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid + 1, right, tmp);
int begin1 = left, end1 = mid;
int begin2 = mid + 1, end2 = right;
int index = left;
//划分为有序的后再依次比大小放到tmp数组中
//一方走完了,另一方原本也已经是有序的,直接拷贝到tmp数组
while (begin1 <= end1 && begin2 <= end2)
{
if (begin1 < begin2)
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
//现在只有一个结束才会走到这一步,左右选只会选出一个值
//现在不清楚是begin1走完还是begin2走完,索性判断他结束的标志
//begin1结束就把begin2里的全拷贝到tmp中,反之亦然
while (begin1 <= end1)//第一段没结束(就说明第二段结束了)就进去,结束了就不会进去,进去后全部拷贝
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
//从临时数组拷贝到原数组
for (int i = left; i < right; left++)
{
a[i] = tmp[i];
}
}
void MergeSort(int *a, int n)
{
int * tmp = (int *)malloc(sizeof(int)*n);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
非递归版本的实现
void MergeSortNonR(int *a, int n)
{
int tmp = (int *)malloc(sizeof(int));
//控制组的个数,一组有1,2,4,8...
int groupnumber = 1;
//[[begin1,end1],[begin2,end2]]
while (groupnumber<n)
{
for (int i = 1; i < n; i += 2 * groupnumber)
{
int begin1 = i, end1 = i + groupnumber - 1;
int begin2 = i + groupnumber, end2 = i + 2 * groupnumber - 1;
int * index = begin1;
if (begin2 >= n)//第二段越界,控制边界
{
begin2 = n + 1;
end1 = n;
}
if (begin1 >= n)//begin1越界,后面整体都越界
{
end1 = n - 1;
}
if (end2 >= n)//END2越界,后半段越界
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (begin1 < begin2)
{
tmp[index++] = a[begin1++];
}
else
{
tmp[index++] = a[begin2++];
}
}
while (begin1 <= end1)//第一段没结束(就说明第二段结束了)就进去,结束了就不会进去,进去后全部拷贝
{
tmp[index++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[index++] = a[begin2++];
}
}
int groupnumber *= groupnumber * 2;
}
for (int i = 0; i < n; i++)
{
a[i] = tmp[i];
}
计数排序
统计每个数出现的次数,出现几次就在映射的位置++几次,再根据数组下标排序
统计排序:差值较大不太实用,适用局部范围集中的数
//绝对映射会有空间浪费,采取相对映射关系,a[i]-min
void countsort(int * a, int n)
{
//找出最大值和最小值
int max = a[0], min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i]>max)
{
max = a[i];
}
}
int range = max - min + 1;
int * count = (int *)calloc(range, sizeof(int));
//统计每个数出现的次数,出现几次就在映射的位置++几次
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//根据数组下标排序
int i = 0;
for (int j = 0; j < n; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
}
统计排序:差值较大不太实用,适用局部范围集中的数
//绝对映射会有空间浪费,采取相对映射关系,a[i]-min
void countsort(int * a, int n)
{
//找出最大值和最小值
int max = a[0], min = a[0];
for (int i = 1; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i]>max)
{
max = a[i];
}
}
int range = max - min + 1;
int * count = (int *)calloc(range, sizeof(int));
//统计每个数出现的次数,出现几次就在映射的位置++几次
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//根据数组下标排序
int i = 0;
for (int j = 0; j < n; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
}