实验一:二分搜索与快速排序

实验一:二分搜索与快速排序

 

 

  1. 问题描述

(1)二分搜索算法

二分搜索(英语:binary search),也称折半搜索(英语:half-interval search)、对数搜索(英语:logarithmic search),是一种在有序数组中查找某一特定元素的搜索算法。搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。这种搜索算法每一次比较都使搜索范围缩小一半。

(2)快速排序算法

快速排序(Quicksort)是对冒泡排序的一种改进。

快速排序由C. A. R. Hoare在1960年提出。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

  1. 实验目的

(1)熟悉分治算法,并学以致用

(2)熟练掌握二分搜索法和快速排序算法

  1. 实验原理

    分治算法的基本思想是将一个规模为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,当在一趟序列遍历中元素没有发生交换,则证明该序列已经有序。但这种改进对于提升性能来说并没有什么太大作用。

  1. 实验设计

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)。

 

  1. 实验结果与分析

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)。

 

  1. 结论

二分查找的基本思想是将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) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。

  1. 程序源码

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]);
    }
}

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值