排序
1. 排序的概念以及应用
1.1 排序的概念
排序:简单来说就是将一组无序的记录序列调整成有序的记录序列的一种操作。
排序的作用:排序之后序列有无序变为有序大大提高了查询效率。
相关名词
- 稳定性:就比如说在一个记录排序当中出现两个相同的数字’5’,排序之前我们在这儿可以将它们分成‘5’(前)和‘5’(后),在排序之后假如两个‘5’的前后关系没有改变的话我们称之为稳定的排序;反之,则这个排序不稳定。
- 关键码值:数据元素中能起标识作用的数据项。
- 内部排序: 数据元素全部都放在内存中的排序。
- 外部排序:数据太多不能同时放在内存当中,根据排序过程的要求不能在内存之间移动数据的排序。(这个学了归并之后就明白了)
- 排序算法的性能评价:排序算法好坏标准可以通过计算算法的时间复杂度和空间复杂度比较;也可以去自己设计案例来计算时间当然自己给出来的数据要足够大范围足够广案例也要足够多;还可以通过比较次数和移动次数来进行衡量。
1.2 排序的运用
生活中常见的排序:比如跑步比赛排名、考试成绩排名、电商价格排名等等。
1.3 常见的排序算法
2. 测试排序时间性能的代码
作用:可以测试各种排序算法所需要的时间。排序OJ(可以使用各种排序跑这个OJ)https://leetcode.cn/problems/sort-an-array/submissions/。需要注意的是时间复杂度为O(N^2)的排序过不了OJ超时了。
代码如下:
void TestOP()
{
srand(time(0));//使用当前时间作为随机数生成器的种子
const int N = 1000000;
//分配6组个数为N的空间
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
//给数组分配随机数
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
}
//记录开始时间
int begin1 = clock();
//排序
InsertSort(a1, N);
//记录排序之后的时间
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
HeapSort(a3, N);
int end3 = clock();
int begin4 = clock();
SelectSort(a4, N);
int end4 = clock();
int begin5 = clock();
QuickSort(a5, N);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, N);
int end6 = clock();
//打印各个排序所消耗的时间
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
//释放开辟的地址空间
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
3. 常见的排序算法实现
3.1 插入排序
3.1.1 插入排序的基本思想
把待排序的记录按其关键码值的大小逐一插入到一个已经排序好的有序序列之中,知直到所有的记录插入完成为止,得到一个新的有序序列。
实际应用中我们玩儿的扑克牌就用到了插入排序的思想:
3.1.2 直接插入排序
- 思路:大致是这样的,再插入第i个元素的时候,前面i-1个元素已经排好序了。将第i个元素依次与之前的i-1个元素依次比较,找到第i个元素的位置,然后从原来位置上面的元素开始依次向后移动。
- 代码实现:
void Insertsort(int* a,int n)
{
//note:一定是n-1因为要是n就数组越界了
for (int i = 0; i < n - 1; i++)//控制end的位置!!
{
//单趟循环
//新插入的元素要从后往前依次对比 新插入的元素就是最后一个元素的下一个位置
int end = i;
//temp就是新插入的元素 保存后一个元素
int temp = a[end + 1];
//指向end后一个位置
while (end >= 0)
{
if (temp < a[end])
{
a[end + 1] = a[end];
end--;
}
//前面已经有序了 比end位置还小就直接放在end的后一个位置即可
else
{
break;
}
}
a[end + 1] = temp;
}
}
- 直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高。(越是有序,单趟比较的次数就少)
- 时间复杂度:O(N^2)逆序。
- 空间复杂度:O(1)
- 稳定性:稳定
3.1.3 希尔排序(缩小增量排序)
- 希尔排序的基本思想:
1.预排序(分组排序):先选定一个整数gap,把待排序的序列分组,所有下标差为gap的为一组,进行比较判断。然后重复进行选取gap,再进行分组和比较。依次进行
2.插入排序:直到gap == 1的时候就是插入排序了
预排序可以走很多很多次。
那么为什么要让gap由大到小呢?
原因:因为gap越大,数据挪动的速率就快(大的数可以更快的到后面,小的数可以更快的到前面),但是越不接近有序;gap越小,数据挪动的速率就越慢,但是越接近有序。设置gap是个较大的值,为的是去让大的数可以更快的到后面,小的数可以更快的到前面,然后gap逐渐变小序列也变得逐渐有序,当gap==1的时候序列已经十分接近有序了,再使用直接插入排序效率就曾加了许多。
NOTE:gap的选取可以选择N/2也可以选择N/3+1。
-
-
计算时间复杂度:( 1+2+3+…+n/gap)*gap 最坏的情况下就是O(N^2)
最好的情况是O(N) -
代码实现:
void ShellSort(int* a, int n)
{
int gap = n;
//预排序
//当gap==1的时候就是插入排序
while (gap > 1)
{
//gap分组可以选取N/2 也可以选取N/3+1
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)//并排!!
{
int end = i;//有序最后一个元素
int temp = a[end + gap];//要插入的新元素!!
while (end >= 0)
{
if (a[end] > temp)
{
a[end + gap] = a[end];//依次向后移动gap个位置!!
end -= gap;
}
else
{
break;
}
}
//插入gap正确的位置!!
a[end + gap] = temp;
}
}
//单趟排序
}
- ** 性能分析**
1.时间复杂度: O(N*logN)
2.空间复杂度:O(1)
3平均复杂度:O(N^1.3)
4.稳定性:不稳定,因为再预排序的时候就有可能,相同的数会分配到不同的组中了,就不能保证原有的顺序了。
- 计算时间复杂度过程
原序列分成gap组,每组N/gap个数据;
每组最坏情况下挪动次数:1+2+3+…——N/gap-1为一个等差数列
因此全部挪动次数就等于每组挪动次数*组数 (1+2+3+…+N/gap-1)gap
以下进行分析
当最开始gap很大的时候: (1+2+3)(N/3+1) —> O(N)
…
N/3/3/3/3/3…/3 = 1 中间时间复杂度为NlogN
…
当gap很小的时候:此时序列已经趋近有序 —>O(N)
- 希尔排序的特性总结:
- 希望排序是对直接插入排序的优化。(有预分组的过程)
- 当gap>1的时候都是预排序,目的是让数组更接近有序。当gap == 1时,数组已经接近有序了,这样就会很快。整体而言,可以达到优化的效果。
- 希尔排序的时间复杂度并不好计算,因为gap的取值方法很多,导致很难去计算,因此在很多书中给出的希尔排序的时间复杂度都不固定。
.
3.2 选择排序
3.2.1基本思想
每次从待排序的数据元素中选择出一个最小的元素,存放在起始位置,直到全部排序的数据元素排完。
3.2.2代码实现
//选择排序
void SelectSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int begin = i;
int Min = i;
while (begin<n)
{
if (a[begin] < a[Min])
{
Swap(&a[begin], &a[Min]);
}
begin++;
}
}
}
-优化代码
每次循环都找到最大值和最小值,最大值放在起始位置,最小值放在结束位置。这样可以提高效率。
//选择排序的优化
void SelectSort1(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int mini = begin;
int 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]);
Swap(&a[end], &a[maxi]);
begin++;
end--;
}
}
- 直接选择排序的特性总结
- 直接选择排序的思路十分简单,但是效率并不好,实际应用中也很少
- 时间复杂度: O(N^2)
- 空间复杂度: O(1)
- 稳定性:不稳定。有相同的数要分到最大位置,在前面的数会被先分配到后面因此前后顺序被打乱,不稳定。
3.3堆排序(重点)
要学习堆排序,首先要学习基础的二叉树结构,学习堆的向下调整算法,使用堆排序之前,我们得先建一个堆出来,堆的向下调整算法的前提是:根节点的左右子树均是大堆或小堆。由于堆排序在向下调整的过程中,需要从孩子中选择出较大或较小的那个孩子,父亲才与孩子进行交换,所以堆排序是一种选择排序。
3.3.1代码实现
void AdjustDown(int* a,int n,int parent)
{
int minchild=2*parent+1;
while(minchild<n)
{
//找较大的那个孩子
if(minchild+1<n&&a[minchild+1]>a[minchild])
{
minchild++;
}
if(a[parent]<a[minchild])
{
Swap(&a[parent],&a[minchild]);
parent=minchild;
minchild=parent*2+1;
}
else
{
break;
}
}
}
void HeapSort(int* a,int n)
{
//先建堆,升序——建大堆
for(int i=(n-1-1)/2;i>=0;i--)
{
AdjustDown(a,n,i);
}
//向下调整建堆
int i=1;//此时最大的数据已经在堆顶
while(i<n)
{
Swap(&a[0],&a[n-i]);
AdjustDown(a,n-i,0);
i++;
}
}
- 堆排序的特性总结:
1.堆排序使用堆来选数,效率较高。
2.时间复杂度:O(N*logN)。
3.空间复杂度:O(1)。
4.稳定性:不稳定。
3.4 交换排序
- 基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换着两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
3.4.1 冒泡排序
3.4.2 基本思路
所谓冒泡,就是像鱼儿在水一样吐泡泡,泡泡越往上越大。冒泡排序也是如此,具体思路是从第一个元素开始,相邻两个数两两比较,若后面的数大于前面就交换,反之继续判断直到走完整躺。
3.4.3 代码实现
void BubbleSort(int* a,int n)
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
if (a[j + 1] < a[j])
{
Swap(&a[j + 1], &a[j]);
}
}
}
}
- 冒泡排序的特性总结:
- 冒泡排序的思路十分简单容易理解
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定。遇到相等的不交换就可以了。
3.5 快速排序(重点)
3.5.1 快排学习的大纲
- 快排介绍::
快排是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后递归左右子序列,直到有序位置。
- 重点概念keyi值:
相当于一个分割线,keyi左边的元素都不大于keyi的元素,keyi右边的元素都不大于keyi的元素。 - 单趟排序的三种方法:
- hoare法
- 具体思路
1. 首先先选择出来一个key,最左边或者最右边都可以。
2. 定义左Left和右Right分别指向区间开始位置和最后的位置。开始移动(key在最左边,右边先动,为了保证相遇位置一定比key要小;key在最右边,左边先动,为了保证相遇位置一定比key要大)。
3. 当右边R遇到比key小的数停下来,然后左边L开始移动寻找比key元素大的值找到之后交换L和R。
4. 然后再继续反复,直到R和L相遇,相遇的位置和key再交换即可完成单躺排序。(此时key就落在了正确的位置,左区间都是不大于key的值,右区间都是不小于key的值)。再然后进行递归左右区间即可。 ----- 很像二叉树中的前序遍历!!
- 挖坑法
- 具体思路:
先将基准值保存起来,留下一个坑位。然后右边进行找小找到比key小的值进行填坑右边停止(就是将小的值赋值给坑位),这时候又会留下新的坑位。再然后左边开始找比key的大的值找到就填坑停止,留下坑位。然后反复。直到左右两边相遇将基准值填进相遇的坑位即可。
- 双指针法
- 具体思路
Step1: cur找比key小,找到后停来
Step2:++prev,交换prev位置和cur位置的值
Step3:最后交换key和prev下标的值
- 快排的两种优化
- 小区间优化
在二叉树结构中,最后一层的结点个数是2^(h-1)个子节点约占50%多,倒数第二层约占25%,倒数倒数第三层约占12.5%。也就是说大部分速率都消耗在后面递归的部分了。递归不光消耗时间还消耗空间,递归太深的话会导致栈溢出。因此,我们在区间小于一定值的时候使用直接插入排序即可提高效率。(不采用希尔排序,原因是希尔排序需要预排序,对于数据量大的序列可以进行但是对于数据量小的效率就可能不如插入排序效率高).
- 三数取中
在介绍三数取中之前我们首先看下图
这就是直接选择key=left的弊端,当序列趋近于有序,我们选择的key值总会在一段,我们可以想到二叉树,如果当每次key值都恰好选择了中间的数的时候。那么快排的时间复杂度就为O(NlogN)。那么为了避免遇到这种key值在一段的情况,我们给出了一种解决办法那就是三数取中。
三数取中思路:我们要取左边、右边和中间的元素进行排序,返回中间的数最为key。
3.5.2 代码实现
3.5.2.1 三数取中代码实现
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
//找到中间位置的值然后排序对比
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
//a[mid]>a[right]
else if (a[right] < a[left])
{
return left;
}
//相等的时候
else
{
return right;
}
}
else
{
if (a[left] < a[right])
{
return left;
}
else if (a[right] < a[mid])
{
return mid;
}
else
{
return right;
}
}
}
3.5.2.2 Hoare法
//Hoare找key
int PartSort1(int* a, int left, int right)
{
int key = left;
//if (left > right)
//{
// return;
//}
while (left < right)
{
while (a[right] >= a[key] && left < right)//重点一定要记住加上left<right否则可能会造成死循环或者R和L错过 1.key右边所有值都大于key 会出现越界
//加=是防止死循环 2.左右两边都有跟key相等的值
{
right--;
}
while (a[left] <= a[key]&&left<right)
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
key = left;
return key;
}
// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{
if (left>=right)
return;
//小区间优化 用直接插入代替,减少递归调用
if (right - left <= 8)//闭区间
{
InsertSort(array+left, (right - left)+1);//一定要注意的是array+left才是右边区间的首地址
}
// 按照基准值对array数组的 [left, right)区间中的元素进行划分
int div = PartSort1(array, left, right);
// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)
// 递归排[left, div)
QuickSort(array, left, div-1);
// 递归排[div+1, right)
QuickSort(array, div + 1, right);
}
//Hoare找key
int PartSort1(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a,left,right);
//交换left与mid的值
Swap(&a[left], &a[mid]);
//这时候把left给key 对应的值就不是一边到了
int key = left;
//if (left > right)
//{
// return;
//}
while (left < right)
{
while (a[right] >= a[key] && left < right)//重点一定要记住加上left<right否则可能会造成死循环或者R和L错过 1.key右边所有值都大于key 会出现越界
//加=是防止死循环 2.左右两边都有跟key相等的值
{
right--;
}
while (a[left] <= a[key]&&left<right)
{
left++;
}
Swap(&a[left], &a[right]);
}
Swap(&a[key], &a[left]);
key = left;
return key;
}
//三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = left + (right - left) / 2;
//找到中间位置的值然后排序对比
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
//a[mid]>a[right]
else if (a[right] < a[left])
{
return left;
}
//相等的时候
else
{
return right;
}
}
else
{
if (a[left] < a[right])
{
return left;
}
else if (a[right] < a[mid])
{
return mid;
}
else
{
return right;
}
}
}
3.5.2.3 挖坑法
//挖坑法
int PartSort2(int* a, int left, int right)
{
//三数取中
int mid = GetMidIndex(a, left, right);
//交换left与mid的值
Swap(&a[left], &a[mid]);
//这时候把left给key 对应的值就不是一边到了
int key = a[left];
int hole = left;//定义坑的位置!!
while (left < right)
{
while (a[right] >= key && left < right)//重点一定要记住加上left<right否则可能会造成死循环或者R和L错过 1.key右边所有值都大于key 会出现越界
//加=是防止死循环 2.左右两边都有跟key相等的值
{
right--;
}
a[hole] = a[right];//填坑的动作
hole = right;//出现新的坑位
while (a[left] <= key && left < right)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
3.5.2.4 前后指针法
//双指针法 前后指针法
int PartSort3(int* a,int begin,int end)
{
int key = begin;
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
//while (a[cur] < a[key] && cur <= end)
//{
// Swap(&a[cur++], &a[++prev]);
//}
//找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻
if (a[cur] < a[key] && ++prev != cur) //在这里++prev!=cur的意思是减少不必要的交换,当prev和cur的位置相等就没必要交换了
Swap(&a[prev], &a[cur]);
cur++;
}
Swap(&a[prev], &a[key]);
return prev;
}
3.5.3 非递归实现快排
需要借助其它数据结构辅助,通常是栈进行辅助。具体实现过程如下图所示
3.5.3.1 非递归实现快排代码
void QuickSortNonR(int* a, int begin, int end)
{
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))//栈不为空 说明还有区间没有进行排序
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int key = PartSort3(a, left, right);
//右区间 判断区间
if (right - key + 1 > 1)
{
StackPush(&st, key + 1);
StackPush(&st, right);
}
//再入左区间
if (key - 1 - left > 1)
{
StackPush(&st, left);
StackPush(&st, key - 1);
}
}
StackDestroy(&st);
}
- 快排特性总结:
- 时间复杂度:O(NlogN)
- 空间复杂度:O(logN)
- 稳定性:不稳定。不管是哪种方式去取key都会出现相同的值位置被破坏的情况。
3.5.4快排优化(大量重复数据的时候)----- 三路划分
快排对于序列中存在很多重复数据的时候性能会下降很多,极端情况下,比如序列全是2的情况下,快排会退化成O(N^2)。原因是快排使用的是两路划分大于key的和小于key的这两段区间,碰到全是2的这种极端情况就会出现一边倒的情况。因此需要在上面进行改进,二改进的方法就是三路排序。具体分析如下如所示:
- 三路划分核心步骤
- 跟key相等的值往中间推
- 比key小的甩到左边
- 比key大的甩到右边
- 跟key相等的就在中间
3.5.4.1三路划分代码实现
//Hoare找key 加上三路划分思想
void QuickSort(int a[], int left, int right)
{
if (left >= right)
return;
//小区间优化 用直接插入代替,减少递归调用
if (right - left <= 8)//闭区间
{
InsertSort(a + left, (right - left) + 1);//一定要注意的是array+left才是右边区间的首地址
}
//三数取中
int mid = GetMidIndex(a, left, right);
//交换left与mid的值
Swap(&a[left], &a[mid]);
//这时候把left给key 对应的值就不是一边到了
int key = a[left];
int cur = left + 1;
int begin = left;
int end = right;
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[cur++], &a[left++]);
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right--]);
}
else
{
cur++;
}
}
//此时区间被分成为[begin,left-1][left,cur-1][cur,end]
QuickSort(a, begin, left - 1);
QuickSort(a, cur, end);
}
3.6归并排序
-基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and
Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有
序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 具体排序步骤如下图所示:
3.6.1 归并排序代码实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
//分治
if (begin>=end)
{
return;
}
int mid = begin+(end-begin) / 2;
//[begin,mid] [mid+1,end]
_MergeSort(a, begin, mid,tmp);
_MergeSort(a, mid+1, end,tmp);
//归并
int begin1 = begin;
int begin2 = mid + 1;
int end1 = mid;
int end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
//左区见的数值大于右区间的 就把小的放入temp中
if (a[begin1] >= a[begin2])
{
tmp[i++] = a[begin2++];
}
else
{
tmp[i++] = a[begin1++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
//从temp拷贝到a中
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
- 归并排序的特性总结:
- 归并排序的缺点是需要O(N)的空见复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题!!
- 时间复杂度:O(NlogN)
- 空间复杂度“O(N)
- 稳定性:稳定。
3.6.2 非递归实现归并排序
- 基本思想:
具体实现过程就是先设置一个RangeN代表的每组元素的个数(分组的过程就是分治的过程),然后每组进行归并到新开辟的数组tmp中。将rangeN的值*2,然后重复以上的操作。直到rangeN的值超过数组长度位置排序完成。具体过程如下图:
- 需要考虑的问题:
- 我们在使用非递归归并的时候需要考虑两个问题:
- 边界值问题,我们需要考虑在选取了RangeN之后左右区间是否越界。
- 拷贝问题:我们需要考虑是什么时候可以边归并便拷贝,什么时候可以全都归并再拷贝。
具体分析如下图所示:
- 有两种解决方法:
修正区间。
遇到越过边界的直接break掉。
重点:两种方法都可以但是一定要注意的是,如果使用的是直接break的方法,就不能全部归并之后再拷贝这种方法了,原因是这样会造成随机值覆盖掉了原来的数据。具体分析如下图所示:
3.6.2.1非递归归并代码实现
- 采用修正方法
//非递归归并
void MergeSortNonR(int* a,int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//开始归并
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2*rangeN)
{
//归并
int begin1 = i, end1 = i+rangeN-1;
int begin2 = i+rangeN, end2 = i+2*rangeN-1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//需要修正区间
//有三种情况下需要修正
//end1超过了 那么begin2 end2 肯定也超过了
if (end1 >= n)
{
end1 = n - 1;
//创建一个不存在的区间
begin2 = n;
end2 = n - 1;
}
//第二种情况
if (begin2>=n)
{
//创建一个不存在的区间
begin2 = n ;
end2 = n - 1;
}
if (end2 >= n)
{
//修正
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//拷贝 归并一部分拷贝一部分
}
//全部归并之后排序
memcpy(a, tmp, sizeof(int) * n);
rangeN *= 2;
}
free(tmp);
tmp = NULL;
}
- 采用break方法
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//开始归并
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
//归并
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
//需要修正区间
//有三种情况下需要修正
//end1超过了 那么begin2 end2 肯定也超过了
if (end1 >= n)
{
break;
}
//第二种情况
if (begin2 >= n)
{
break;
}
if (end2 >= n)
{
//修正
end2 = n - 1;
}
int j = i;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
//拷贝 归并一部分拷贝一部分
memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
}
全部归并之后排序
//memcpy(a, tmp, sizeof(int) * n);
rangeN *= 2;
}
free(tmp);
tmp = NULL;
}
3.7计数排序
计数排序也是非比较排序。
3.7.1 基本思想
- 开辟一个计数数组,数组大小为(元素最大值-最小值+1)闭区间
- 统计出每个数据出现的次数,做相对映射(a[i]-min)可以解决负数问题
- 统计个数依次拷贝到原数组 a[i]有几个放几个(i+min)
3.7.2代码实现
//计数排序
void CountSort(int* a, int n)
{
int max = a[0];
int min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
{
max = a[i];
}
if (a[i] < min)
{
min = a[i];
}
}
//计数数组大小
int range = max - min + 1;
int* tmp = (int*)calloc(range,sizeof(int));
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
//统计计数
for (int i = 0; i <= n; i++)
{
tmp[a[i] - min]++;
}
//排序
int j = 0;
for (int i = 0; i < range; i++)
{
while (tmp[i]--)
{
a[j++] = i + min;
}
}
free(tmp);
tmp = NULL;
}
3.7.3特性总结
- 技术排序在数据范围集中的时候,效率很高,但是使用范围以及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围) //一般都是范围大
- 稳定性:稳定。
4.总结对各种排序进行比较