C/C++数据结构——剖析排序算法

 1. 排序的概念及其运用

1.1 排序的概念

https://en.wikipedia.org/wiki/Insertion_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Insertion_sort

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

1.2 常见的排序算法

                         


// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n);
// 快速排序递归实现
// 快速排序hoare版本
int PartSort1(int* a, int left, int right);
// 快速排序挖坑法
int PartSort2(int* a, int left, int right);
// 快速排序前后指针法
int PartSort3(int* a, int left, int right);
void QuickSort(int* a, int left, int right);
// 快速排序 非递归实现
void QuickSortNonR(int* a, int left, int right);
// 归并排序递归实现
void MergeSort(int* a, int n);
// 归并排序非递归实现
void MergeSortNonR(int* a, int n);
// 计数排序
void CountSort(int* a, int n);
// 测试排序的性能对比
void TestOP()
{srand(time(0));const int N = 100000;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();SelectSort(a3, N);int end3 = clock();int begin4 = clock();HeapSort(a4, N);int end4 = clock();int begin5 = clock();QuickSort(a5, 0, N - 1);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);
}

排序OJ(可使用各种排序跑这个OJ)力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台备战技术面试?力扣提供海量技术面试资源,帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。icon-default.png?t=N7T8https://leetcode-cn.com/problems/sort-an-array/

2. 常见排序算法的解析与实现

2.1 插入排序 
2.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列 。

实际中我们玩扑克牌时,就用了插入排序的思想

                                    

2.1.2 直接插入排序

当插入第i(i>=1)个元素时,前面的array[0],array[1],…,array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移

2.1.3 直接插入排序的实现
// 定义一个插入排序函数
void InsertSort(int* a,int n)
{// 遍历数组,从第一个元素到倒数第二个元素for (int i = 0; i < n-1; i++){// 定义当前元素的位置int end = i;// 保存当前元素的值int temp = a[end + 1];// 循环向前比较,直到找到合适的位置插入当前元素while (end >= 0){if (temp < a[end]){a[end + 1] = a[end];end--;}else{break;}}// 将当前元素插入到合适的位置a[end + 1] = temp;	}
}

直接插入排序的特性总结:
1. 元素集合越接近有序,直接插入排序算法的时间效率越高
2. 时间复杂度:   Worst : O(N^2) ,  Best : O(N)
3. 空间复杂度:O(1),它是一种稳定的排序算法
4. 稳定性:稳定

                      

2.2 希尔排序
2.2.1 基本思想

希尔排序法又称缩小增量法希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达=1时,所有记录在统一组内排好序。

2.2.2 希尔排序的实现

一组一组排:

void ShellSort(int* a, int n)
{int gap = n; // 初始化间隔为数组长度// 开始希尔排序while (gap > 1) // 当间隔大于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 (temp < a[end]) // 如果当前值小于前一个值,则交换位置{a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = temp; // 插入当前值到正确位置}}
}

多组并排:

void ShellSort(int* a, int n)
{int gap = n; // 初始化间隔为数组长度// 开始希尔排序while (gap > 1) // 当间隔大于1时进行排序{gap = gap / 3 + 1; // 根据规则更新间隔// 以下是希尔排序的核心算法for (int j = 0; j < gap; j++) // 以间隔为步长进行插入排序{for (int i = j; i < n - gap; i += gap){int end = i;int temp = a[end + gap]; // 保存当前位置的值while (end >= 0){if (temp < a[end]) // 如果当前值小于前一个值,则交换位置{a[end + gap] = a[end];end -= gap;}else{break;}}a[end + gap] = temp; // 插入当前值到正确位置}}}
}

希尔排序的特性总结:
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
3. 希尔排序的时间复杂度不好计算,因为gap的取值方法很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定:

《数据结构(C语言版)》--- 严蔚敏

《数据结构-用面相对象方法与C++描述》--- 殷人昆

https://en.wikipedia.org/wiki/Shellsorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Shellsort 4. 稳定性:不稳定

2.3 选择排序
2.3.1 基本思想

https://en.wikipedia.org/wiki/Selection_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Selection_sort

每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。

2.3.2 直接选择排序

(1)在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素。
(2)若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
(3)在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。

2.3.3 选择排序的实现
void SelectSort(int* a, int n)
{int begin = 0;int end = n - 1;// 开始选择排序while (begin < end){int min = begin;int max = begin;// 找到当前范围内的最小值和最大值的索引for (int i = begin + 1; i <= end; i++){if (a[i] < a[min]){min = i;}if (a[i] > a[max]){max = i;}}// 将最小值与当前范围的起始位置交换int temp1 = a[begin];a[begin] = a[min];a[min] = temp1;// 如果最大值的索引等于当前范围的起始位置,更新最大值的索引if (max == begin){max = min;}// 将最大值与当前范围的结束位置交换int temp2 = a[max];a[max] = a[end];a[end] = temp2;begin++;end--;}
}

直接选择排序的特性总结:
1. 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

2.4 堆排序
2.4.1 基本思想

堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆

https://en.wikipedia.org/wiki/Heapsorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Heapsort

2.4.2 堆排序的实现
typedef int HeapDataType; // 定义堆数据类型为整型void Swap(int* x, int* y)
{int temp = *x;*x = *y;*y = temp;
}// 向上调整建大堆
void AdjustUp1(HeapDataType* a, int child)
{int parent = (child - 1) / 2; // 计算父节点索引while (child > 0){if (a[child] > a[parent]) // 如果子节点大于父节点{Swap(&a[child], &a[parent]); // 交换子节点和父节点的值child = parent; // 更新子节点索引parent = (parent - 1) / 2; // 更新父节点索引}else{break; // 如果子节点不大于父节点,则退出循环}}
}// 向上调整建小堆
void AdjustUp2(HeapDataType* a, int child)
{int parent = (child - 1) / 2; // 计算父节点索引while (child > 0){if (a[child] < a[parent]) // 如果子节点小于父节点{Swap(&a[child], &a[parent]); // 交换子节点和父节点的值child = parent; // 更新子节点索引parent = (parent - 1) / 2; // 更新父节点索引}else{break; // 如果子节点不小于父节点,则退出循环}}
}// 大堆的向下调整
void AdjustDown1(HeapDataType* a, int size, int parent)
{int child = parent * 2 + 1; // 计算左孩子节点索引while (child < size){if (child + 1 < size && a[child] < a[child + 1]) // 如果右孩子存在且大于左孩子{child++; // 右孩子节点索引加1,选择较大的孩子节点}if (a[child] > a[parent]) // 如果孩子节点大于父节点{Swap(&a[child], &a[parent]); // 交换孩子节点和父节点的值parent = child; // 更新父节点索引child = child * 2 + 1; // 更新孩子节点索引}else{break; // 如果孩子节点不大于父节点,则退出循环}}
}// 小堆的向下调整
void AdjustDown2(HeapDataType* a, int size, int parent)
{int child = parent * 2 + 1; // 计算左孩子节点索引while (child < size){if (child + 1 < size && a[child] > a[child + 1]) // 如果右孩子存在且小于左孩子{child++; // 右孩子节点索引加1,选择较小的孩子节点}if (a[child] < a[parent]) // 如果孩子节点小于父节点{Swap(&a[child], &a[parent]); // 交换孩子节点和父节点的值parent = child; // 更新父节点索引child = child * 2 + 1; // 更新孩子节点索引}else{break; // 如果孩子节点不小于父节点,则退出循环}}
}void HeapSort(HeapDataType* a, int n)
{
#if 1// 大堆排序for (int i = ((n - 1) - 1) / 2; i >= 0; i++) // 从最后一个非叶子节点开始向上调整建堆{AdjustDown1(a, n, i); // 向下调整建大堆}int end = n - 1; // 堆尾索引while (end > 0){Swap(&a[0], &a[end]); // 将堆顶元素与末尾元素交换AdjustDown1(a, end, 0); // 重新调整堆顶元素end--; // 缩小堆范围}#endif#if 0for (int  i = 0; i < n; i++){AdjustUp2(a, i);}int end = n - 1;while (end > 0){Swap(&a[0], &a[end]);AdjustDown2(a, end, 0);end--;}#endif
}

堆排序的特性总结:
1. 堆排序使用堆来选数,效率就高了很多。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(1)
4. 稳定性:不稳定

2.5 冒泡排序

https://en.wikipedia.org/wiki/Bubble_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Bubble_sort

2.5.1 基本思想

基本思想:所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动

2.5.2 冒泡排序的实现
// 定义一个函数,用于交换两个整数指针指向的值
void Swap(int* x, int* y)
{int temp = *x; // 用一个临时变量保存x指向的值*x = *y; // 将y指向的值赋给x指向的值*y = temp; // 将临时变量的值赋给y指向的值
}// 定义一个函数,用于对整型数组进行冒泡排序
void BubbleSort(int* a, int n)
{for (int i = 0; i < n; i++) // 外层循环控制比较的轮数{int flag = 1; // 设置一个标志位,用于判断是否已经完成排序for (int j = 0; j < n - 1 - i; j++) // 内层循环进行相邻元素的比较和交换{if (a[j] > a[j + 1]) // 如果前一个元素大于后一个元素{flag = 0; // 将标志位置为0,表示还未完成排序Swap(&a[j], &a[j + 1]); // 调用Swap函数交换两个元素的值}}if (flag) // 如果标志位仍为1,说明已经完成排序break; // 跳出循环,排序结束}
}

 冒泡排序的特性总结:
1. 冒泡排序是一种非常容易理解的排序
2. 时间复杂度:O(N^2)
3. 空间复杂度:O(1)
4. 稳定性:稳定

2.6 快速排序

https://en.wikipedia.org/wiki/Quicksorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Quicksort

 

2.6.1 基本思想

快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止

// 假设按照升序对array数组中[left, right)区间中的元素进行排序
void QuickSort(int array[], int left, int right)
{if(right - left <= 1)return;// 按照基准值对array数组的 [left, right)区间中的元素进行划分int div = partion(array, left, right);// 划分成功后以div为边界形成了左右两部分 [left, div) 和 [div+1, right)// 递归排[left, div)QuickSort(array, left, div);// 递归排[div+1, right)QuickSort(array, div+1, right);
}
2.6.2 快速排序优化

1. 三数取中法选key
2. 递归到小的子区间时,可以考虑使用插入排序

                ​​​​​​​        ​​​​​​​

这里的partion(分区)方案我选的是三数取中

int partion(int* a, int begin, int end)
{int mid = (begin + end) / 2;if (a[begin] < a[mid]){if (a[end] < a[begin])return begin;else if (a[end] > a[mid])return mid;elsereturn end;}else{if (a[end] < a[mid])return mid;else if (a[end] > a[begin])return begin;elsereturn end;}
}

上述为快速排序递归实现的主框架,发现与二叉树前序遍历规则非常像,在写递归框架时可想想二叉树前序遍历规则即可快速写出来,后序只需分析如何按照基准值来对区间中数据进行划分的方式即可。

2.6.3 快速排序的实现
C/C++库函数实现:

qsort:

int compare(const void* a, const void* b)
{return (*(int*)a - *(int*)b);
}
int main()
{int arr[] = { 3, 2, 6, 8, 4, 10, 0, 9, 5, 7, 1 };qsort(arr, sizeof(arr) / sizeof(arr[0]), sizeof(int), compare);for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)printf("%d ", arr[i]);return 0;
}

sort:

 

// 排序算法示例
#include <iostream>     // 标准输入/输出操作
#include <algorithm>    // std::sort 函数
#include <vector>       // std::vector 容器// 比较两个整数以进行排序的函数
bool myfunction(int i, int j) { return (i < j); }// 用于比较两个整数进行排序的仿函数结构体
struct myclass {bool operator() (int i, int j) { return (i < j); }
} myobject;int main() {int myints[] = { 32,71,12,45,26,80,53,33 };std::vector<int> myvector(myints, myints + 8);               // 使用myints中的值初始化向量// 使用默认比较函数(operator <)对前4个元素进行排序std::sort(myvector.begin(), myvector.begin() + 4);           // 结果: (12 32 45 71) 26 80 53 33// 使用函数myfunction作为比较函数对剩余元素进行排序std::sort(myvector.begin() + 4, myvector.end(), myfunction); // 结果: 12 32 45 71 (26 33 53 80)// 使用对象myobject作为比较函数对整个向量进行排序std::sort(myvector.begin(), myvector.end(), myobject);       // 结果: (12 26 32 33 45 53 71 80)// 打印向量的内容std::cout << "myvector contains:";for (std::vector<int>::iterator it = myvector.begin(); it != myvector.end(); ++it)std::cout << ' ' << *it;std::cout << '\n';return 0;
}
 递归方式实现:

1. hoare版本:

 

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart1(int* a, int left, int right)
{// 找到枢纽元素并对数组进行分区int mid = partion(a, left, right); // 假设partion函数在其他地方定义// 将枢纽元素与最左边元素交换位置Swap(&a[left], &a[mid]);// 初始化用于分区的变量int begin = left;int end = right;int key = begin;// 执行分区操作while (begin < end){// 将'end'指针向左移动,直到找到比枢纽元素小的元素while (begin < end && a[end] >= a[key])end--;// 将'begin'指针向右移动,直到找到比枢纽元素大的元素while (begin < end && a[begin] <= a[key])begin++;// 交换'begin'和'end'指针指向的元素Swap(&a[begin], &a[end]);}// 将枢纽元素放置到正确的位置Swap(&a[begin], &a[key]);return begin;
}
//一定注意是右边先走!!!

【注意】

为什么相遇位置比key要小?

 

2. 挖坑法版本:

 

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart2(int* a, int left, int right)
{// 找到枢纽元素并对数组进行分区int mid = partion(a, left, right); // 假设partion函数在其他地方定义// 将枢纽元素与最左边元素交换位置Swap(&a[left], &a[mid]);// 保存枢纽元素的值int key = a[left];int hole = left;// 执行分区操作while (left < right){// 将'right'指针向左移动,直到找到比枢纽元素小的元素while (left < right && a[right] >= key)right--;// 将找到的元素放入之前枢纽元素的位置a[hole] = a[right];hole = right;// 将'left'指针向右移动,直到找到比枢纽元素大的元素while (left < right && a[left] <= key)left++;// 将找到的元素放入之前枢纽元素的位置a[hole] = a[left];hole = left;}// 将枢纽元素放置到正确的位置a[hole] = key;return hole;
}

3. 前后指针版本:

​​​​​​​

// 参数:
// a: 指向待排序整数数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int QuickSortPart3(int* a, int left, int right)
{// 找到枢纽元素并对数组进行分区int mid = partion(a, left, right); // 假设partion函数在其他地方定义// 将枢纽元素与最左边元素交换位置Swap(&a[left], &a[mid]);// 初始化前后指针int prev = left;int cur = prev + 1;int key = left;// 执行分区操作while (cur <= right){// 如果当前元素小于枢纽元素,则交换当前元素和前指针指向的元素if (a[cur] < a[key]){prev++;Swap(&a[prev], &a[cur]);cur++;}else{cur++;}}// 将枢纽元素放置到正确的位置Swap(&a[prev], &a[key]);key = prev;return key;
}
非递归方式实现(借助栈):
// 参数:
// a: 指向待排序结构体数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
// 返回值:
// 分区后枢纽元素所在的索引
int PartSort1(STDataType* a, int left, int right)
{// 找到枢纽元素并对数组进行分区int mid = GetMid(a, left, right);// 将枢纽元素与最左边元素交换位置Swap(&a[left], &a[mid]);// 初始化起始和结束指针int begin = left;int end = right;int key = begin;// 执行分区操作while (begin < end){// 将'end'指针向左移动,直到找到比枢纽元素小的元素while (begin < end && a[end] >= a[key])end--;// 将'begin'指针向右移动,直到找到比枢纽元素大的元素while (begin < end && a[begin] <= a[key])begin++;// 交换找到的元素的位置Swap(&a[begin], &a[end]);}// 将枢纽元素放置到正确的位置Swap(&a[key], &a[begin]);return begin;
}// 参数:
// a: 指向待排序结构体数组的指针
// left: 数组最左边元素的索引
// right: 数组最右边元素的索引
void QuickSortNonR(STDataType* a, int left, int right)
{Stack st;StackInit(&st);StackPush(&st, right);StackPush(&st, left);// 非递归快速排序while (!StackEmpty(&st)){int begin = StackTop(&st);StackPop(&st);int end = StackTop(&st);StackPop(&st);int key = PartSort1(a, begin, end);// 将未排序部分压入栈中if (key + 1 < end){StackPush(&st, end);StackPush(&st, key + 1);}if (key - 1 > begin){StackPush(&st, key - 1);StackPush(&st, begin);}}StackDestroy(&st);
}

快速排序的特性总结:
1. 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
2. 时间复杂度:O(N*logN)

        ​​​​​​​        ​​​​​​​        

3. 空间复杂度:O(logN)
4. 稳定性:不稳定 

2.7 归并排序

https://en.wikipedia.org/wiki/Merge_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Merge_sort

2.7.1 基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

 

2.7.2 归并排序的实现 
递归方式实现:
/*** @brief 归并排序算法的实现函数,使用递归的方式进行排序* * @param a 待排序数组的指针* @param begin 子数组的起始位置* @param end 子数组的结束位置* @param temp 临时数组用于存放排序结果*/
void _MergeSort(int* a, int begin, int end, int* temp)
{if (begin >= end)return;// 计算中间位置int mid = (begin + end) / 2;// 递归调用_MergeSort对左右两个子数组进行排序_MergeSort(a, begin, mid, temp);_MergeSort(a, mid + 1, end, temp);// 合并两个有序子数组int begin1 = begin;int end1 = mid;int begin2 = mid + 1;int end2 = end;int i = begin; // i表示临时数组的下标while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){temp[i] = a[begin1];i++;begin1++;}else{temp[i] = a[begin2];i++;begin2++;}}// 处理剩余元素while (begin1 <= end1){temp[i] = a[begin1];i++;begin1++;}while (begin2 <= end2){temp[i] = a[begin2];i++;begin2++;}// 将排序后的结果拷贝回原数组memcpy(a + begin, temp + begin, sizeof(int) * ((end + 1) - begin));
}/*** @brief 归并排序算法的入口函数,分配临时数组并调用_MergeSort函数* * @param a 待排序数组的指针* @param n 数组的大小*/
void MergeSort(int* a, int n)
{// 分配临时数组int* temp = (int*)malloc(sizeof(int) * n);if (temp == NULL){perror("malloc fail");return;}// 调用_MergeSort函数对整个数组进行排序_MergeSort(a, 0, n - 1, temp);// 释放临时数组内存free(temp);
}
*/
非递归方式实现: 
// 这个函数以迭代方式实现归并排序算法,而不使用递归
// 参数:
// a:要排序的整数数组
// n:数组中元素的数量
// 返回值:voidvoid MergeSortNonR(int* a, int n)
{// 分配内存用于临时数组以存储排序后的元素int* temp = (int*)malloc(sizeof(int) * n);// 检查内存分配是否成功if (temp == NULL){printf("malloc失败");return;}// 初始化要比较的元素之间的间隔int gap = 1;// 循环直到间隔小于元素数量while (gap < n){// 以当前间隔大小的增量遍历数组for (int i = 0; i < n; i += (gap) * 2) {// 定义要合并的两个子数组的边界int begin1 = i;int end1 = i + gap - 1;int begin2 = i + gap;int end2 = i + 2 * gap - 1;// 检查边界是否超过数组大小if (end1 >= n || begin2 >= n){break;}// 如果结束边界超过数组大小,则调整它if (end2 >= n){end2 = n - 1;}// 打印正在合并的子数组的边界// printf("[%2d,%2d][%2d,%2d]\n", begin1, end1, begin2, end2);int j = begin1;// 按顺序合并两个子数组while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){temp[j] = a[begin1];j++;begin1++;}else{temp[j] = a[begin2];j++;begin2++;}}while (begin1 <= end1){temp[j] = a[begin1];j++;begin1++;}while (begin2 <= end2){temp[j] = a[begin2];j++;begin2++;}memcpy(a + i, temp + i, sizeof(int) * ((end2 - i) + 1));}gap *= 2;}
}

归并排序的特性总结:
1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
2. 时间复杂度:O(N*logN)
3. 空间复杂度:O(N)
4. 稳定性:稳定

2.8 计数排序

https://en.wikipedia.org/wiki/Counting_sorticon-default.png?t=N7T8https://en.wikipedia.org/wiki/Counting_sort

2.8.1 基本思想

计数排序是一种根据小正整数键对对象集合进行排序的算法。也就是说,它是一种整数排序算法。它通过对拥有不同键值的对象数量进行计数,并对这些计数应用前缀和来确定每个键值在输出序列中的位置来进行操作

2.8.2 计数排序的实现
// 参数:
// a:要排序的整数数组
// n:数组中元素的数量
// 返回值:void
void CountSort(int* a, int n)
{// 寻找数组中的最小值和最大值int min = a[0];int max = 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));// 检查内存分配是否成功if (count == NULL){printf("calloc失败");return;}// 统计每个元素出现的次数for (int i = 0; i < n; i++){count[a[i] - min]++;}// 将排序后的元素放回原数组int i = 0;for (int j = 0; j < range; j++){while (count[j]--){a[i++] = j + min;}}
}

计数排序的特性总结: 

1. 时间复杂度:O(N+k) , 其中N为数组的长度,k为哈希表的长度
2. 空间复杂度:O(N+k), 需要用到一个哈希表和一个额外数组
3. 稳定性:稳定

3. 排序算法复杂度及稳定性分析

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/688211.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

力扣hot2--哈希

推荐博客&#xff1a; for(auto i : v)遍历容器元素_for auto 遍历-CSDN博客 字母异位词都有一个特点&#xff1a;也就是对这个词排序之后结果会相同。所以将排序之后的string作为key&#xff0c;将排序之后能变成key的单词组vector<string>作为value。 class Solution …

探索未来科技前沿:深度学习的进展与应用

深度学习的进展 摘要&#xff1a;深度学习作为人工智能领域的重要分支&#xff0c;近年来取得了巨大的进展&#xff0c;并在各个领域展现出惊人的应用潜力。本文将介绍深度学习的发展历程、技术原理以及在图像识别、自然语言处理等领域的应用&#xff0c;展望深度学习在未来的…

SQL29 计算用户的平均次日留存率(lead函数的用法)

代码 with t1 as(select distinct device_id,date --去重防止单日多次答题的情况from question_practice_detail ) select avg(if(datediff(date2,date1)1,1,0)) as avg_ret from (selectdistinct device_id,date as date1,lead(date) over(partition by device_id order by d…

C++入门学习(二十八)跳转语句—continue语句

当在循环中遇到continue语句时&#xff0c;它会跳过当前迭代剩余的代码块&#xff0c;并立即开始下一次迭代。这意味着continue语句用于跳过循环中特定的执行步骤&#xff0c;而不是完全终止循环。 直接看一下下面的代码更清晰&#xff1a; 与上一节的break语句可以做一下对比…

什么是软件测试?软件测试的目的与原则是什么?

&#x1f525; 交流讨论&#xff1a;欢迎加入我们一起学习&#xff01; &#x1f525; 资源分享&#xff1a;耗时200小时精选的「软件测试」资料包 &#x1f525; 教程推荐&#xff1a;火遍全网的《软件测试》教程 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1…

原创详解OpenAI Sora是什么?技术先进在哪里?能够带来什么影响?附中英文技术文档

一&#xff1a;Sora是什么 Sora是一个文本到视频的模型&#xff0c;由美国的人工智能研究机构OpenAI开发。Sora可以根据描述性的文本提示&#xff0c;生成高质量的视频&#xff0c;也可以根据已有的视频&#xff0c;向前或向后延伸&#xff0c;生成更长的视频。 Sora的主要功…

反射的作用

获取一个类里面所有的信息&#xff0c;获取到了之后&#xff0c;再执行其他的业务逻辑结合配置文件&#xff0c;动态的创建对象并调用方法 练习1&#xff1a; public class MyTest {public static void main(String[] args) throws IllegalAccessException, IOException {Stude…

Swift 5.9 新 @Observable 对象在 SwiftUI 使用中的陷阱与解决

概览 在 Swift 5.9 中&#xff0c;苹果为我们带来了全新的可观察框架 Observation&#xff0c;它是观察者开发模式在 Swift 中的一个全新实现。 除了自身本领过硬以外&#xff0c;Observation 框架和 SwiftUI 搭配起来也能相得益彰&#xff0c;事倍功半。不过 Observable 对象…

C++学习Day06之继承方式

目录 一、程序及输出1.1 公共继承1.1.1 父类中公共成员&#xff0c;子类可以正常访问1.1.2 父类中保护成员&#xff0c;子类类外不可以访问1.1.3 父类中私有成员&#xff0c;子类无法访问 1.2 保护继承1.2.1 父类中公共权限 子类中变为 保护权限1.2.2 父类中保护权限 子类中变为…

自定义Linux登录自动提示语

设置提示语的方式 在Linux系统中&#xff0c;可以通过修改几个特定的文件来实现在用户登录时自动弹出提示语。以下是几个常用的方法&#xff1a; 1. 修改/etc/issue文件&#xff1a; 这个文件用于显示本地登录前的提示信息 sudo vi /etc/issue在项目合作的时候&#xff0c;…

平衡二叉树(AVL树)

定义&#xff1a; 左右子树高度之差不超过1左右子树都是平衡二叉树 平衡二叉树的增删操作都离不开二叉树的调整 二叉树调整 LL型&#xff1a;右旋 LR型&#xff1a;左旋右旋 RR型&#xff1a;左旋 RL型&#xff1a;右旋左旋

FPFH特征描述符、对应关系可视化以及ICP配准

一、FPFH特征描述符可视化 C #include <pcl/point_types.h> #include <pcl/point_cloud.h> #include <pcl/search/kdtree.h> #include <pcl/io/pcd_io.h> #include <pcl/features/normal_3d_omp.h>//使用OMP需要添加的头文件 #include <boo…

批量追踪中通快递

在物流信息的管理中&#xff0c;批量追踪中通快递单号一直是个让人头疼的问题。但有了固乔快递查询助手&#xff0c;这一切都变得轻而易举。 固乔快递查询助手&#xff0c;作为市场上备受好评的快递查询软件&#xff0c;专门针对批量查询需求进行了优化。用户只需将中通快递单号…

【Linux】简单的网络计算器的实现(自定义协议,序列化,反序列化)

文章目录 前言一、 服务端1.ServerCal.cc&#xff08;服务器主文件&#xff09;2.ServerCal.hpp3.Sock.hpp(套接字封装)4.TcpServer.hpp(服务器)5.Protocol&#xff08;自定义协议&#xff09; 二、用户端1.ClientCal 三、Log.hpp&#xff08;日志&#xff09;四、makefile 前言…

【Qt学习】QIcon类 + 利用qrc机制设置图片路径(QtCreator)

文章目录 1. QIcon / windowIcon2. setIcon() 与 setwindowIcon()2.1 setIcon() 介绍与使用2.2 setWindowIcon 介绍与使用 3. 路径问题 & qrc机制的引入3.1 绝对路径 / 相对路径 的问题3.2 qrc机制3.3 在QtCreator下利用qrc机制引入图片 1. QIcon / windowIcon QIcon QIco…

单输入Doherty 功率放大器的极限带宽分析(含Matlab分析代码)

单输入Doherty 功率放大器的极限带宽分析&#xff08;含Matlab分析代码&#xff09; 参考论文&#xff1a;高效连续型射频功率放大器研究 DPA的基础理论参考&#xff1a;理想架构的Doherty功率放大器理论与仿真 1、单输入Doherty 功率放大器架构 假设 Doherty 功率放大器的…

介绍7款免费的最佳地图/导航/定位/GIS开源项目

文章目录 1、xdh-map新德汇地图应用类库1.1、独立引用1.2、与MyUI结合使用1.3、快速上手1.3.1、采用项目工程模板创建项目【推荐】1.3.2、 调用组件库功能 2、蚂蚁金服AntV-L7地理空间数据可视分析引擎2.1、AntV-L7简介2.2、核心特性2.3、支持丰富的图表类型2.4、如何使用2.4.1…

反转一个单链表

反转一个单链表 题意&#xff1a;反转一个单链表。 示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 思路 需要虚拟节点么&#xff1f; 答&#xff1a;不需要&#xff0c;因为没有删除节点&#xff0c;只是改变了节点的指向。 遍…

计算机设计大赛 深度学习二维码识别

文章目录 0 前言2 二维码基础概念2.1 二维码介绍2.2 QRCode2.3 QRCode 特点 3 机器视觉二维码识别技术3.1 二维码的识别流程3.2 二维码定位3.3 常用的扫描方法 4 深度学习二维码识别4.1 部分关键代码 5 测试结果6 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天…

Rust-知多少?

文章目录 前言1.使用下划线开头忽略未使用的变量2. 变量解构3.常量4.变量遮蔽&#xff08;shadowing&#xff09;5. 类似println!("{}", x); 为啥加感叹号6.单元类型总结 前言 Rust 学习系列&#xff0c;记录一些rust使用小技巧 1.使用下划线开头忽略未使用的变量 …