在学习数据结构和算法的过程中,排序算法是最基础也是最重要的一个模块。通过对排序算法的学习和实现,不仅能帮助我们更好地理解算法的复杂度,还能提升我们解决实际问题的能力。本文将详细介绍几种常见的排序算法,包括插入排序、冒泡排序、快速排序、堆排序、归并排序以及计数排序。每种算法都将通过 C++ 实现,进行代码分析,并提供一些优化建议。
插入排序是一种简单的排序算法,它通过将每个待排序元素插入到已经排好序的部分。尽管插入排序的时间复杂度是 O(n^2)
,但它在小规模数据上表现优秀,并且是稳定的排序算法。
//插入排序算法
void insertSort(int* arr, int n) {
int i, j;
for (i = 1; i < n; i++)//第二个元素开始 遍历数组
{
if (arr[i]=0; j--)
{
arr[j + 1] = arr[j];
}
arr[j+1] = temp;//直接定义j变量的好处
}
}
printfarr(arr, n);
}
优化建议:
减少无效交换:在插入排序中,如果数组已经有序,就会进行不必要的比较和交换。我们可以在比较时加入提前结束的标志,避免无用的操作。
冒泡排序通过反复交换相邻的元素来将最大(或最小)元素“冒泡”到数组的一端。它的平均和最坏时间复杂度是 O(n^2)
,因此对大规模数据排序时效率较低。
void BubbleSort(int* arr, int n) {//冒泡排序
for (size_t i = 0; i < n-1; i++)
{
bool flag = false;
for (size_t j =0 ; j < n-1-i; j++)//jarr[j+1])
{
swap(arr[j], arr[j + 1]);
flag = true;
}
}
if (!flag) {
break;
}
}
printfarr(arr, n);
}
优化建议:
提前终止:通过 flag
判断每一轮排序是否有交换,如果没有交换,就可以提前终止排序。
快速排序是一种分治法的排序算法,它通过选取一个基准值,将数组分为两个部分,再递归地排序这两个部分。快速排序的时间复杂度平均为 O(n log n)
,但是最坏情况下会退化为 O(n^2)
。
void QuickSort(int * arr,int low ,int high) {//快速排序
if (low low) {
while (high > low&&arr[high]>=pivot) {//不加等于可能出现循环
high--;
}
arr[low] = arr[high];
while (high>low&&arr[low]<=pivot)
{
low++;
}
arr[high] = arr[low];
}
arr[low] = pivot;
return low;
}
优化建议:
基准值的选择:选择不同的基准值可能影响排序的效率,常见的优化方法是采用“三数取中”法,避免选择极端的基准值。
堆排序是一种利用堆数据结构的排序算法。堆排序的时间复杂度是 O(n log n)
,并且它是一个不稳定的排序算法。堆排序利用大顶堆或小顶堆来快速找到最大或最小元素。
void HeadAdjudt(int * arr,int root,int len) {
for (size_t i = root*2; i <=len; i*=2)//
{
if (iarr[i])
{
break;//跳出循环
}
else {
swap(arr[root], arr[i]);
root = i;//现在向下检查
}
}
}
void BuildMaxheap(int *arr,int len) {
for (int i = len/2; i >0; i--)
{
HeadAdjudt(arr, i, len);
}
}
void HeapSort(int* arr, int len) {
BuildMaxheap(arr, len);
for (size_t i = len-1; i > 1; i--)
{
swap(arr[i], arr[1]);
HeadAdjudt(arr, 1, i - 1);//只需要对最上面根节点下降就行
}
}
void testHeapSort() {//因为HeapSort的特性 直接重新弄了一个从1开始的数组方便堆排序
int arr2[8] = { -1,4,6,2,6,9,1,3 };//第一个arr2[0]的元素没有意义
int len2 = 8;
HeapSort(arr2, len2);
for (size_t i = 1; i < len2; i++)
{
cout << arr2[i] << " ";
}
}
注意:这里对于堆排序是使用数组下标为1的开始存储数据
优化建议:
调整堆的根节点:每次交换后,堆顶元素需要重新调整,因此堆的结构需要重新保持最大(或最小)堆。
归并排序是一种分治法的排序算法,它将一个大数组分为两个小数组,分别排序后再合并。归并排序的时间复杂度是 O(n log n)
,是稳定的排序算法。
void Merge(int *arr,int low,int mid,int high) {
int k = low;//用于辅助数组的填入
int p = low;
int q = mid + 1;
while (p<=mid&&q<=high)
{
if (arr[p]
优化建议:
空间优化:归并排序需要额外的存储空间,如果空间较紧张,可以考虑使用其他排序算法。
计数排序是一种非比较排序算法,适用于范围较小的整数数据。它的时间复杂度是 O(n + k)
,其中 n
是数组大小,k
是数据的范围。
void CountSort(int* arr, int len) {
int min = arr[0];
int max = arr[0];
for (size_t i = 0; i < len; i++)
{
if (arr[i]max)
{
max = arr[i];
}
}
int num = max - min + 1;//这个是计数数组长度
int* count = new int[num];//构造num长度的数组
for (size_t i = 0; i < num; i++) {//初始化count数组
count[i] = 0;
}
int* arr2 = new int[len];//辅助数组
for (size_t i = 0; i < len; i++)//遍历数组 计数
{
count[arr[i] - min]++;
}
for (size_t i = 1; i < num; i++)//将count数组的值表示小于该值的个数
{
count[i] = count[i - 1] + count[i];
}
for (int i = len-1; i >=0; i--)//从后往前遍历数组 保证稳定性
{
arr2[--count[arr[i]-min]]=arr[i];
}
for (size_t i = 0; i < len; i++)
{
arr[i] = arr2[i];
}
delete[] arr2;
delete[] count;
printfarr(arr, len);
}
优化建议:
负数的处理:如果数组中存在负数,计数排序需要特别处理,确保索引不会越界。
希尔排序是插入排序的一种优化,它通过对数组进行分组并在各组内进行插入排序来减少元素的移动。希尔排序的时间复杂度依赖于增量序列的选择,最优情况下为 O(n log n)
。
void ShellSort(int* arr, int n) {//分组用插入排序
int d, i, j,temp;
for (d = n/2; d >=1 ; d/=2)//增量的变化
{
for ( i = d+1; i < n; i++)//i++交替对不同的组进行插入排序
{
if (arr[i]= 0&&arr[j]>temp; j-=d)
{
arr[j + d] = arr[j];
}
arr[j + d] = temp;
}
}
}
printfarr(arr, n);
}
优化建议:
增量序列的选择:不同的增量序列会显著影响希尔排序的效率。常见的增量序列有 n/2, n/4, ...
,或者使用更优化的序列,如 Hibbard 序列。
简单选择排序通过每次选择未排序部分中的最小元素,并将其交换到已排序部分的末尾。它的时间复杂度为 O(n^2)
,适用于小规模数据的排序。
void SimpleSelect(int* arr, int n) {//简单选择排序
for (size_t i = 0; i < n-1; i++)//循环n-1轮
{
for (size_t j = i+1; j < n; j++)
{
if (arr[j]
优化建议:
减少交换次数:可以通过记录最小元素的下标并只进行一次交换,来减少交换的次数。
本文介绍了几种常见的排序算法及其 C++ 实现。每种排序算法都有其优缺点,在选择排序算法时,应根据实际情况选择适合的算法。例如,快速排序和归并排序适用于大规模数据排序,而插入排序和冒泡排序适合小规模数据。
在实现这些排序算法时,我们还可以做一些优化,例如提前终止不必要的循环,选择合适的基准值等。这些优化能提高排序算法的执行效率。
通过这些排序算法的学习,我不仅掌握了常见的排序技巧,还了解了算法的时间复杂度和空间复杂度,为后续的算法学习奠定了基础。