#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
typedef int bool;
#define true 1
#define false 0
void bubbleSort(int arr[], int n); //冒泡排序
void bubbleSort2(int arr[] , int n); //冒泡排序 改进一
void bubbleSort3(int arr[], int n); //冒泡排序 改进二
void insertionSort(int arr[], int n); //直接插入排序
void shellSort(int arr[], int n); //希尔排序
void selectionSort(int arr[], int n); //简单选择排序
void mergeSort_iteration(int arr[], int n); //归并排序(迭代)
void mergeSort_recursion(int arr[], int n); //归并排序(递归)
void heapSort(int arr[], int n); //堆排序
void quickSort(int arr[], int n); //快速排序
void countSort(int arr[], int n); //计数排序
void bucketSort(int arr[], int n); //桶排序
void radixSort_LSD(int arr[], int n); //基数排序(LSD低位优先)
int main(int argc, const char * argv[]) {
// int arr[] = {12,4,23,188,53,23,1,43,63,7};
int arr[] = {20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0};
// int arr[] = {15,28,17,6,14,29,80,30,50};
int num = sizeof(arr)/sizeof(arr[0]);
printf("Before Sorting:");
for (int i = 0; i<num; i++) {
printf("%d ",arr[i]);
}
printf("\n");
// bubbleSort(arr, num); //冒泡排序
// bubbleSort2(arr, num); //冒泡排序 改进一
// bubbleSort3(arr, num); //冒泡排序 改进二
// insertionSort(arr, num); //直接插入排序
// shellSort(arr, num); //希尔排序
// selectionSort(arr, num); //简单选择排序
// mergeSort_iteration(arr, num); //归并排序(迭代)
// mergeSort_recursion(arr, num); //归并排序(递归)
// heapSort(arr, num); //堆排序
// quickSort(arr, num); //快速排序
// countSort(arr, num); //计数排序
// bucketSort(arr, num); //桶排序
radixSort_LSD(arr, num); //基数排序
printf("After Sorting:");
for (int i = 0; i<num; i++) {
printf("%d ",arr[i]);
}
printf("\n");
return 0;
}
//数值交换函数
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
/*冒泡排序 时间复杂度O(n^2) 稳定*/
void bubbleSort(int arr[], int n){
for (int i = 0; i<n; i++) {
for (int j = 0; j<n-i-1; j++) {
if (arr[j] > arr[j+1]) {
swap(&arr[j], &arr[j+1]);
}
}
}
}
/*冒泡排序改进一
思路:设置标记位标记序列是否已经有序,若一遍排序过程没有交换,说明序列已经有序,停止排序
时间复杂度最佳为O(n) 最坏为O(n^2)*/
void bubbleSort2(int arr[], int n){
for (int i = 0; i<n; i++) {
bool isOrdered = true;
for (int j = 0; j<n-i-1; j++) {
if (arr[j] > arr[j+1]) {
swap(&arr[j], &arr[j+1]);
isOrdered = false;
}
}
if (isOrdered) {
break;
}
}
}
/*冒泡排序改进二
思路:设置标记位标记一轮下来最后的交换位置,再此位置之后的序列已经有序,无需再遍历
最佳时间复杂度O(n) 最差时间复杂度O(n^2)*/
void bubbleSort3(int arr[], int n){
int k = n;
int tag = k;
while (tag) {
k = tag;
tag = 0;
for (int i = 0; i<k-1; i++) {
if (arr[i] > arr[i+1]) {
swap(&arr[i], &arr[i+1]);
tag = i+1; //标记最后交换的位置
}
}
}
}
/*直接插入排序
思路:将第一个元素作为一个有序数列,依次遍历后面的元素,每遍历一个元素将其置入有序数列,规则为若该元素比前一个数大,则直接置入;若该数比前一个数小,则交换两数后继续与前一个数比较,直到比前一个数大或称为第一个元素
最佳时间复杂度O(n) 最坏时间复杂度O(n^2)
稳定
适用于整体递增(目标升序)/递减(目标降序)序列*/
void insertionSort(int arr[], int n){
for (int i = 1; i<n; i++) {
int k = i;
while (k && arr[k]<arr[k-1]) {
swap(&arr[k], &arr[k-1]);
k--;
}
}
}
/*希尔排序
思路:对于一个序列,设置一定的增量递减规则,对于每一个增量,将序列分成若干个子序列分别进行直接插入排序。当增量递减到1时,则为整个序列的直接插入排序。
希尔排序是对直接插入排序的改进,按照增减递减序列进行多趟排序使得序列整体逐渐向有序靠近。
时间复杂度平均O(n^1.3)
不稳定
*/
void shellInsertionSort(int arr[], int n, int inc){
for (int i = 0; i<inc; i++) {
for (int j = 0; j<n; j+=inc) {
int k = j;
while (k-i && arr[k] < arr[k-inc]) {
swap(&arr[k], &arr[k-inc]);
k -= inc;
}
}
}
}
void shellSort(int arr[], int n){
int inc = n/2; //这里按照增量递减序列(n/2,n/4,n/8,...,1)进行排序
while (inc) {
shellInsertionSort(arr, n, inc);
inc /= 2;
}
}
/*简单选择排序 时间复杂度O(n^2) 稳定*/
void selectionSort(int arr[], int n){
for (int i = 0; i<n; i++) {
int minIndex = i;
for (int j = i; j<n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
swap(&arr[minIndex], &arr[i]);
}
}
}
/*归并排序(迭代)
思路:将原始序列的每一个元素有看成是一个有序的序列,从左到右将相邻的有序序列两两合并成一个新的有序序列,此时各个有序序列的元素个数为2或1(1为原始序列个数为奇数情况下最后一个有序序列元素的个数)。重复操作,将新得到的有序序列从左到右合并成新的有序序列。最后一次合并是将元素索引为0~(n-1)/2的序列与(n-1)/2+1~n-1的序列合并。
时间复杂度O(nlogn)
稳定*/
void merge_iteration(int arr[], int *temArray, int n){
int inc = 1;
while (inc<n) {
for (int i = 0; i<n; i+=inc*2) {
int lBegin = i;
int lEnd = i+inc-1;
int rEnd = (i+inc*2-1)>n-1?n-1:(i+inc*2-1);
int lHalfIndex = lBegin;
int rHalfIndex = lEnd+1;
int temIndex = lBegin;
while (lHalfIndex<=lEnd && rHalfIndex<=rEnd) {
if (arr[lHalfIndex]<=arr[rHalfIndex]) {
temArray[temIndex++] = arr[lHalfIndex++];
}else{
temArray[temIndex++] = arr[rHalfIndex++];
}
}
while (lHalfIndex<=lEnd) {
temArray[temIndex++] = arr[lHalfIndex++];
}
while (rHalfIndex<rEnd) {
temArray[temIndex++] = arr[rHalfIndex++];
}
for (int i = temIndex-1; i>=lBegin; i--) {
arr[i] = temArray[i];
}
}
inc *= 2;
}
}
void mergeSort_iteration(int arr[], int n){
int *temArray;
temArray = (int *)malloc(sizeof(int)*n);
if (temArray == NULL) {
printf("error to alloc memory!");
exit(1);
}
merge_iteration(arr, temArray, n);
free(temArray);
}
/*归并排序(递归)
时间复杂度O(nlogn)
稳定*/
void merge_recursion(int arr[], int temArr[], int lBegin, int lEnd, int rEnd){
if (lBegin < rEnd) {
merge_recursion(arr, temArr, lBegin, (lBegin+lEnd)/2, lEnd);
merge_recursion(arr, temArr, lEnd+1, (lEnd+1+rEnd)/2, rEnd);
int lHalfIndex = lBegin;
int rHalfIndex = lEnd+1;
int temIndex = lBegin;
while (lHalfIndex <= lEnd && rHalfIndex <= rEnd) {
if (arr[lHalfIndex] <= arr[rHalfIndex]) {
temArr[temIndex++] = arr[lHalfIndex++];
}else{
temArr[temIndex++] = arr[rHalfIndex++];
}
}
while (lHalfIndex <= lEnd) {
temArr[temIndex++] = arr[lHalfIndex++];
}
while (rHalfIndex <= rEnd) {
temArr[temIndex++] = arr[rHalfIndex++];
}
for (int i = temIndex-1; i>=lBegin; i--) {
arr[i] = temArr[i];
}
}
}
void mergeSort_recursion(int arr[], int n){
int *temArray;
temArray = (int *)malloc(n * sizeof(int));
if (temArray == NULL) {
printf("error to alloc memory!");
exit(1);
}
int centerIndex = (n-1)/2;
merge_recursion(arr, temArray, 0, centerIndex, n-1);
free(temArray);
}
/*堆排序 时间复杂度O(nlogn) 不稳定
说明:将序列数组模拟成一个堆,则堆是一个完全二叉树结构。堆分为小根堆和大根堆,小根堆所有的根节点小于其子节点,大根堆所有根节点大于其子节点。若要对一个序列进行升序排序,只要借助堆的性质,将序列构造成堆,这样堆顶元素便是序列中的最小(或最大)元素,将其输出后将剩余元素重新调整成堆,重复操作,每次便可获取到最小(最大)元素。
思路:①将序列构造成堆,并输出堆顶元素。 ②将剩余n-1个元素重新调整成堆结构,输出堆顶元素,重复操作。
难点:①如何构造堆 ②获取堆顶元素后,如何调整剩余n-1个元素使之成为新堆。
讨论:
首先讨论第②个问题,实现思路如下(这里以小根堆为例)
1.一个有n个元素的堆,输出堆顶元素后,将堆底元素送入堆顶,这时堆结构被破坏。
2.将根节点元素与左右子树中较小的元素交换。
3.若与左子树交换,判断左子树堆是否被破坏(若根节点比左右节点之一大则破坏)。若左子树堆被破坏,则对左子树重复 2 操作。
4.若与右子树交换,判断右子树堆是否被破坏。若右子树堆被破坏,则对右子树重复 2 操作。
5.继续对交换后不满足堆性质的子树进行上述操作,直到叶子节点。
上述这一过程暂且称之为堆的调整。
下面讨论第①个问题,如何构建堆?思路如下:
由于序列数组对应的堆是一个完全二叉树结构,因此最后一个叶子节点的父节点对应的索引为(n-2)/2,n为元素个数。当只有一个元素时,不存在父节点。要做的是:从索引为(n-2)/2的节点依次遍历到索引为0的节点,每遍历一个节点,以该节点为根节点,对其所在的树进行②的调整。
*/
//堆的调整 n为元素个数 nodeIndex为要调整的树的根节点索引
void adjustHeap(int arr[],int n,int nodeIndex){
int leftIndex = 2*nodeIndex+1;
while (leftIndex < n) {
int minIndex;
if ((leftIndex+1) <= (n-1)) { //存在右子树
minIndex = arr[leftIndex]<arr[leftIndex+1]?leftIndex:leftIndex+1;
}else{
minIndex = leftIndex;
}
if (arr[nodeIndex] < arr[minIndex]) {
break;
}
int temp = arr[nodeIndex];
arr[nodeIndex] = arr[minIndex];
arr[minIndex] = temp;
nodeIndex = minIndex;
leftIndex = nodeIndex*2+1;
}
}
//堆的构建
void constructHeap(int arr[], int n){
if (n>1) {
int nodeIndex = (n-1-1)/2;
for (int i = nodeIndex; i>=0; i--) {
adjustHeap(arr,n,i);
}
}
}
void heapSort(int arr[], int n){
constructHeap(arr, n); //构造小根堆
int *tempArray = (int *)malloc(sizeof(int)*n);
tempArray[0] = arr[0]; //输出堆顶元素
arr[0] = arr[n-1]; //将堆底元素送入堆顶
for (int i = n-2; i>=0; i--) { //堆的调整,重复操作,每次调整后都将堆顶元素输出,并将堆底元素送入堆顶
adjustHeap(arr,i+1,0);
tempArray[n-i-1] = arr[0];
arr[0] = arr[i];
}
for (int i = 0; i<n; i++) {
arr[i] = tempArray[i];
}
free(tempArray);
}
/*快速排序 时间复杂度O(nlogn) 不稳定
思路:
①初始时将原始序列第一个元素看成锚点,进行一趟排序,结果使得该锚点左侧的元素均比锚点小,右侧的元素均比锚点大。
②分别将锚点左侧的元素和右侧的元素重新看成一个新的序列,重复①的操作,直到子序列只有一个元素。
难点:如何调整序列使得一趟排序之后锚点左侧元素均比其小,右侧元素均比其打?
步骤:
①对于一个序列,设置两个标记left和righ分别标记首元素和末尾元素,初始将left所指的元素看成锚点。
②若锚点为left标记的元素,将其与right标记的元素比较。若锚点小于等于right标记的元素,right左移;若锚点大于right标记的元素,交换两元素,并将left右移,这时候锚点由right标记。
③若锚点为right标记的元素,将其与left标记的元素比较。若锚点大于等于left标记的元素,left右移;若锚点小于left标记的元素,交换两元素,并将right左移,这时候锚点由left标记。
④重复②③操作,直到left与right重合。
*/
//调整left与right包围的序列,使锚点左侧元素均比其小,右侧元素均比其大。
int adjustSequence(int arr[], int n, int left, int right){
int sign = 0; //标记 0表示锚点在左,1表示锚点在右
while (left != right) {
if (sign == 0) {
if (arr[left] <= arr[right]) {
right--;
}else{
swap(&arr[left], &arr[right]);
left++;
sign = 1;
}
}else{
if (arr[right] >= arr[left]) {
left++;
}else{
swap(&arr[left], &arr[right]);
right--;
sign = 0;
}
}
}
return left;
}
void qSort(int arr[], int n, int left, int right){
if (left < right) {
int loc = adjustSequence(arr, n, left, right);
qSort(arr, n, left, loc-1);
qSort(arr, n, loc+1, right);
}
}
void quickSort(int arr[], int n){
qSort(arr, n, 0, n-1);
}
/*计数排序 时间复杂度O(n+k)
思路:计数排数借助一个用于计数的辅助数组,该数组的容量为序列最大元素max与最小元素min的差值+1,第i个元素值为序列中值为(i+min)的元素个数。之后遍历整个辅助数组,若值为x(x不为0),说明原始序列中有x个值为辅助数组当前索引值的元素,输出x次索引值即可。
缺点:执行过程中需要动态分配一个用于计数的辅助数组,数组容量取决于原始序列中数值的范围,对于范围很大的序列,需要消耗大量的时间和内存。
*/
void countSort(int arr[], int n){
int max = arr[0];
int min = arr[0];
for (int i = 1; i<n; i++) { //n
if (max < arr[i]) {
max = arr[i];
}
if (min > arr[i]) {
min = arr[i];
}
}
int k = max-min+1;
int *tempArray = (int *)malloc(sizeof(int)*k);
if (tempArray == NULL) {
printf("fail to alloc memory!");
exit(1);
}
memset(tempArray, 0, sizeof(int)*k);
for (int i = 0; i<n; i++) {
tempArray[arr[i]-min] += 1;
}
int index = 0;
for (int i = 0; i<k; i++) {
if (tempArray[i] != 0) {
for (int j = 0; j<tempArray[i]; j++) {
arr[index] = i+min;
index++;
}
}
}
free(tempArray);
}
/*桶排序
桶排序采用的是分治的思想,将序列元素按照某一规则分成若干组,每个组便是一个“桶”,将各个桶里的元素按照任意一种排序方法排好序之后,最终将所有元素重新合并成一个有序序列。
例如对一组数值为0~100的均匀分布的元素序列进行桶排序,步骤如下:
①设计好元素的映射规则,可以设置“0号~9号”10个“桶”,分别存放“0~9、10~19、...、90~100”范围内的元素,这样元素就能根据“n/10”映射到对应的桶。
②创建10个“空桶”,可以用链表的形式表示每一个桶,一个“空桶”即为一个链表的头结点。
③遍历序列元素,将元素映射到“桶”里。
④遍历每一个“桶”,对其中的元素排序
⑤合并每一个“桶”里的元素,得到新的排好序的序列。
时间复杂度:对于桶排序而言,分的“桶”越多,消耗的时间就越少,但是空间开销就越大,这也就是时间空间不两立。假设有n个元素,m个桶,元素大小是均匀分布的,那么平均每一桶里有n/m个元素。若桶内元素采用的是快速排序的方法,那么时间复杂度将是O(n+m*n/m*log(n/m))即O(n+n(logn-logm)),可见当m越接近n,时间复杂度越靠近O(n)。
稳定性无法确定,取决于分桶后的排序方法
*/
#define bucketNum 10 //分10个桶
typedef struct node{
int data;
struct node *next;
}*Node;
void bucketSort(int arr[], int n){
Node bucket[bucketNum];
for (int i = 0; i<bucketNum; i++) {
bucket[i] = (Node)malloc(sizeof(Node));
bucket[i]->data = 0; //链头节点data标记当前链表中节点数(即当前桶中的元素个数)
bucket[i]->next = NULL;
}
int max = arr[0];
int min = arr[0];
for (int i = 1; i<n; i++) {
if (arr[i] > max) {
max = arr[i];
}
if (arr[i] < min) {
min = arr[i];
}
}
int interval = max-min+1;
int bucketSpace = (int)ceil(interval/(float)bucketNum); //计算每个桶的容量
printf("元素映射:\n");
for (int i = 0; i<n; i++) {
int key = (arr[i]-min)/bucketSpace; //获取元素映射桶的下标
printf("%d:%d\n",arr[i],key);
Node item = (Node)malloc(sizeof(Node));
item->data = arr[i];
Node temp = bucket[key];
if (!temp->next) { //空桶
temp->next = item;
item->next = NULL;
bucket[key]->data++;
}else{ //非空桶 插入链表排序排序
while (temp->next && temp->next->data<item->data) {
temp = temp->next;
}
item->next = temp->next;
temp->next = item;
bucket[key]->data++;
}
}
printf("映射完毕:\n");
//合并桶中元素
int index = 0;
Node temp;
for (int i = 0; i<bucketNum; i++) {
printf("%d:",i);
if (bucket[i]->data) {
temp = bucket[i];
while (temp->next) {
arr[index] = temp->next->data;
printf("%d ",temp->next->data);
temp = temp->next;
index++;
}
}
printf("\n");
}
//释放动态分配的内存
for (int i = 0; i<bucketNum; i++) {
temp = bucket[i];
while (temp->next) {
temp = temp->next;
bucket[i]->next = temp->next;
free(temp);
}
free(bucket[i]);
}
}
/*基数排序
计数排序和桶排序都只是研究一个关键字的排序,基数排序则是针对多个关键字的排序。
对于扑克牌而言,每一张牌牌值由两个要素构成:
花色:梅花<方块<红心<黑心 ♣ < ♦ < ♥ < ♠
面值:2<3<4<5<6<7<8<9<10<J<Q<K<A
那么牌值的大小顺序为 ♣2<♣3<♣4<...<♣A<♦2<♦3<♦4<...<♦A<♥2<♥3<♥4<...<♥A<♠2<♠3<♠4<...<♠A
对于一副牌的排序,按照基数排序,有这样两种方案:
①先将牌按花色排序,将其分成4个组,即梅花组,方块组,红心组,黑心组。再对每个组的牌按面值排序,最后将4个组按顺序合并起来即可。
②先将牌按面值排序,将其分成13个组。再对每个组的牌按花色排序,最后将13个组按顺序合并起来即可。
上面两种方案,对应基数排序的两种方案:最高位优先(Most Significant Digit first简称MSD)和最低位优先(Least Significant Digit first简称LSD)。顾名思义,前者以权重大的关键字优先排序,后者以权重小的关键字优先排序。
对于多关键字的元素排序而言,只需要按照各个关键字逐个分组排序,最终合并所有分组即可,因此基数排序本质上是分解成了多个桶排序。至于基数排序的“基数”,是指关键字的范围,例如扑克牌花色的基数值为4,面值的基数值为13。而对于数值或字符类型的序列,若将其看成单关键字序列进行基数排序,那么它就是桶排序。其实我们可以把数值看成是多关键字的元素,关键字是它的数位值(个位、十位...),每个关键字的基数值均为10。
*/
void radixSort_LSD(int arr[], int n){
Node bucket[10]; //由于对于数值而言,各个关键字的基数值都为10,因此只需借助一个“桶数组”即可
Node tail[10]; //用于保存各个桶链表链尾结点的指针,这样插入结点的时候可以避免遍历整个链表
memset(bucket, 0, sizeof(bucket));
memset(tail, 0, sizeof(tail));
int max = arr[0];
for (int i = 0; i<n; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
//计算最大位数
int maxDigit = 1;
while (max >= (int)pow(10, maxDigit)) {
maxDigit++;
}
for (int digit = 0; digit<maxDigit; digit++) { //对每一位(每个关键字)做桶排序
//关键字为第digit位的桶排序
for (int i = 0; i<n; i++) {
Node node = (Node)malloc(sizeof(Node));
node->data = arr[i];
int key = arr[i]/(int)pow(10, digit)%10; //获取映射桶的索引
if (bucket[key] == NULL) {
bucket[key] = node;
tail[key] = node;
}else{
tail[key]->next = node;
tail[key] = node;
}
}
//桶中数值送回数组同时回收内存
int index = 0;
Node temp;
for (int key = 0; key<10; key++) {
temp = bucket[key];
while (temp != NULL) {
arr[index] = temp->data;
bucket[key] = bucket[key]->next;
free(temp);
temp = bucket[key];
index++;
}
}
printf("digit %d:",digit);
for (int i = 0; i<n; i++) {
printf("%d ",arr[i]);
}
printf("\n");
}
}
十大排序算法再总结
最新推荐文章于 2024-09-19 19:03:21 发布