算法的泛化问题,这些坑你可能都经历过!|周立功教你学软件设计

电子说

1.3w人已加入

描述

第一章为程序设计基础,本文为1.6.3泛型编程。

 

>>>> 1.      泛型编程

 

下面将进一步以一个简单的循环查找为例,全面考察算法的泛化问题。假设要编写一个findValue()函数,在array数组中寻找一个特定的int值,详见程序清单 1.29。

 

程序清单 1.29 findValue()查找函数(1)

1     int *findValue(int *arrayHead, int arraySize, int value)

2     { 

3            for(int i =0; i < arraySize; ++i) 

4                   if(arrayHead[i] == value) 

5                          break; 

6            return &(arrayHead[i]);

7     }

 

该函数在某个范围内查找value,返回的是一个指针,指向它所找到的第一个符合条件的元素。如果没有找到,则返回最后一个元素的下一个位置(地址)。“最后元素的下一个位置”称为end,其作用是返回end表示“查找无结果”,为何不返回null呢?因为end指针可以对其它种类的数据结构带来泛化的效果,这是null做不到的。

 

在学习数组时,我们就被告诫,千万不要超越其下标范围,但事实上一个指向array元素的指针,不但可以合法地指向array的任何位置,也可以指向array尾端以外的任何位置。只不过,当指针指向array尾端以外的位置时,它只能用于与其它array指针相比较,不能间接引用其值。findValue()函数的使用方式如下:

const int arraySize = 7; 

int array[arraySize] = {0, 1, 2, 3, 4, 5, 6}; 

int *end = array + arraySize; 

 

int *ip = findValue(array, sizeof(array) / sizeof(int), 4); 

if(ip == end) 

       return false; 

else

       return true; 

 

显然,findValue()函数暴露了数组的实现细节,比如,arraySize,太过于依赖特定的数据结构。那么,如何设计一个算法,使它适用于大多数数据结构呢?或者说,如何在即将处理的未知的数据结构上,正确地实现所有的操作呢?事实上,一个序列有开始和结尾,既可以使用++得到下一个元素,也可以使用“*”得到当前元素的值。

 

显然,让阅读代码的人理解你的本意,至关重要是取一个不会让人产生误解的名字。对于包含范围,常用first和last。对于包含/排序范围,常用begin和end。比如,对于大多数需要分片的数组,使用begin和end表示包含/排除范围是最好的选择。遗憾的是,类似limit、filter和length这样具有多义性的英文单词会带来一定的困惑,而定义一个值的上限或下限时,max_和min_就是很好的前缀。

 

为了让findValue()函数适用于所有类型的数据结构,其操作应该更抽象化,让findValue()函数接受两个指针作为参数,表示一个操作范围,详见程序清单 1.30。

 

程序清单 1.30 findValue()查找函数(2)

1     int *findValue(int *begin, int *end, int value)

2     { 

3            while(begin != end && *begin != value) 

4                   ++begin;

5            return begin; 

6     }

 

由于findValue函数的返回值begin是一个指针,因此该函数是一个返回指针的函数,即指针函数。这个函数在“前闭后开”范围[begin, end)内(包含了begin迭代器的当前元素,而到end迭代器的前一个元素为止)查找value,并返回一个指针,指向它所找到的第一个符合条件的元素,如果没有找到就返回end。这里之所以用“!=”,而不是用“<”判断是否到达数组的尾部,因为这样处理更精确。findValue()函数的使用方式如下:

const int arraySize = 7; 

int array[arraySize] = {0, 1, 2, 3, 4, 5, 6}; 

int *end = array + arraySize;

 

int *ip = findValue(array, end, 4);

if(ip == end) 

       return false; 

else 

       return true; 

 

当然,findValue()函数也可以方便地用于查找array的子范围:

int *ip = findValue(array + 2, array + 5, 3); 

if(ip == end) 

       return false; 

else

       return ture; 

 

由此可见,findValue()函数中并无任何操作是针对特定的整数array的,即只要将操作对象的类型加以抽象化,且将操作对象的表示法和范围目标的移动行为抽象化,则整个算法就可以工作在同一个抽象层面上了。通常将整个算法的过程称为算法的泛型化,简称泛化。泛化的目的旨在使用同一个findValue()函数处理各种数据结构,通过抽象创建可重用代码。

 

如果一个序列是有序的,则不需要用finValue()从开始位置查找,可以使用标准C提供的bsearch()二分查找算法。对于一个更长的序列,二分查找也比findValue()线性查找法的速度更快。即使序列中只有10个元素,也足以体现二分查找的比较优势。对于一个有1000个元素的序列,最多需要进行10次比较,其查找的速度要快200倍。

 

显然,求数组中元素的最大值,其最好的方法是通过传递2个指针指定元素范围。一个指针标识数组的开头,另一个指针标识数组的尾部。比如:

int iMax(const int *begin, const int *end);

 

显然,如果只是传递指针,数据就有被修改的可能。如果不希望数据被修改,就要传递指向整数常量的指针。使用for循环的示例如下:

for(ptr = begin; ptr != end; ptr++)

       total = total +*ptr; 

 

将ptr设置为待处理的第一个元素(begin指向的元素)的指针,并将*ptr(元素的值)加入到total中。然后循环通过递增操作来更新ptr,使之指向下一个元素。只要ptr不等于end,这一过程将继续下去。当ptr等于end时,它将指向范围中的最后一个元素后面的位置,此时循环结束。其次,请注意不同的函数调用是如何指定数组中不同的范围的。比如:

int array[] = {39, 33, 18, 64, 73, 30, 49, 51, 81}; 

int n = sizeof(array) / sizeof(array[0]); 

int *past = array + n;

 

int max = iMax(array, array + n); 

int max = iMax(array, array + 3); 

int max = iMax(array +3, array + 8); 

 

指针array+n指向最后一个元素后面的一个位置(数组只有n个元素,最后一个元素的地址为array+n-1),因此范围[array,array+n]指定的是整个数组。同样array,array+3指定了前3个元素,依此类推。注意,根据指针减法规则,表达式end–begin是一个整数值,等于数组的元素个数。

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分