实验目的:
在代码实践中,进一步了解十大内部排序算法及其应用场景,增强写代码的能力。
预期效果:
完成十大常用排序算法的代码编写,并使用一组数据进行实验。
实验思路:
排序算法均采用统一接口,模板为"int *算法名 (int *arr, int length);"。
测试数据为{3,7,12,4,6,2,11,1,5,8,10,7,13,9}。
实验内容:
基于比较的排序
一、冒泡排序
稳定性:✓
时间复杂度:平均O(n^2) 最好O(n) 最坏O(n^2)
是否需要额外空间:否
原理:从第一个位置开始,连续将相邻元素进行比较,大的元素排在小的元素后面,一轮结束后最后一个元素即为最大的元素,在排除最后一个元素后,重复以上步骤,直至完全有序(通过加入一个检查变量来避免不必要的循环)。
使用场景:待排数据规模较小,且有序程度高。
示例代码:
/* 冒泡排序 */
void bubbleSort(int *arr, int length) {
int ifChanged = 0;
for (int i=0; i<length; i++) {
for (int j=0; j<length-i-1;j++) {
if (arr[j]>arr[j+1]) {
swap(arr,j,j+1);
ifChanged = 1;
}
}
if (!ifChanged) return;
}
return;
}
二、插入排序
稳定性:✓
时间复杂度:平均O(n^2) 最好O(n) 最坏O(n^2)
是否需要额外空间:否
原理:(认为第一个元素已排好)从第二个元素开始,与已排序序列中的最后一个元素比较,若已排元素小于待排元素,则继续寻找下一个待排元素。若已排元素大于待排元素,则将已排元素向后移动一位,直到找到待排元素小于等于已排元素的位置或已排元素序列头位置,将待排元素插入至该位置,重复以上步骤,直至完全有序。
使用场景:待排数据规模较小,且有序程度高。
示例代码:
void insertionSort(int *arr, int length) {
for (int i=1; i<length; i++) {
int tmp = arr[i];
int j=i;
for(;(j>0)&&(arr[j-1]>tmp); j--) {
arr[j] = arr[j-1];
}
arr[j] = tmp;
}
}
三、希尔排序
稳定性:×
时间复杂度:平均O(nlogn),具体时间复杂度所选增量序列影响
是否需要额外空间:否
原理:(优化版插入排序)以一定的增量序列作为间隔,进行插入排序,使待排序列一步步接近完全有序,最后进行一次直接插入排序,达到完全有序状态。
使用场景:待排数据规模较大,且有序程度不高。
示例代码:
/* 希尔排序 - 使用{1,3,5,7}增量序列 */
void shellSort(int *arr, int length) {
int increasement[4] = {7,5,3,1};
int increaseIndex = 0;
for (int i=increasement[increaseIndex]; increaseIndex<4; i=increasement[++increaseIndex]) {
for (int j=i; j<length; j++) {
int tmp = arr[j];
int k=j;
for (;k>=i&&arr[k-i]>tmp; k-=i) {
arr[k] = arr[k-i];
}
arr[k] = tmp;
}
}
}
四、选择排序
稳定性:×
时间复杂度:平均O(n^2) 最好O(n^2) 最坏O(n^2)
是否需要额外空间:需要一个待排元素的辅助空间以临时存储当前最小值
原理:从待排序列第一个元素开始,每次遍历找出最小值,并将其排除在外 ,在剩下待排序列中继续找最小值,直至遍历至最后一个元素。
使用场景:待排数据规模较小,且有序程度不高。
示例代码:
/* 选择排序 */
void selectionSort(int *arr, int length) {
for (int i=0; i<length; i++) {
int min = i;
for (int j=i;j<length;j++) {
if(arr[j]<arr[min]) {
min = j;
}
}
swap(arr,i,min);
}
}
五、堆排序
稳定性:×
时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(nlogn) (参考堆排序时间复杂度定理,此处的nlogn比真正的nlogn要小一些)
是否需要额外空间:需要临时空间存储比较中的两个变量。
原理:(优化版选择排序)将待排序列建为大顶堆,使顶部元素与最后一个元素交换位置,然后将最后一个元素排除在外,重复上述操作,直至遍历至第二个元素。
使用场景:待排数据规模较大。
示例代码:
/* 堆排序 */
void _percDown(int *arr, int root, int n) {
// arr==待排数组,root==需要调整的根节点,n==需要调整为最大堆的长度
int P,maxC;
int rootValue = arr[root];
for (P=root;P*2+1<n;P=maxC) {
maxC = P*2+1;
if ((maxC!=n-1)&&(arr[maxC]<arr[maxC+1])) {
maxC++;
}
if (rootValue>=arr[maxC]) {
break;
} else {
swap(arr,P,maxC);
}
}
arr[P] = rootValue;
}
void heapSort(int *arr, int length) {
for (int i=length/2-1; i>=0; i--) {
_percDown(arr,i,length);
}
for (int i=length-1; i>0; i--) {
swap(arr,0,i);
_percDown(arr,0,i);
}
}
六、归并排序
稳定性:✓
时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(nlogn)
是否需要额外空间:需要与待排序列大小相同的辅助序列空间
原理:将待排序列从中间分为两个子序列,对两个子序列分别进行归并排序,最后将排好序的两个子序列合并(按顺序比较并排列)为最终有序的序列。
使用场景:待排数据规模较大,空间较为富裕。
示例代码:
/* 归并排序 */
void _mSort(int *arr, int start, int end, int *tmp) {
int center;
if (start<end) {
center = (start+end)/2;
_mSort(arr, start, center, tmp);
_mSort(arr, center+1, end, tmp);
int currentIndex = start;
int L = start;
int R = center+1;
while (L<=center&&R<=end) {
if (arr[L]<=arr[R]) {
tmp[currentIndex++] = arr[L++];
} else {
tmp[currentIndex++] = arr[R++];
}
}
while (L<=center) {
tmp[currentIndex++] = arr[L++];
}
while (R<=end) {
tmp[currentIndex++] = arr[R++];
}
for (int i=start;i<=end;i++) {
arr[i] = tmp[i];
}
}
}
void mergeSort(int *arr, int length) {
int *tmp;
tmp = (int *)malloc(length*sizeof(int));
if (tmp != NULL) {
_mSort(arr,0,length-1,tmp);
free(tmp);
}
else printf("Error! There is not enough space.");
}
七、快速排序
稳定性:×
时间复杂度:平均O(nlogn) 最好O(nlogn) 最坏O(n^2)
是否需要额外空间:需要空间存储轴元素key,各个部分均有一个轴。
原理:挑选出轴元素key,将比key小的放在其左边,比key大的放在其右边。之后再对key左右两边的序列进行快速排序,最终得到有序的序列。
使用场景:待排数据规模足够大,需要较快的执行速度(需要数据达到一定规模,才具有速度优势,否则与简单排序相比速度甚至存在劣势),可以通过与简单排序混搭使用实现效率最大化。
示例代码:
/* 快速排序 */
void _qSort(int *arr, int left, int right) {
if (left<right) {
int center = (left+right)/2;
if (arr[left]>arr[center]) {
swap(arr,left,center);
}
if (arr[left]>arr[right]) {
swap(arr,left,right);
}
if (arr[center]>arr[right]) {
swap(arr,center,right);
}
int key = arr[center];
swap(arr,center,right-1); // 将key藏至右边,比较只需考虑left+1~right-2
int low = left;
int high = right-1;
while (1) {
while (arr[++low]<key);
while (arr[--high]>key);
if (low<high) swap(arr,low,high);
else break;
}
if (right-left!=1) { // 防止当待排序列只有2个数字时,已排好的序列被交换
swap(arr,low,right-1); // 将key还回正确位置
}
_qSort(arr,left,low-1);
_qSort(arr,low+1,right);
}
}
void quickSort(int *arr, int length) {
_qSort(arr,0,length-1);
}
不基于比较的排序
八、计数排序
稳定性:✓/×(由代码结构决定)
时间复杂度:平均O(n+k) 最好O(n+k) 最坏O(n+k)
是否需要额外空间:需要额外空间,但需要空间大小由代码结构与数据规模决定
原理:新建一个计数序列,遍历待排序列,每碰到待排序列的一个元素的值,就使计数序列对应下标的数加一。遍历完毕后,计数序列中的每一个值,代表了待排序列中元素出现的次数,且有顺序。最后根据计数序列进行排序即可。
使用场景:待排数据规模不算大,对执行速度有一定要求。
注意:一般的计数排序只能对正整数进行排序。
示例代码:
/* 计数排序 (只能进行正整数的排序) */
#define MAXSIZE_FOR_COUNTINGSORT 100
void countingSort_instable(int *arr, int length) {
int *countArr = (int *)malloc(MAXSIZE_FOR_COUNTINGSORT*sizeof(int));
for (int i=0; i<MAXSIZE_FOR_COUNTINGSORT; i++) {
countArr[i] = 0;
}
for (int j=0; j<length; j++) {
countArr[arr[j]]++;
}
int currentIndex = 0;
for (int k=0; k<length; k++) {
while (countArr[k]-- > 0) {
arr[currentIndex++] = k;
}
}
free(countArr);
}
void countingSort_stable(int *arr, int length) {
int *countArr = (int *)malloc(MAXSIZE_FOR_COUNTINGSORT*sizeof(int));
int *sortArr = (int *)malloc((length+1)*sizeof(int));
for (int i=0; i<MAXSIZE_FOR_COUNTINGSORT; i++) {
countArr[i] = 0;
}
for (int j=0; j<length; j++) {
countArr[arr[j]]++;
}
for (int k=1; k<MAXSIZE_FOR_COUNTINGSORT; k++) {
countArr[k] += countArr[k-1];
}
for (int l=length-1; l>=0; l--) {
sortArr[--countArr[arr[l]]] = arr[l];
}
for (int m=0; m<length; m++) {
arr[m] = sortArr[m];
}
free(countArr);
free(sortArr);
}
九、桶排序
稳定性:✓
时间复杂度:平均O(n+k) 最好O(n+k) 最坏O(n^2)
是否需要额外空间:需要额外空间,但需要空间大小由映射关系与数据规模决定
原理:新建一个定量数组(桶),根据一定的映射关系,把数据尽量均匀的放入对应的桶中,对每个不是空的桶进行排序,将排好的数据拼接。(示例代码将使用元素与桶的下标对应的方式将数据放入桶中)
使用场景:待排数据规模不算大,对执行速度有一定要求。
示例代码:
/* 桶排序 */
#define MAXSIZE_FOR_BUCKET 100
void bucketSort(int *arr, int length) {
int *bucket = (int *)malloc(MAXSIZE_FOR_BUCKET*sizeof(int));
for (int i=0; i<MAXSIZE_FOR_BUCKET; i++) {
bucket[i] = 0;
}
for (int j=0; j<length; j++) {
bucket[arr[j]]++;
}
int currentIndex = 0;
for(int k=0; k<MAXSIZE_FOR_BUCKET; k++) {
for (;bucket[k]!=0; bucket[k]--) {
arr[currentIndex++] = k;
}
}
free(bucket);
}
十、基数排序
稳定性:✓
时间复杂度:平均O(nk) 最好O(nk) 最坏O(nk)
是否需要额外空间:需要额外空间,但需要空间大小由映射关系,数据规模与各个位的优先级决定。
原理:这里拿数字比较举例。首先取得待排序列最多要考虑的位数,建一定数量的桶,然后按照低优先级位排序,收集,再循环利用桶按高优先级位排序,再收集,直至最高优先级。
使用场景:待排数据规模不算大,对执行速度有一定要求。
示例代码:
/* 基数排序 */
#define MAXDIGIT_FOR_RADIXSORT 2
#define RADIX_FOR_RADIXSORT 10
typedef struct Element *ptrToElement;
struct Element {
int key;
ptrToElement next;
};
typedef struct BucketHead Bucket;
struct BucketHead {
ptrToElement head;
ptrToElement tail;
};
void radixSort(int *arr, int length) {
Bucket bucket[RADIX_FOR_RADIXSORT];
for (int i=0; i<RADIX_FOR_RADIXSORT; i++) {
bucket[i].head = bucket[i].tail = NULL;
}
ptrToElement tempNode, tempList = NULL;
for (int j=0; j<=length-1; j++) {
tempNode = (ptrToElement)malloc(sizeof(struct Element));
tempNode->key = arr[j];
tempNode->next = tempList;
tempList = tempNode;
}
for (int k=1; k<=MAXDIGIT_FOR_RADIXSORT; k++) {
int di;
while (tempList) {
int tmp = tempList->key;
for (int l=1; l<=k; l++) {
di = tmp % RADIX_FOR_RADIXSORT;
tmp /= RADIX_FOR_RADIXSORT;
}
tempNode = tempList;
tempList = tempList->next;
tempNode->next = NULL;
if (bucket[di].head == NULL) {
bucket[di].head = bucket[di].tail = tempNode;
}
else {
bucket[di].tail->next = tempNode;
bucket[di].tail = tempNode;
}
}
tempList = NULL;
for (di=RADIX_FOR_RADIXSORT-1; di>=0; di--) {
if (bucket[di].head) {
bucket[di].tail->next = tempList;
tempList = bucket[di].head;
bucket[di].head = bucket[di].tail = NULL;
}
}
}
for (int m=0; m<length; m++) {
tempNode = tempList;
arr[m] = tempNode->key;
tempList = tempList->next;
free(tempNode);
}
}
测试结果:
以上代码均通过自行测试,由于测试量比较大,就不逐一展示。