实验一:二分搜索与快速排序
- 问题描述
(1)二分搜索算法
二分搜索(英语:binary search),也称折半搜索(英语:half-interval search)、对数搜索(英语:logarithmic search),是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。
(2)快速排序算法
快速排序(Quicksort)是对冒泡排序的一种改进。
快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
- 实验目的
(1)熟悉分治算法,并学以致用
(2)熟练掌握二分搜索法和快速排序算法
- 实验原理
分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
(1)二分搜索算法
二分搜索法是利用分治策略的典型例子。它充分利用了元素之间的次序关系(也正因此,二分搜索的元素必须是有序数组)采用分治策略。
分治策略:基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题互相独立且与原来的问题相同。递归地解这些子问题,然后将各子问题的解合并得到原来问题的解。
分治法的两个核心是子问题的划分依据和递归。首先是子问题的划分问题,不同问题有不同的划分方法,但一般用二分法,即将大问题划分为2个小问题;接下来是递归问题。分治法一般伴随着递归。反复调用同一个函数但传入不同规模的值最终不断逼近问题的解。
二分搜索算法的基本思想是:将n个元素分成个数大致相同的两半,取a[n/2]和x比较,若x=a[n/2],则找到x,算法终止。若x<a[n/2],则只在数组a的左半部继续搜索x(同理右半部)时间复杂度为O(log n),但值得一提的是,二分搜索算法所用的时间和被查找的元素紧密相关。因此就被查找的目标对时间的影响进行实验。
(2)快速排序算法
快速排序算法的步骤如下:
1. 从数列中挑出一个元素,称为 "基准"(pivot);
2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
顺便写一下冒泡排序的原理:冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
作为最简单的排序算法之一,冒泡排序给我的感觉就像 Abandon 在单词书里出现的感觉一样,每次都在第一页第一位,所以最熟悉。冒泡排序还有一种优化算法,就是立一个 flag,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。
- 实验设计
4.1 二分查找
输入:{1,2,3,4,5,6,7,8,9,10}
输出:被查找目标数字的索引(按照惯例首位索引为0)
为了测试查找步骤,为每一次递归调用额外打印输出。这可能会耗费一部分时间。实验不打算为这部分消耗掉的时间额外设置对比组。
不设置性能对比组,因此唯一的变量只有目标数字而没有别的。
核心代码如下:
int BinarySearch(int nums[], int target, int length)
{
int left = 0;
int right = length - 1;
while (left <= right)
{
int middle = (left + right) / 2;
if (target == nums[middle])
{
printf("middle = %d\n",middle);
printf("SUCCESS:Middle found.\n");
return middle;
}
else if (target > nums[middle])
{
printf("middle = %d\n",middle);
left = middle + 1;
printf("NEXT:left=middle+1.\n");
}
else
{
printf("middle = %d\n",middle);
right = middle - 1;
printf("NEXT:right=middle-1.\n");
}
}
return -1;
}
4.2 快速排序
快速排序的时间取决于待排序数组本身的性质。对于随机性较强的数组,其时间复杂度接近O(nlogn),但对于本来有序或者有序但反序的数组,其所用的时间将会不一样。因此对这三种情况进行分析。为了保证数据的有效性,数字个数多一点。
另外对不同个数的数字性能进行分析。
快速排序是对冒泡排序的改良,因此有必要将两者在排序相同的数列的性能进行对比。
一共3个函数:产生随机数的random函数,快排本身的quicksort函数,以及入口main函数。
实验的随机数由c语言的rand()产生,由srand()对时间产生种子确保每次循环的随机数都不一样。时间种子在必要的时候会取消掉保证数组的一致性。
为了测试查找步骤,为结果额外打印输出。这可能会耗费一部分时间。实验不打算为这部分消耗掉的时间额外设置对比组。
1. 三种数组
这个很好解决。有序与反序的数组可以用for循环产生,只不过一个是i一个是n-i。乱序就循环将随机数加入数组。
2. 多次性能
没什么好说的,改下n就可以。
3. 对比冒泡排序的性能
冒泡排序单独写一个函数。然后取消掉时间种子使两次编译出来的随机数组是一模一样的。快速排序的平均时间复杂度是O(nlogn),这也是最好的情况(本来就是正序),最坏的情况是O(n^2),也就是本来是反序;冒泡的平均时间复杂度是O(n^2),最好与最坏的情况和快速排序一样,分别是O(n)和O(n^2)。
- 实验结果与分析
5.1 二分查找
查找目标 | 查找时间(秒) | 查找次数 |
1 | 0.3 | 3 |
2 | 0.3 | 2 |
3 | 0.3 | 3 |
4 | 0.3 | 3 |
5 | 0.3 | 1 |
6 | 0.3 | 3 |
7 | 0.3 | 3 |
8 | 0.3 | 2 |
9 | 0.3 | 3 |
10 | 0.3 | 4 |
由于数据量太少,这样的查找是看不出时间的差别的。但查找次数基本符合预期。即二分查找所需要的时间与被查找的数字在数组中的位置有关。
我将数据增加到1000,100000依然是0.3秒左右的数值。下面是在100000个数中查找10的结果。将数据增加到10000000后出现code=3221225725内存溢出错误(其实是静态申请数组空间申请不了这么大的),只能停止测试。
5.2 快速排序
1. 首先是快速排序本身在3种类型的数组(1-100)下的平均时间:(有序,乱序,反序),值得一提的是,我的快速排序的基准值是第一个数,否则无法测试有序
数组类型 | 平均时间(秒) |
正序 | 0.3 |
乱序 | 0.3 |
反序 | 0.32 |
正序乱序时间差不多,反序时间长一点,基本符合预期。1000个数结果其实也差不多。
2. 不同个数的数据性能比较(随机数)
数据个数 | 时间(秒) |
10 | 0.310 |
100 | 0.311 |
1 000 | 0.311 |
10 000 | 0.315 |
100 000 | 0.350 |
附一张100000个随机数的排序打印截图:
3. 与冒泡排序的对比:
直接用100 000个数的数据进行对比:
这里说一下为什么不用正序和反序,因为冒泡排序和快速排序的问题是一样的,正序快,反序慢,没什么对比意义。
排序 | 时间(秒) |
冒泡 | 29.00 |
快排 | 0.35 |
(我一度怀疑冒泡排序的程序卡死了)
冒泡排序其中一次运行的截图:
事实上,冒泡排序在不同数据情况下时间也不太一样,这里贴出对比:
数据个数 | 冒泡 | 快排 |
100 | 0.32 | 0.31 |
1000 | 0.32 | 0.32 |
10000 | 0.57 | 0.32 |
100000 | 28.8 | 0.35 |
快排也在升高,但快排升高的速度远小于冒泡。这是因为冒泡排序的平均时间复杂度是O(n^2),而快速排序是O(nlogn)。
- 结论
二分查找的基本思想是将n个元素分成大致相等的两部分,取a[n/2]与x做比较,如果x=a[n/2],则找到x,算法中止;如果x<a[n/2],则只要在数组a的左半部分继续搜索x,如果x>a[n/2],则只要在数组a的右半部搜索x.
时间复杂度即是while循环的次数。
总共有n个元素,
渐渐跟下去就是n,n/2,n/4,....n/2^k(接下来操作元素的剩余个数),其中k就是循环的次数
由于n/2^k取整后>=1
即令n/2^k=1
可得k=log2n,(是以2为底,n的对数)
所以时间复杂度可以表示O(h)=O(log2n)
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
- 程序源码
7.1 二分搜索
#include <stdio.h>
int BinarySearch(int nums[], int target, int length)
{
int left = 0;
int right = length - 1;
while (left <= right)
{
int middle = (left + right) / 2;
if (target == nums[middle])
{
printf("middle = %d\n",middle);
printf("SUCCESS:Middle found.\n");
return middle;
}
else if (target > nums[middle])
{
printf("middle = %d\n",middle);
left = middle + 1;
printf("NEXT:left=middle+1.\n");
}
else
{
printf("middle = %d\n",middle);
right = middle - 1;
printf("NEXT:right=middle-1.\n");
}
}
return -1;
}
int main(void)
{
int special[10000000];
for(int i = 1;i<=10000000;i++){
special[i-1]=i;
}
int test[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int result = BinarySearch(special, 10, 10000000);
printf("result:%d\n", result);
return 0;
}
7.2 快速排序
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
int QuickSort(int nums[],int left,int right){
//@param: nums[]: numbers
//@param: left: the number of the [0]
if(left<right){
int i=left;
int j=right;
int temp_middle=nums[left];//standard number
while (i<j)
{
//from right to left,find a number smaller than standard number
while (i<j&&nums[j]>=temp_middle)
{
j--;
}
if(i<j)
{
nums[i]=nums[j];
i++;
}
while (i<j&&nums[i]<temp_middle)
{
i++;
}
if(i<j)
{
nums[j]=nums[i];
j--;
}
}
nums[i]=temp_middle;
QuickSort(nums,left,i-1);
QuickSort(nums,j+1,right);
}
}
void bubbleSort(int nums[],int len){
int i, j, temp;
for (i = 0; i < len - 1; i++)
for (j = 0; j < len - 1 - i; j++)
if (nums[j] > nums[j + 1]) {
temp = nums[j];
nums[j] = nums[j + 1];
nums[j + 1] = temp;
}
}
int random(int max,int min){
int origin = rand();
int random_num = origin%(max-min+1)+min;
return random_num;
}
int main(void){
srand((unsigned int)time(NULL));
int nums[100000];
for(int i=0;i<100000;i++){
//nums[999-i]=i+1;
int num = random(100000,1);
nums[i]=num;
//printf("%d,%d\n",i,num);
// sleep(1);
}
QuickSort(nums,0,99999);
//bubbleSort(nums,100000);
for(int i=0;i<100000;i++){
if(i%10==0)
printf("\n");
printf("%2d,",nums[i]);
}
}