内部排序是指待排序列完全存放在内存中所进行的排序过程,适合不太大的元素序列。
排序是计算机程序设计中的一种重要操作,其功能是对一个数据元素集合或序列重新排列成一个按数据元素某个相知有序的序列。排序分为两类:内排序和外排序。
其中快速排序的是目前排序方法中被认为是最好的方法。
内部排序方法:
1.插入排序(直接插入排序);
2.快速排序;
3.选择排序(简单选择排序);
4.归并排序;
5.冒泡排序;
6.希尔排序(希尔排序是对直接插入排序方法的改进);
7.堆排序;
——摘自百度百科
#ifndef SORT_H_ #define SORT_H_ #define ARRAY_LEN 1000 // 数组长度 #define MIN 1 // 数组的最小值 #define MAX 1000 // 数组的最大值 int Comparisons_num; // 比较次数 int Mobile_num; // 移动次数 void Create_data(int *a, int n, int min, int max); // 建立伪随机 void Copy_array(int *tar, int *arr, int len); // 复制数组 void Swap_element(int *a, int *b); // 交换元素 void Insert_sort(int *arr, int len); // #1 直接插入排序 void Shell_sort(int *arr, int len); // #2 希尔排序 void Bubble_sort(int *arr, int len); // #3 冒泡排序 int Division(int *a, int left, int right); // 分隔过程(快速排序) void Quick_sort(int *arr, int left, int right, int count); // #4 快速排序(left和count初始值为0,right初始值为数组长度 - 1) void Select_sort(int *arr, int len); // #5 选择排序 void Heap_adjust(int arr[], int i, int len); // 构成堆过程 (堆排序) void Heap_sort(int arr[], int len); // #6 堆排序 void Merge(int arr[], int target[], int start, int mid, int end); // 归并过程(归并排序) void Merge_sort(int arr[], int target[], int start, int end, int count); // #7 归并排序(start和count初始值为0,end初始值为数组长度 - 1) void Print_sort_positive(int *arr, int len); // 正序输出 void Print_sort_negative(int *arr, int len); // 逆序输出 void Print_mob_com(); // 显示移动次数和比较次数 #endif
在实现排序前,先定义函数的功能模块,即sort.h。
头文件中定义了三个常量为ARRAY_LEN,MIN和MAX,代表数组的长度为1000,最大值为1000,最小值为1;建立伪随机函数Create_data()、复制数组函数Copy_array()和交换元素函数Swap_element(),这三个函数功能为初始化数组的元素;七种内部排序的函数与过程的定义,最后定义了数组的正序和逆序输出,以及打印排序过程中元素的移动次数和比较次数。
#include <stdio.h> #include <stdlib.h> #include <time.h> #include <math.h> #include <conio.h> #include <string.h> //#pragma warning (disable:4996) extern int Comparisons_num; // 比较次数 extern int Mobile_num; // 移动次数 // 建立伪随机数组 void Create_data(int *a, int n, int min, int max) { int flag; // 避免取重复取值 srand(time(NULL)); if (n > max - min + 1) return 0; for (int i = 0, j = 0; i<n; i++) { do { a[i] = (max - min + 1) * rand() / (RAND_MAX + 1) + 1; flag = 0; for (j = 0; j < i; j++) { if (a[i] == a[j]) flag = 1; } } while (flag); } } // 复制数组 void Copy_array(int *tar, int *arr, int len) { int i; for (i = 0; i < len; i++) tar[i] = arr[i]; } // 交换元素 void Swap_element(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; Mobile_num += 3; // 一次关键字交换计为3次移动 }
以上为文件sort.c中实现数组的初始化程序,函数Create_data()创建没有重复取值的数组,函数Copy_array()将数组arr全部内容复制到数组tar中,函数Swap_element()负责交换元素内容,每次调用移动次数Mobile_num将会增加三次。
// 直接插入排序 void Insert_sort(int *arr, int len) { int i, j; int tmp; // 待排序的元素 for (i = 0; i < len; i++) { tmp = arr[i]; for (j = i - 1; j >= 0 && tmp < arr[j]; j--) { Swap_element(arr + j, arr + j + 1); // tmp < arr[j],因此arr[j]向后移动 Comparisons_num++; } arr[j + 1] = tmp; } }
直接插入排序:按顺序每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。
直接插入排序是由两层嵌套循环组成的。外层循环标识并决定待比较的数值。内层循环为待比较数值确定其最终位置。直接插入排序是将待比较的数值与它的前一个数值进行比较,所以外层循环是从第二个数值开始的。当前一数值比待比较数值大的情况下继续循环比较,直到找到比待比较数值小的并将待比较数值置入其后一位置,结束该次循环。
插入排序的基本方法是:每步将一个待排序的记录按其关键字的大小插到前面已经排序的序列中的适当位置,直到全部记录插入完毕为止。
// 希尔排序 void Shell_sort(int *arr, int len) { int i, j; int d = len / 2; int lookouts; // 监视哨 while (d >= 1) { for (i = d; i < len; i++) { lookouts = arr[i]; for (j = i - d; j >= 0 && lookouts < arr[j]; j = j - d) { Swap_element(arr + j + d, arr + j); Comparisons_num++; } if (arr[j + d] != lookouts) { Swap_element(arr + j + d, &lookouts); Comparisons_num++; } } d /= 2; } }
希尔排序是基于插入排序的以下两点性质而提出改进方法的,算法如下:
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
先取一个len/2的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2=d1/2,重复上述的分组和排序,直至所取的增量 =1( < …<d2<d1),即所有记录放在同一组中进行直接插入排序为止。
// 冒泡排序 void Bubble_sort(int *arr, int len) { int i, j; int flag = 1; // 标记循环过程是否进行过交换,如果为1则进行了交换 for (i = 0; i < len && flag; i++) { flag = 0; for (j = 1; j < len - i; j++) { if (arr[j - 1] > arr[j]) { Swap_element(arr + j, arr + j - 1); flag = 1; } Comparisons_num++; } } }
冒泡排序的算法:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。(就好比大的气泡浮出了水面)
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
// 分隔(快速排序) int Division(int *a, int left, int right) { int base = a[left]; while (left < right) { while (left < right && base < a[right]) { right--; Comparisons_num++; } a[left] = a[right]; Mobile_num++; while (left < right && a[left] < base) { left++; Comparisons_num++; } a[right] = a[left]; Mobile_num++; } a[left] = base; return left; } // 快速排序 // left 和 count初始值为0 right 初始值为数组长度 - 1 void Quick_sort(int *arr, int left, int right, int count) { int i; int count_temp = count + 1; if (left < right) { i = Division(arr, left, right); Quick_sort(arr, left, i - 1, count_temp); Quick_sort(arr, i + 1, right, count_temp); } }
快速排序的算法:
- 设置两个变量left和right,排序开始的时候:left = 0, right = len - 1;
- 每次分割过程中,以第一个数组元素作为关键数据,赋值给base,即base = a[left];
- 从right开始向前搜索,即由后开始向前搜索(right--),找到第一个小于base的值a[right],将A[left]和A[right]互换;
- 接着从left开始向后搜索,即由前开始向后搜索(left++),找到第一个大于base的a[left],将A[left]和A[right]互换;
- 重复第3、4步,直到left >= right; (3,4步中,没找到符合条件的值,即3中a[right]不小于base,4中a[left]不大于base的时候改变left和right的值,使得right--,left++,直至找到为止。找到符合条件的值,进行交换的时候left和right指针位置不变。另外,left >= right这一过程一定正好是left++或right--完成的时候,此时令循环结束)。
// 选择排序 void Select_sort(int *arr, int len) { int i, j; int tmp; // 记录待排序元素的下标 for (i = 0; i < len - 1; i++) { tmp = i; for (j = i + 1; j < len; j++) { if (arr[tmp] > arr[j]) tmp = j; Comparisons_num++; } if (tmp != i) Swap_element(arr + tmp, arr + i); } }
直接选择排序的算法:
程序采用双层嵌套循环,外循环按顺序每次选择一个待排序的元素,内循环每次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
// 构成堆 (堆排序) void Heap_adjust(int arr[], int parent, int len) { int child; int temp; for (temp = arr[parent]; 2 * parent + 1 < len; parent = child) { child = 2 * parent + 1; if (child < len - 1 && arr[child + 1] > arr[child]) { child++; Comparisons_num++; } Comparisons_num++; if (temp < arr[child]) { Swap_element (arr + child, arr + parent); } else break; } } // 堆排序 void Heap_sort(int arr[], int len) { int i; for (i = (len - 1) / 2; i >= 0; i--) Heap_adjust(arr, i, len); for (i = len - 1; i > 0; i--) { Swap_element(arr, arr + i); // 每次将最大的数排在最后 Heap_adjust(arr, 0, i); // 重新构成堆,将最大的数放在第一位 } }
堆分为大根堆和小根堆,是完全二叉树。
堆排序的算法:
- 建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,此处len是堆中元素的个数。建堆的过程是线性的过程,从len/2到0处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
- 调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,那边交互节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
- 堆排序:堆排序是利用上面的1、2两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlgn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是O(nlgn)
// 归并 (归并排序) void Merge(int arr[], int target[], int start, int mid, int end) { int i, j, k; for (i = mid + 1, j = start; start <= mid && i <= end; j++) { if (arr[start] < arr[i]) target[j] = arr[start++]; else target[j] = arr[i++]; Mobile_num++; Comparisons_num++; } if (start <= mid) { for (k = 0; k <= mid - start; k++) { target[j + k] = arr[start + k]; Mobile_num++; } } if (i <= end) { for (k = 0; k <= end - i; k++) { target[j + k] = arr[i + k]; Mobile_num++; } } } // 归并排序 // start 和 count初始值为0 end 初始值为数组长度 - 1 void Merge_sort(int arr[], int target[], int start, int end, int count) { int mid; int count_temp = count + 1; int * temp_arr = (int *)calloc(end + 1, sizeof(int)); if (start == end) { target[start] = arr[start]; Mobile_num++; } else { mid = (start + end) / 2; Merge_sort(arr, temp_arr, start, mid, count_temp); Merge_sort(arr, temp_arr, mid + 1, end, count_temp); Merge(temp_arr, target, start, mid, end, count_temp); } if (count == 0) { free(temp_arr); } }
归并排序采用了分治法,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
归并排序的算法通常用递归实现,先把待排序区间[s, e]以中点二分,接着把左边子区间排序,再把右边子区间排序,最后把左区间和右区间用一次归并操作合并成有序的区间[s, e]。
其算法如下:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤3直到某一指针超出序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
// 显示打印 // 正序输出 void Print_sort_positive(int *arr, int len) { int i; for (i = 0; i < len; i++) { if (i % 10 == 0 && i != 0) putchar(‘\n‘); printf("%3d ", arr[i]); } putchar(‘\n‘); } // 逆序输出 void Print_sort_negative(int *arr, int len) { int i; for (i = 0; i < len; i++) { if (i % 10 == 0 && i != 0) putchar(‘\n‘); printf("%3d ", arr[len - i - 1]); } putchar(‘\n‘); } // 显示移动次数和比较次数 void Print_mob_com() { printf("移动次数:%d\n", Mobile_num); printf("比较次数:%d\n\n", Comparisons_num); // 初始化 Mobile_num = Comparisons_num = 0; }
最后是实现打印信息的函数:正序输出函数Print_sort_positive()、逆序输出函数Print_sort_negative()和显示移动次数和比较次数函数Print_mob_com()。
#include "Sort.h" #include <stdio.h> int main(void) { int arr[ARRAY_LEN]; int temp_arr[ARRAY_LEN]; int target_arr[ARRAY_LEN]; Create_data(arr, ARRAY_LEN, MIN, MAX); Copy_array(target_arr, arr, ARRAY_LEN); printf("排序前: \n"); Print_sort_positive(target_arr, ARRAY_LEN); Bubble_sort(target_arr, ARRAY_LEN); printf("\n完全正序: \n"); Print_sort_positive(target_arr, ARRAY_LEN); printf("\n完全逆序: \n"); Print_sort_negative(target_arr, ARRAY_LEN); // 开始进行七种排序比较 Copy_array(target_arr, arr, ARRAY_LEN); Bubble_sort(target_arr, ARRAY_LEN); printf ("冒泡排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Quick_sort(target_arr, 0, ARRAY_LEN - 1, 0); printf ("快速排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Copy_array(temp_arr, arr, ARRAY_LEN); Merge_sort(temp_arr, target_arr, 0, ARRAY_LEN - 1, 0); printf ("归并排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Heap_sort(target_arr, ARRAY_LEN); printf ("堆排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Insert_sort(target_arr, ARRAY_LEN); printf ("直接插入排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Select_sort(target_arr, ARRAY_LEN); rintf ("选择排序:\n"); Print_mob_com(); Copy_array(target_arr, arr, ARRAY_LEN); Shell_sort(target_arr, ARRAY_LEN); printf ("希尔排序:\n"); Print_mob_com(); return 0; }
编写测试程序的文件use_sort.c,并比较七种排序的结果,如下图:
其中移动和比较次数最多的排序方法为冒泡排序,而移动次数最少的则是快速排序,比较次数最少为希尔排序。。