实验目的
通过尝试多种方法,对基本的快速排序算法进行优化,并体会排序算法的多样性与可操作性。
实验原理
用不同算法对指定记录数的文件进行排序,经过一定数量的重复实验,取得每种算法的平均运行时长,进而比较各种算法的优劣。
一、步骤
1、从数列中挑出一个元素,称为"基准"(pivot)。
2、重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
3、递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
二、复杂度
快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。平均需要O(log2n)趟,平均算法复杂度为O(nlogn),但最坏的情况下会变成O(n^2)。
最坏情况就是每次将一组数据划分为两组的时候,分界线都选在了边界上,使得划分了和没划分一样,最后就变成了普通的选择排序了。
实验内容
一、简单快速排序最坏情况(排列好的数列)
(一)完整代码
/* C implementation QuickSort */
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
/* Function to print an array */
void printArray(int arr[], int size)
{
int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
int partition(int arr[], int low, int high)
{
int pivot = arr[low]; // pivot, 中心值
int i = low;
int j = high;
while (i < j)
{
while (i < j && arr[j] > pivot)
j--;
if (i < j)
{
arr[i] = arr[j];
i++;
}
while (i < j && arr[i] <= pivot)
i++;
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
arr[i] = pivot;
return i;
}
/* The main function that implements QuickSort
arr[] --> Array to be sorted,
low --> Starting index,
high --> Ending index */
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
/* pi is partitioning index, arr[p] is now
at right place */
int pi = partition(arr, low, high);
// Separately sort elements before
// partition and after partition
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
// Driver program to test above functions
int arr[500000];//如果在main()内部声明,可能会因空间不足而无输出。
int main()
{
clock_t start, end;
srand(10086);
for (int j = 0; j < 500000; j++)
arr[j] = j;//rand() % 500000;
int n = 500000;
start = clock();
quickSort(arr, 0, n - 1);
end = clock();
//printf("Sorted array: \n");
//printArray(arr[1], n);
printf("time=%f\n", (double)(end - start) / (CLOCKS_PER_SEC * 3));
return 0;
}
(二)数据记录
无结果,每次都超时而退出。验证了我们之前的猜想。
二、简单快速排序(随机排列的数列)
(一)对代码一的修改如下
int main()
{
clock_t start, end;
srand(10086);
for (int j = 0; j < 500000; j++)
arr[j] = rand() % 500000;//原来是有序的数列,现在是随机排列的
int n = 500000;
start = clock();
quickSort(arr, 0, n - 1);
end = clock();
printf("time=%f\n", (double)(end - start) / (CLOCKS_PER_SEC));
return 0;
}
(二)数据记录
次序 | 耗时(s) |
---|---|
1 | 0.084999 |
2 | 0.090999 |
3 | 0.090000 |
4 | 0.089001 |
5 | 0.087999 |
平均 | 0.088600 |
三、中间值均分(排列好的数列)
(一)对代码一的修改如下
int partition(int arr[], int low, int high)
{
int pivot = arr[(low+high)/2]; // 将中心值放在已排列好的数列中间而不是开头,起到左右均分的作用
int i = low;
int j = high;
while (i < j)
{
while (i < j && arr[j] > pivot)
j--;
if (i < j)
{
arr[i] = arr[j];
i++;
}
while (i < j && arr[i] <= pivot)
i++;
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
arr[i] = pivot;
return i;
}
(二)数据记录
次序 | 耗时(s) |
---|---|
1 | 0.086000 |
2 | 0.096000 |
3 | 0.100000 |
4 | 0.095000 |
5 | 0.100000 |
平均 | 0.095400 |
四、中间值均分(随机排列的数列)
数据记录
次序 | 耗时(s) |
---|---|
1 | 0.084000 |
2 | 0.079000 |
3 | 0.100000 |
4 | 0.091000 |
5 | 0.094000 |
平均 | 0.089600 |
虽然相比简单快速排序法的0.088600s无明显变化,但由代码三可知该算法能胜任“最坏情况”,且时长较为稳定。看来如果我们恰当地(尽可能让中间值的大小在数列中处在中间位置)选择中间值确实可以大大减少时间开销。
五、三数取中(排列好的数列)
(一)算法简介
1.枢纽值的选取与移动
在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。
2.根据枢纽值进行扫描、分割
3.递归
重复之前的操作,直至每一部分都不需要再处理。
(二)完整代码
/* C implementation QuickSort */
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
/* Function to print an array */
void printArray(int arr[], int size)
{
int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swap1(int arr[], int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
void median3(int A[], int left, int right)
{
int center = (left + right) / 2;
if (A[left] > A[center])
swap(&(A[left]), &(A[center]));
if (A[left] > A[right])
swap(&(A[left]), &(A[right]));
if (A[center] > A[right])
swap(&(A[center]), &(A[right]));
swap(&(A[center]), &(A[right - 1]));
}
/* The main function that implements QuickSort
arr[] --> Array to be sorted,
low --> Starting index,
high --> Ending index */
void quickSort(int arr[], int low, int high)
{
if (low < high)
{
/* pi is partitioning index, arr[p] is now
at right place */
median3(arr, low, high);
int pivot = high - 1;
int i = low;
int j = high - 1;
while (1)
{
while (arr[++i] < arr[pivot]) {}
while (j > low && arr[--j] > arr[pivot]) {}
if (i < j)
{
swap1(arr, i, j);
}
else
{
break;
}
}
if (i < high)
{
swap1(arr, i, high - 1);
}
quickSort(arr, low, i - 1);
quickSort(arr, i + 1, high);
}
}
// Driver program to test above functions
int arr[500000];
int main()
{
clock_t start, end;
srand(10086);
for (int j = 0; j < 500000; j++)
arr[j] = j;//rand() % 500000;
int n = 500000;
start = clock();
quickSort(arr, 0, n - 1);
end = clock();
//printf("Sorted array: \n");
//printArray(arr[1], n);
printf("time=%f\n", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}
(三)数据记录
次序 | 耗时(s) |
---|---|
1 | 0.050000 |
2 | 0.046000 |
3 | 0.044000 |
4 | 0.046000 |
5 | 0.052000 |
平均 | 0.047600 |
相较代码三的0.095400s有了较大的提升。尽管平均时间复杂度也为O(nlogn)级,但我们通过三数取中法选择了一个枢纽值,使得一部分记录的关键字均小于另一部分。然后分别对这两组继续进行排序,以使整个序列有序。这样可以很大程度上避免分组"一边倒"的情况。(也就是代码二)
六、三数取中(随机排列的数列)
数据记录
次序 | 耗时(s) |
---|---|
1 | 0.113000 |
2 | 0.117000 |
3 | 0.109000 |
4 | 0.108000 |
5 | 0.117000 |
平均 | 0.112800 |
三数取中法面对随机排列的数列时表现得没有中间值均分法优秀。但实际生活中大多数时候我们面对的都是基本排列好的文件,因此三数取中法更具优势。
七、三数取中+直接插入(排列好的数列)
(一)算法简介
根据我们之前学过的知识,直接插入算法在处理少量排好序的文件时具有较大的优势,而这恰恰可以与快速排序算法形成互补(快速排序算法不适于排好序的文件)。因此,我们可以在大多数元素都已经排好序的情况下使用直接插入排序法,从而加快速度。
(二)完整代码
/* C implementation QuickSort */
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
/* Function to print an array */
void printArray(int arr[], int size)
{
int i;
for (i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
void swap(int *a, int *b)
{
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swap1(int arr[], int a, int b)
{
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
void median3(int A[], int left, int right)
{
int center = (left + right) / 2;
if (A[left] > A[center])
swap(&(A[left]), &(A[center]));
if (A[left] > A[right])
swap(&(A[left]), &(A[right]));
if (A[center] > A[right])
swap(&(A[center]), &(A[right]));
swap(&(A[center]), &(A[right - 1]));
}
/* The main function that implements QuickSort
arr[] --> Array to be sorted,
low --> Starting index,
high --> Ending index */
void directSort(int arr[], int n) //直接插入排序
{
int i, j;
int temp;
for (i = 1; i < n; i++)
{
j = i;
temp = arr[i];
while (j > 0 && temp < arr[j - 1])
{
arr[j] = arr[j - 1];
j--;
}
arr[j] = temp;
}
}
void quickSort(int arr[], int low, int high)
{
if (low + 20< high) //剩余20个记录交给更快捷的直接插入排序法
{
/* pi is partitioning index, arr[p] is now
at right place */
median3(arr, low, high);
int pivot = high - 1;
int i = low;
int j = high - 1;
while (1)
{
while (arr[++i] < arr[pivot]) {}
while (j > low && arr[--j] > arr[pivot]) {}
if (i < j)
{
swap1(arr, i, j);
}
else
{
break;
}
}
if (i < high)
{
swap1(arr, i, high - 1);
}
quickSort(arr, low, i - 1);
quickSort(arr, i + 1, high);
}
}
// Driver program to test above functions
int arr[500000];
int main()
{
clock_t start, end;
srand(10086);
for (int j = 0; j < 500000; j++)
arr[j] = j;//rand() % 500000;
int n = 500000;
start = clock();
quickSort(arr, 0, n - 1); //先使用快速排序
directSort(arr + 250000 - 1, 41); //再使用直接插入排序
end = clock();
//printf("Sorted array: \n");
//printArray(arr[1], n);
printf("time=%f\n", (double)(end - start) / CLOCKS_PER_SEC);
return 0;
}
(三)数据记录
次序 | 耗时(s) |
---|---|
1 | 0.028000 |
2 | 0.021000 |
3 | 0.020000 |
4 | 0.021000 |
5 | 0.018000 |
平均 | 0.021600 |
相较代码五的0.047600s有了较大的提升。
八、三数取中+直接插入(随机排列的数列)
数据记录
次序 | 耗时(s) |
---|---|
1 | 0.090000 |
2 | 0.084000 |
3 | 0.096000 |
4 | 0.094000 |
5 | 0.093000 |
平均 | 0.091400 |
相较代码六的0.112800s有了一定的提升。
数据处理
排列好的 | 随机排列 | |
---|---|---|
简单快排 | 超时 | 0.0886 |
中间值均分 | 0.0954 | 0.0896 |
三数取中 | 0.0476 | 0.1128 |
三数取中+直接插入 | 0.0216 | 0.0914 |
综上,“三数取中+直接插入”面对已排列好的数列时有着巨大优势,更适用于实际情况。简单快排更适合随机排列。
总结与反思
要拓展思维,敢于探索,在实践中不断优化算法。此外,还要结合实际情况进行充分考虑,使得算法更符合一般情况。最后,还要具有搜集、整理资料的能力,在实践中增长知识。