【数据结构】十大排序全面分析讲解及其对比分析
🔥个人主页:大白的编程日记
🔥专栏:数据结构
文章目录
- 【数据结构】十大排序全面分析讲解及其对比分析
- 前言
- 一.排序的概念及其运用
- 1.1排序的概念
- 1.2排序的应用
- 二.插入排序
- 2.1 插入排序
- 2.2希尔排序
- 三.选择排序
- 3.1选择排序
- 3.2堆排序
- 四.交换排序
- 4.1冒泡排序
- 4.2快排
- 五. 归并排序
- 六.非比较排序
- 6.1计数排序
- 6.2基数排序
- 6.3桶排序
- 后言
前言
哈喽,给位小伙伴大家好!上期我们讲了链式二叉树,今天我们来讲排序算法。话不多说,咱们进入正题!向大厂冲锋!
一.排序的概念及其运用
1.1排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不断地在内外存之间移动数据的排序。
1.2排序的应用
排序在日常生活当中很常见。
比如班级成绩排名,学校排名,CSDN热榜排名等等都是排序。
我们购物的时候也有许多按价格,销量等进行排序。
还有高校的排名也是排序。
我们常见的排序算法有7种。根据每个排序的特点又可以分类。
如果大家想看各个排序的动画演示的话。
可以看这个链接:排序算法动画演示
二.插入排序
2.1 插入排序
-
动画演示
-
思路分析
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。
当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移
简单来说就是把左边第一个数看作有序。
然后把第二个数作为插入数从右往左与有序区间的每个数做对比。
如果插入数比该数小,则该数往后挪动,插入数继续向左比较。
如果插入数比该数大,则说明插入数找到了插入有序区间的位置。直接插入
以此类推,直到最后一个数插入后就完成了排序。
实际中我们玩扑克牌时,就用了插入排序的思想
我们整理牌的时候就是这样依次找到合适的位置插入,直到所有的数都插入完成。
- 代码实现
void InserSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];//保存插入数
while (end >= 0)//对比直到最后一个数
{
if (a[end] > tmp)//比插入数大
{
a[end + 1] = a[end];//后移
end--;//更新比较数
}
else
{
a[end + 1] = tmp;//插入数据
break;
}
}
}
}
这样写还存在一个小问题就是。
如果插入数是有序数组最小的。那比较完最后一个数后。
全都大于插入数,就导致最后一个位置没有插入数据。
void InserSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
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;//插入数据
}
}
所以我们统一遇到比插入数小后结束循环。
循环结束后再插入。
- 算法分析
时间复杂度
空间复杂度
只开辟常数级别空间。O(1)。
稳定性
遇到相等的数放在后面。稳定。
2.2希尔排序
- 思路分析
希尔排序其实就是对插入排序进行优化。
也就是插入排序的pro max。
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。
希尔排序就是先预排序使数组接近有序。在进行插入排序。
- gap取值
gap太大到不到预排序的优化。太小性能又得不到太多的提升。
所以我们让gap是变化。但是要保证最后gap是1.才能保证最后是插入排序。
- 代码实现
前面我们说了gap组数据进行插入排序。每组数据相隔gap位置。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//+1保证最后一次一定等于1
//gap>1是预排序
//gap=1是插入排序
gap = gap / 3 + 1;
for (int j = 0; j < gap; j++)//gap组数据
{
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;
}
}
}
}
但是这样写三层循环有些不好看。
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//+1保证最后一次一定等于1
//gap>1是预排序
//gap=1是插入排序
gap = gap / 3 + 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--;//更新比较数
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
我们改成两层循环。这样就相当于多组一起排序。先把每一组的第一个数插入完后。在每一组的第二个数插入。以此类推。
- 算法总结
时间复杂度
希尔排序的时间复杂度不好计算,因为gap是变化的。且前面都排序会对,导致很难去计算,因此在好些书中给出的
希尔排序的时间复杂度都不固定:
- 证明
我们只能记住结论希尔排序的时间复杂度大概在O(N^1.3)左右。
空间复杂度
只用到常数级别空间。O(1),
稳定性
相同的数据分到不同组时候。不可控制。
不稳定
三.选择排序
3.1选择排序
- 动画演示
- 思路分析
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
在元素集合array[i]–array[n-1]中选择关键码最大(小)的数据元素若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
简单来说就是遍历一次数组。确定最小的数。然后
将未排序区间的第一个数与最小的数交换。以此类推。
每次都能排序一个数。
- 代码实现
这里我们做了优化。遍历一次选出最大和最小的数。
然后分别放在未排序区间的末尾和开始位置即可。
void SelectSort(int* a,int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int max = begin, min = begin;
for (int i=begin+1;i<=end;i++)
{
if (a[i] > a[max])//找最大值
{
max = i;
}
if (a[i] < a[min])//找最小值
{
min = i;
}
}
Swap(&a[begin], &a[min]);
if (max == begin)//max与begin重叠
{
max = min;
}
Swap(&a[max], &a[end]);
begin++;
end--;
}
}
不过要注意的是如果我们先交换最小的数。如果max和begin位置重叠。begin被交换后。max也被交换了。所以要重新更新一下max=min。
- 算法分析
时间复杂度
时间复杂度为O(N^2).
空间复杂度
只用了常数级别空间。O(1).
稳定性
不稳定
3.2堆排序
- 动画演示
- 思路分析
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
堆排序就是利用堆顶数据是最大或最小这一特性进行排序。每次都把堆顶元素放在待排序区间后面。然后重新调整建堆。以此类推即可完成排序。
具体过程看这篇:堆的实现以及建堆算法和堆排序
- 代码实现
void AdjustDown(HPDataType* a, int size,int parent)
{
int child = parent * 2 + 1;//孩子节点
while (child<size)
{
int min = child;//左右孩子中最小的孩子
if (min+1<size&&a[min] > a[min + 1])//防止没有右孩子
{
min = child + 1;
}//假设法
if (a[parent] > a[min])//判断
{
Swap(&a[parent], &a[min]);//交换
parent = min;
child = parent * 2 + 1;
}
else
{
break;//调整完毕
}
}
}
void HeapSort(HPDataType* p, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)//向下调整建堆
{
AdjustDown(p, n, i);
}
int end = n - 1;//最后一个堆元素
while (end > 0)//
{
Swap(&p[0], &p[end]);//交换堆顶元素和最后一个堆元素
AdjustDown(p,end,0);//向下调整
end--;//更新最后一个堆元素
}
}
- 算法分析
时间复杂度
堆排序的时间复杂度是O(N*logN).
![](https://i-blog.csdnimg.cn/direct/f3f789d90f324f21b7 95db6bfa6aaaa1.png)
空间复杂度
只用到常数级别空间。O(1).
稳定性
当数据都相同时。交换首尾。
不稳定。
四.交换排序
基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动
4.1冒泡排序
-
动画演示
-
思路分析
冒泡排序思路就是从左边开始两两比较。将大的数移到右边。再往后两两比较。这样每都选出一个待排序区间最大的数。进行n-1躺排序就能完成数组有序。 -
代码实现
void bubble_sort(int arr[],int sz)
{
for (int i = 0; i < sz - 1; i++)//控制趟数
{
int flag = 0;
for (int j = 0; j < sz - 1 - i; j++)//控制比较次数
{
if (arr[j] > arr[j + 1])//判断是否交换
{
flag = 1;
int tmp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = tmp;//交换
}
}
if (flag == 0)
break;//已经排好序直接结束循环
}
}
这里我们用flag做优化。如果发生交换就标记一下。然后每次循环判断一下标记。如果没有标记说明没发生交换,说明已经有序。直接break结束即可。
-
算法分析
时间复杂度
冒泡排序的时间复杂度是O(N^2)。空间复杂度
只用到常数级别空间。O(1).稳定性
只让大的数交换到右边。相等的数不交换
稳定。4.2快排
-
动画演示
霍尔法
前后指针法
-
思路分析
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快排的思路就是找到一个数key。将其放在最左边。然后定义一个左指针和右指针。左指针往右找大,右指针往左找小。找到后交换左右指针的位置数据。直到左右指针相遇。然后将key与相遇数据位置交换。这样key左边都是小于key的数。右边都是大于key的数。就完成了key的排序。再去递归左右区间。就可以完成整个数组的排序。
但是得保证相遇位置一定比key小才可以保证左边区间有序
如何证明相遇位置一定比key小呢?
-
证明
-
代码实现
不论是霍尔法还是前后指针法(快慢指针)都是排好key同时分割左右区间。
霍尔法:
int Part1Sort(int* a, int left, int right)//霍尔法
{
Swap(&a[mid], &a[left]);
int keyi = left;//keyi为左 右先走 keyi为右 左先走
while (begin < end)
{
while (begin < end && a[end] >= a[keyi])
{
--end;
}
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);//两个都找到,交换
}
Swap(&a[keyi], &a[begin]);//相遇交换keyi和相遇的数
return begin;//keyi移动到相遇位置
}
void QuickSort(int* a,int left,int right)//递归
{
if (left >= right)//区间不存在结束递归
{
return;
}
else
{
int keyi=Part2Sort(a, left, right);
QuickSort(a, left, keyi - 1);//递归左序列
QuickSort(a, keyi + 1,right);//递归右序列
}
}
前后指针法:
int Part2Sort(int* a, int left, int right)//快慢指针法
{
int slow, fast,keyi;
keyi = left;
slow = left;
fast = slow + 1;
int mid = GetMid(a, left, right);//三数取中
Swap(&a[mid], &a[left]);
while (fast <= right)
{
if (a[fast] < a[keyi] && slow!=fast )
{
slow++;//扩大区间
Swap(&a[fast], &a[slow]);//交换
}
fast++;//扫描
}
Swap(&a[keyi], &a[slow]);//交换
return slow;
}
-
优化
但是现在的快排在一些特殊场景下效率会降低。 -
三数取中
int GetMid(int* a, int left, int right)//随机数三数取中
{
int mid = left+rand()%(right-left);
int leftnumber= left + rand() % (right - left);
int rightnumber = left + rand() % (right - left);
if (a[leftnumber] < a[rightnumber])
{
if (a[rightnumber] < a[mid])
{
return rightnumber;
}
else if (a[leftnumber] < a[mid])
{
return mid;
}
else
{
return leftnumber;
}
}
else
{
if (a[rightnumber] > a[mid])
{
return rightnumber;
}
else if (a[leftnumber] > a[mid])
{
return mid;
}
else
{
return leftnumber;
}
}
}
- 小区间优化
当递归数据较小时。我们选择插入排序排序比递归快排效率更高。
void QuickSort(int* a,int left,int right)//递归
{
if (left >= right)
{
return;
}
//小区间优化
if (right - left + 1 <= 10)
{
InserSort(a + left, right - left + 1);
}
else
{
int keyi=Part2Sort(a, left, right);
QuickSort(a, left, keyi - 1);//递归左序列
QuickSort(a, keyi + 1,right);//递归右序列
}
}
- 非递归
- 代码实现
void QuickSortNonR(int* a, int left, int right)//非递归
{
ST pst;
STInit(&pst);
STPush(&pst, left);//入栈
STPush(&pst, right);//入栈
while (!STEmpty(&pst))//栈不为空
{
int end = STTop(&pst);//右区间
STPop(&pst);//出栈
int begin = STTop(&pst);//左端点
STPop(&pst);//出栈
int keyi = Part2Sort(a, begin, end);//分割
if (keyi+1<end)//判断区间存在
{
STPush(&pst, keyi + 1);//区间入栈
STPush(&pst, end);
}
if (begin<keyi-1)//判断区间存在
{
STPush(&pst, begin);//区间
STPush(&pst, keyi - 1);
}
}
STDestroy(&pst);//栈销毁
}
- 三路划分
void Part3Sort(int* a, int begin, int end)//三路划分
{
if (begin >= end)
{
return;
}
//小区间优化
if (begin - end + 1 <= 10)
{
InserSort(a + begin, end - begin + 1);
}
else
{
int mid = GetMid(a, begin, end);//随机数三数取中
Swap(&a[mid], &a[begin]);
int left = begin, right = end;
int cur = left + 1;
int key = a[left];
while (cur <= right)
{
if (a[cur] < key)
{
Swap(&a[left], &a[cur]);
cur++;
left++;
}
else if (a[cur] > key)
{
Swap(&a[cur], &a[right]);
right--;
}
else
{
cur++;
}
}
Part3Sort(a, begin, left - 1);//递归左区间
Part3Sort(a, right + 1, end);//递归右区间
}
}
力扣有个排序的OJ。如果我们不用三路划分是过不了的。因为他设计了有大量重复数据的数组。
力扣排序OJ
霍尔法:
三路划分:
- 自省排序
introsort是introspectivesort采⽤了缩写,他的名字其实表达了他的实现思路,他的思路就是进⾏⾃我侦测和反省,快排递归深度太深(sgistl中使用的是深度为2倍排序元素数量的对数值)那就说明在这种数据序列下,选key出现了问题,性能在快速退化,那么就不要再进⾏快排分割递归了,改换为堆排序进⾏排序
- 代码实现
void InserSort(int* a, int n)//插入排序
{
for (int i = 0; i < n - 1; i++)
{
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;
}
}
void Swap(int* x,int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
void AdjustDown(int* a, int size, int parent)
{
int child = parent * 2 + 1;//孩子节点
while (child < size)
{
int tmp = child;//左右孩子中最小的孩子
if (tmp + 1 < size && a[tmp] < a[tmp + 1])//防止没有右孩子
{
tmp = child + 1;
}//假设法
if (a[parent] < a[tmp])//判断
{
Swap(&a[parent], &a[tmp]);//交换
parent = tmp;
child = parent * 2 + 1;
}
else
{
break;//调整完毕
}
}
}
void HeapSort(int* p, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; i--)//向下调整建堆
{
AdjustDown(p, n, i);
}
int end = n - 1;//最后一个堆元素
while (end > 0)//
{
Swap(&p[0], &p[end]);//交换堆顶元素和最后一个堆元素
AdjustDown(p, end, 0);//向下调整
end--;//更新最后一个堆元素
}
}
int GetMid(int* a, int left, int right)//三数取中
{
int mid = left+rand()%(right-left);
int leftnumber= left + rand() % (right - left);
int rightnumber = left + rand() % (right - left);
if (a[leftnumber] < a[rightnumber])
{
if (a[rightnumber] < a[mid])
{
return rightnumber;
}
else if (a[leftnumber] < a[mid])
{
return mid;
}
else
{
return leftnumber;
}
}
else
{
if (a[rightnumber] > a[mid])
{
return rightnumber;
}
else if (a[leftnumber] > a[mid])
{
return mid;
}
else
{
return leftnumber;
}
}
}
int Part1Sort(int* a, int left, int right)//霍尔法
{
int begin = left, end = right;
int mid = GetMid(a, left, right);//三数取中
Swap(&a[mid], &a[left]);
int keyi = left;//keyi为左 右先走 keyi为右 左先走
while (begin < end)
{
while (begin < end && a[end] >= a[keyi])
{
--end;
}
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);//两个都找到,交换
}
Swap(&a[keyi], &a[begin]);//相遇交换keyi和相遇的数
return begin;//keyi移动到相遇位置
}
void IntroSort(int* a,int left,int right,int depth, int defaultDepth)//递归
{
if (left >= right)
{
return;
}
//小区间优化
if (right - left + 1 <= 10)
{
InserSort(a + left, right - left + 1);
}
// 当深度超过2 * logN时改⽤堆排序 i
if (depth > defaultDepth)
{
HeapSort(a + left, right - left + 1);
return;
}
else
{
int keyi=Part1Sort(a, left, right);
IntroSort(a, left, keyi - 1,depth+1, defaultDepth);//递归左序列
IntroSort(a, keyi + 1, right,depth + 1, defaultDepth);//递归右序列
}
}
void QuickSort(int* a, int left, int right)
{
int depth = 0;
int logn = 0;
int k = right - left + 1;
for (int i = 1; i < k; i *= 2)
{
logn++;
}
IntroSort(a, left, right, depth, logn * 2);
}
int* sortArray(int* nums, int numsSize, int* returnSize) {
srand(time(0));
QuickSort(nums,0,numsSize-1);
*returnSize=numsSize;
return nums;
}
- 算法分析
时间复杂度
空间复杂度
递归深度logN.开辟O(logN)栈空间。
稳定性
交换key和相遇位置时。相遇位置大小和左区间大小不确定。
不稳定。
五. 归并排序
- 动画演示
- 思路分析
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序核心步骤
- 代码实现
void _MergeSort(int*a,int*tmp,int left,int right)
{
if (left == right)
{
return;
}//只有一个
int mid = left + (right - left) / 2;
_MergeSort(a, tmp, left, mid-1 );//让左边有序
_MergeSort(a, tmp, mid, right);//让右边有序//注意下限区间
int begin1 = left, end1 = mid-1;
int begin2 = mid , end2 = right;
int i = begin1;
//归并
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] < a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}//将小的数尾插
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}//将剩余元素尾插
memcpy(a+left, tmp+ left, sizeof(int) * (right-left+1));//拷贝
}
void MergeSort(int*a,int n)//递归
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(1);
}
_MergeSort(a, tmp, 0, n - 1);
free(tmp);
tmp = NULL;
}
但是这样会存在死循环的问题。
其实我们递归归并的过程就相当走了一个后序遍历。
- 非递归
我们用循环就可以控制归并。
但是万一数组不是2的次方倍就会存在不对齐的情况。
- 代码实现
void MergeSortNonR(int* a, int n)//非递归
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(1);
}
int gap = 1;//每组归并大小
while (gap < n)//多次gap组归并
{
for (int i = 0; i < n; i += 2*gap)//每组归并
{
int begin1 = i, end1 =i+gap-1;//左区间
int begin2 = i+gap, end2 = i+2*gap-1;//右区间
int j = begin1;
//归并
if (begin2 >= n)
{
break;//不用归并
}
if (end2 >= n)
{
end2 = n - 1;//归并到最后一个数即可
}
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));//归并一段拷贝一段
}
gap *= 2;
}
free(tmp);
tmp = NULL;
}
- 算法分析
时间复杂度
空间复杂度
开辟N个数的空间。O(N).
稳定性
将数据相同的左区间数据尾插即可。稳定。
- 外排序
归并排序既可以做内排序也可以做外排序。
外排序(Externalsorting)是指能够处理极⼤量数据的排序算法。通常来说,外排序处理的数据不能⼀次装⼊内存,只能放在读写较慢的外存储器(通常是硬盘)上。外排序通常采⽤的是⼀种“排序-归并”的策略。在排序阶段,先读⼊能放在内存中的数据量,将其排序输出到⼀个临时⽂件,依此进⾏,将待排序数据组织为多个有序的临时⽂件。然后在归并阶段将这些临时⽂件组合为⼀个⼤的有序⽂件,也即排序结果。
跟外排序对应的就是内排序,我们之前讲的常⻅的排序,都是内排序,他们排序思想适应的是数据在内存中,⽀持随机访问。归并排序的思想不需要随机访问数据,只要依次按序列读取数据,所以归并排序既是⼀个内排序,也是⼀个外排序。
-
思路分析
-
代码实现
-
随机数函数
我们先生成N个随机数然后写到文件里面去。
//创建N个随机数,写到⽂件中void CreateNDate()
{
//
造数据 int n = 1000000;
srand(time(0));
const char* file = "data.txt";
FILE* fin = fopen(file, "w");
if (fin == NULL)
{
perror("fopen error");
return;
}
for (int i = 0; i < n; ++i)
{
int x = rand() + i;
fprintf(fin, "%d\n", x);
}
fclose(fin);
}
- 读取文件函数
这里我们从文件读取n个数据。
读取后用堆排序对数据进行排序。再写入文件中。
如果一个数据读取不到说明已经排序完成。返回0表示排序完成
// 返回读取到的数据个数int ReadNNumSortToFile(FILE* fout, int* a, int n, const char* file)
{
int x = 0;
// 读取n个数据放到file
int i = 0;
while (i < n && fscanf(fout, "%d", &x) != EOF)
{
a[i++] = x;
}
// ⼀个数据都没有读到,则说明⽂件已经读到结尾了
if (i == 0)
return i;
// 排序
HeapSort(a, i);
FILE* fin = fopen(file, "w");
if (fout == NULL)
{
printf("打开⽂件%s失败\n", file);
exit(-1);
}
for (int j = 0; j < i; j++)
{
fprintf(fin, "%d\n", a[j]);
}
fclose(fin);
return i;
}
- 归并文件函数
归并思想一样。就是要注意数据小的文件用读取。另外一个文件不用读取。
// file1⽂件的数据和file2⽂件的数据归并到mfile⽂件中
void MergeFile(const char* file1, const char* file2, const char* mfile)
{
FILE* fout1 = fopen(file1, "r");
if (fout1 == NULL)
{
printf("打开⽂件失败\n");
exit(-1);
}
FILE* fout2 = fopen(file2, "r");
if (fout2 == NULL)
{
printf("打开⽂件失败\n");
exit(-1);
}
FILE* fin = fopen(mfile, "w");
if (fin == NULL)
{
printf("打开⽂件失败\n");
exit(-1);
}
// 这⾥跟内存中数组归并的思想完全类似,只是数据在硬盘⽂件中⽽已
//依次读取file1和file2的数据,谁的数据⼩,谁就往mfile⽂件中去写
// file1和file2其中⼀个⽂件结束后,再把另⼀个⽂件未结束⽂件数据,
// 依次写到mfile的后⾯
int num1, num2;
int ret1 = fscanf(fout1, "%d\n", &num1);
int ret2 = fscanf(fout2, "%d\n", &num2);
while (ret1 != EOF && ret2 != EOF)
{
if (num1 < num2)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
else
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
}
while (ret1 != EOF)
{
fprintf(fin, "%d\n", num1);
ret1 = fscanf(fout1, "%d\n", &num1);
}
while (ret2 != EOF)
{
fprintf(fin, "%d\n", num2);
ret2 = fscanf(fout2, "%d\n", &num2);
}
fclose(fout1);
fclose(fout2);
fclose(fin);
}
- 主体函数
先读取两个n个数数据的文件并且排序。
然后归并两个文件到mfile文件。
之后删除file1和file2文件。
让mfile文件成为file1文件。读取数据到file2文件
继续归并。直到读取文件数据完毕。排序完毕。
oid MergeSortFile(const char* file, int n)
{
FILE* fout = fopen(file, "r");
if (fout == NULL)
{
printf("打开⽂件%s失败\n", file);
exit(-1);
}
int i = 0;
int x = 0;
const char* file1 = "file1";
const char* file2 = "file2";
const char* mfile = "mfile";
// 分割成⼀段⼀段数据,内存排序后写到,⼩⽂件,
int* a = (int*)malloc(sizeof(int) * n);
if (a == NULL)
{
perror("malloc fail");
return;
}
// 分别读取前n个数据排序后,写到file1和file2⽂件
ReadNNumSortToFile(fout, a, n, file1);
ReadNNumSortToFile(fout, a, n, file2);
while (1)
{
// file1和file2归并到mfile⽂件中
MergeFile(file1, file2, mfile);
// 删除file1和file2
if (remove(file1) != 0 || remove(file2) != 0)
{
perror("Error deleting file");
return;
}
// 将mfile重命名为file1
if (rename(mfile, file1) != 0)
{
perror("Error renaming file");
return;
}
// 读取N个数据到file2,继续⾛归并
// 如果⼀个数据都没读到,则归并结束了
if (ReadNNumSortToFile(fout, a, n, file2) == 0)
{
break;
}
}
printf("%s⽂件成功排序到%s\n", file, file1);
fclose(fout);
free(a);
}
六.非比较排序
6.1计数排序
- 动画演示
- 思路分析
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。
1.统计相同元素出现次数
2. 根据统计的结果将序列回收到原来的序列中
- 代码实现
void CountSort(int*a,int n)
{
int max = a[0], 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* count = (int*)calloc(range,4);//开辟空间
if (count == NULL)
{
perror("calloc fail");
return;
}
for (int i = 0; i < n; i++)//统计次数
{
count[a[i] - min]++;
}
int j = 0;
for (int i = 0; i < range; i++)//排序
{
while (count[i]--)
{
a[j++] = i + min;
}
}
free(count);
count == NULL;
}
- 算法分析
时间复杂度
空间复杂度
开辟N个空间。O(N).
还有两个不太实用的排序简单讲一下思想即可。
6.2基数排序
-
动画演示
-
思路分析
基数排序的思想就是先按个位数去排序。这样可以确保十位相同时。个位小的在各位大的后面。这样加入只看前两位数的话就是有序的了。再按照十位后面的位数去排序。全部排序完后就有序了。 -
算法分析
只能排正整数。只适合位数相同的数
6.3桶排序
-动画演示
-
思路分析
建立一个指针数组。每个指针指向一个链表。建立10个桶
将十位为9的放在0号桶。十位为1的放在1号桶。以此类推。
放的时候尾插到链表。然后在对链表排序。 -
算法分析
只适合两位数排序。
后言
这就是排序的全部内容。内容有点多,大家自己好好消化吧!今天就分享到这。感谢各位的耐心垂阅!咱们下期见!拜拜~