C++抽象编程——算法分析(1)——选择排序

在递归中,我们介绍了函数fib(n)的两个不同的递归实现(参见:斐波那契数的分析与拓展),该函数计算第n个斐波那契数。第一个是直接基于数学定义:

事实证明这是非常低效的。第二个实现使用加法序列(additive sequences)的概念来产生效率与传统迭代方法相当的fib(n)版本,表明递归本身不是问题的原因。 即使如此,因为像斐波纳契函数的例子它具有如此高的执行成本,因此递归有时会得到不好的名声。
但是接下来你会在这个系列中看到的那样,关于问题的递归思考的能力通常会导致新的策略比任何迭代设计过程中出现的更为有效的策略。 分治算法(divide-and-conquer)的力量是巨大的,对实践中出现的许多问题产生深远的影响。 通过使用这种形式的递归算法,可以实现效率的显着提高,其可以减少解决问题的时间,但不是减少两倍或三倍,而是减少上千倍。
然而,在查看这些算法之前,问一些问题很重要。计算机术语在算法语境中意味着什么? 你如何衡量效率呢? 这些问题构成了被称为算法分析的计算机科学子领域的基础。 虽然对算法分析的详细了解需要具有数学和大量仔细思考的合理设施,但你可以通过调查几种简单算法的性能来了解工作原理。

排序问题

说明算法分析的最简单方法是考虑不同算法在性能上差别很大的问题。其中最令人感兴趣的是排序问题,其中包括对数组或vector中的元素进行重新排序,以便它们以一定的顺序排列。例如,假设你已经将以下整数存储在变量vec中,它是vector < int >:

您的任务是编写一个按照升序排列元素的函数sort(vec),如下所示:

选择排序(The selection sort algorithm)

有许多算法可以选择将升序的整数排序vector。最简单的一个叫做选择排序。给定大小为N的vector,选择排序算法遍历每个元素位置,并找到应该在排序向量中占据该位置的值。当找到适当的元素时,算法将其与先前占据所需位置的值进行交换,以确保没有元素丢失。因此,在第一个周期,算法找到最小的元素,并与第一个向量位置进行交换。 在第二个循环中,它找到剩余元素中的最小的元素并与第二个位置交换。此后,算法继续该策略,直到向量中的所有位置被正确排序。使用选择排序的排序实现如下所示:

#include <iostream>
#include <vector>
using namespace std;
void sort(vector<int> & vec);
int main(){
    vector<int> vec;
    for (int i = 0; i < 10; i++){
        int n;
        cin >> n;
        vec.push_back(n);
    }
    sort(vec);
    for(int k = 0; k < vec.size();k++){
        cout << vec[k] <<" ";
    }
    return 0;
}

/*
*该实现使用称为选择排序的算法,其可以描述如下。
*从左手(lh)边,依次指向vector中的每个元素,
*从下标0开始。在循环中的每个步骤中:
*1.找到你的左手和vector的最后一个范围内的最小元素,并用右手(rh)指向该元素。
*2. 通过交换左手和右手指示的元素将该元素移动到正确的位置
*/
void sort(vector<int> & vec){
    int n = vec.size();
    for(int lh = 0; lh < n; lh++){
        int rh = lh; //一开始的时候,左右手指向同一个元素,就是第一个元素 
        for(int i = lh + 1; i < n; i++){ //从左手边的剩下的元素开始遍历 
            if(vec[i] < vec[rh]) rh = i;/*如果找到比rh所指的值小,那么rh就指向
                                          它,直到rh指向剩余元素中最小的一个 */ 
        }
        /*最后实现lh与rh的交换,交换放在外循环,目的就是每当lh增加1,那么
         *就执行一次交换,直到lh执行最后一个元素,交换完成
         *也就意味着排序完成
         */ 
        int tmp = vec[lh];
        vec[lh] = vec[rh];
        vec[rh] = tmp;
    }
} 

运行结果:

我们再举个例子看看,假设你要求将下列的vector排序:

通过外部for循环的第一个循环将下标位置5中的19标识为整个向量中的最小值,然后将其与索引位置0中的56进行交换,以保留以下配置:

在第二个循环中,该算法在位置1和位置7之间找到最小的元素,其结果为位置1中的25。程序继续执行交换操作。在每个后续周期中,算法执行交换操作以将下一个最小值移动到其适当的最终位置。当for循环完成时,整个向量被排序完了。

性能的经验测量(Empirical measurements of performance)

选择排序算法作为排序策略的效率如何?为了回答这个问题,它有助于收集关于计算机对各种大小的vector进行排序所需的时间的经验数据。当我在笔记本电脑上进行了这个实验时,我观察了以下选择排序的时序数据,其中N表示vector中元素的数量:

对于10个整数的向量,选择排序算法在几微秒内完成其工作。即使是5000个整数,这种排序的执行不到一秒钟,这在我们的人类时间感上看起来肯定是够快的。然而,随着矢量大小越来越大,选择排序的表现开始下降。 对于10万个整数的向量,算法需要两分半钟以上。如果你坐在你的电脑前等待它的回复,那是一个非常长的时间。
更令人不安的是,随着向量大小的增加,选择排序的性能迅速变得更糟。从时间数据可以看出,每次将值的乘数乘以10时,对vecctor进行排序所需的时间会增加一百倍。如果这种模式继续下去,排序一百万个数字的列表将需要大约四个半小时。如果你的业务需要按此规模分类向量,你别无选择,只能找到更有效的方法.

分析选择排序的性能(Analyzing the performance of selection sort)

当排序的值的数量变大时,是什么使选择排序执行得如此糟糕?为了回答这个问题,我们思考算法在外循环的每个周期上必须做的事情。为了正确确定vector中的第一个值,选择排序算法在搜索最小值时必须考虑所有N个元素。因此,循环的第一个循环所需的时间大概与N成比例。对于向量中的每个其他元素,算法执行相同的基本步骤,但每次看一个元素。 它在第二个周期的N-1个元素,第三个N-2,等等,所以总的运行时间大致为这样的:

因为很难用这种扩展形式的表达式来处理,所以通过应用一些数学来简化它。正如我们在代数课程中学到的,前N个整数的总和由公式(表示要执行的操作数):

或者把N乘进去

至于公式的证明,可以用数学归纳法,以后再提。
如果我们按照上面的公式计算,可以得到下列的表格:

因为选择排序算法的运行时间可能与算法需要做的工作量有关,所以该表中的值应该与观察到的算法执行时间大致成正比,结果证明是正确的。如果你在下图中查看测量的选择排序顺序数据,例如,会发现该算法需要1.58秒来对10000个数字进行排序。 在那个时候,选择排序算法必须在其最内循环中执行50,005,000个操作。假设这两个值之间确实存在比例关系,将时间除以操作次数给出以下比例常数(实际就是每次操作所要花费的时间)的估计:

如果将相同的比例常数应用于表中的其他条目,则会发现公式:

用这个公式能大致算出大致会花费的时间,至少对于大的N值。使用该公式计算的观察时间和估计值出现在下图中,以及两者之间的相对误差。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值