周立功教授谈迭代器模式设计

电子说

1.3w人已加入

描述

近日周立功教授公开了数年的心血之作《程序设计与数据结构》,电子版已无偿性分享到电子工程师与高校群体下载,经周立功教授授权,特对本书内容进行连载。

>>>> 1.1 迭代器模式

>>> 1.1.1 迭代器与容器

在初始化数组中的元素时,通常使用下面这样的for循环语句遍历数组

int i, a[10];

for(i = 0; i < 10; i++) 

      a[i] = i;

这段代码中的循环变量i该变量的初始值为0然后递增为123...程序在每次i递增后都将值赋给a[i]。数组中保存了许多元素,通过指定数组下标,即可从中选择任意一个元素。for语句中的i++的作用是让i的值在每次循环后自增1,这样就可以访问数组中的下一个元素,从而实现了从头到尾逐一遍历数组元素的功能。

由此可见,常用的循环结构就是一种迭代操作,在每一次迭代操作中,对迭代器的修改即等价于修改循环控制的标志或计数器。而容器是一种保存值的集合的数据结构,C有两种内建的容器:数组和结构体。虽然C没有提供更多的容器,但用户可以按需编写自己的容器,比如,链表、哈希表等。

i的作用抽象化、通用化后形成的模式在设计模式中i称为迭代器(Iterator)模式,Iterate的字面意思是重复、反复声明其实就是重复做某件事,Iterator模式用于遍历数组中的元素。迭代器的基本思想是迭代器变量存储了容器的某个元素的位置,因此能够遍历该位置的元素。通过迭代器提供的方法,可以继续遍历容器的下一个元素。

显而易见,迭代器是一种抽象的设计概念,因为在程序设计语言中并没有直接对应于这个概念的实物。《设计模式》一书提供了23种设计模式的完整描述,其中iterator模式的定义为:在遍历一个容器对象时,提供一种方法顺序访问一个容器对象中的各个元素,而又不暴露该对象的内部表示方式。其中心思想是将数据容器和算法分开且彼此独立,最后再用黏合剂将它们撮合在一起。

>>> 1.1.2 迭代器接口

为什么一定要考虑引入Iterator这种复杂的设计模式呢?如果是数组,直接使用for循环语句进行遍历处理不就可以了吗?为什么要在集合之外引入Iterator这个角色呢?一个重要的理由是,引入Iterator后可以将遍历与实现分离。

实际上无论是单向链表还是双向链表,其查找算法与遍历算法的实现没有多少差别,基本上都是重复劳动。如果代码中有bug,则需要修改所有相关的代码。为什么会出现这样的情况呢?主要是接口设计不合理所造成的,其最大的问题就是将容器和算法放在了一起,且算法的实现又依赖于容器的实现,因而必须为每一个容器开发一套与之匹配的算法。

假设要在2种容器(双向链表、动态数组)中分别实现6种算法(交换、排序、求最大值、求最小值、遍历、查找),显然需要2×6=12个接口函数才能实现目标。随着算法数量的不断增多,势必导致函数的数量成倍增加,重复劳动的工作量也越大。如果将容器和算法单独设计,则只需要实现6个算法函数就行了。即算法不依赖容器的特定实现,算法不会直接在容器中进行操作。比如,排序算法无需关心元素是存放在数组或线性表中。

在正式引入迭代器之前,不妨分析一下如程序清单 3.49所示的冒泡排序算法。

程序清单 3.49  冒泡排序算法

1    #include

2    #include "swap.h"

3

4    void bubbleSort(int *begin, int *end)

5    {

6          int flag = 1;                                      // flag = 1表示指针的内容未交换

7          int *p1 = begin;                                      // p1指向数组的首元素

8          int *p2 = end;                                   // p2指向数组的尾元素

9          int *pNext;                                        // pNext指向p1所指向的元素的下一个元素

10 

11        while(p2 != begin){

12             p1 = begin;

13             flag = 1;

14             while(p1 != p2){

15                   pNext = p1+1;

16                   if(*p1>*pNext)                            // 比较指针所指向的值的大小

17                   {

18                        swap(p1, pNext);                   // 交换2个指针的内容

19                        flag = 0;                          // flag = 0,表示2个指针的内容交换

20                   }

21                   p1++;                              // p1指针后移

22             } 

23             if(flag)  return;                              // 没有交换,表示已经有序,则直接返回

24             p2--;                                           // p2指针前移

25        }

26  }

27

28  int main(int argc, char *argv[])

29  {

30        int a[]={5, 3, 2, 4, 1};

31        int i = 0;

3233        bubbleSort(a, a+4);

34        for(i = 0; i < sizeof(a) / sizeof(a[0]); i++){

35              printf("%d ", a[i]);

36        }

37        return 0;

38  } 

如果任何一次遍历没有执行任何交换,则说明记录是有序的且终止排序。其中,p1指向数组的首元素,pNext指向p1所指向的元素的下一个元素,p2指向数组的尾元素(图 3.21(a))。如果*p1>*pNext,则交换指针所指向的内容,p1与pNext后移(图 3.21(b)),反之指针所指向的内容不变,p1与pNext后移,经过一轮排序之后,直到p1 = p2为止,最大元素移到数组尾部。

图 3.21  内部循环执行过程示意图

当最大元素移到数组的尾部时,则退出内部循环。p2前移后程序跳转到程序清单 3.49(15),p1再次指向数组的首元素,pNext指向p1所指向的元素的下一个元素(图 3.22(a))。此时,图 3.22(a)与图 3.21(a)的差别在于p2指向a[3]。经过一轮循环之后,直到p1 = p2,此时整数4移到a[3]所在的位置,剩余的排序详见图 3.22。当p1与p2重合在数组首元素所在的位置时,表示排序结束(图 3.22(d))。

周立功

图 3.22 外部循环执行过程示意图

由此可见,冒泡排序算法的核心是指针的操作,其主要行为如下:

比较指针所指向的值的大小;

● 交换指针所指向的内容;

● 指针后移,即指针指向下一个元素;

● 指针前移,即指针指向前面一个元素。

由于这里是以int类型数据为例实现冒泡排序的,因此用户知道如何比较数据和如何交换指针所指向的内容,以及指针的前后移动。当使用支持任意类型数据的void *时,虽然算法程序不知道传入什么类型的数据,但调用者知道,因此在调用排序算法函数时,可以由用户传递参数通过回调函数实现。修改后的冒泡排序函数原型如下:

void iter_sort (void *begin, void *end, compare_t compare, swap_t swap);

其中,compare用于比较两个指针所指向的值的大小,compare_t类型定义如下

typedef int (*compare_t) (void *p1, void *p2); 

swap函数用于交换两个指针指向的内容,swap_t类型定义如下:

typedef void (*swap_t) (void *p1, void *p2);

显然无法通过++或--移动指针,因为不知道传入的是什么类型的数据。如果知道数据占用4个字节,则可以通过指针的值加4或减4实现指针的移动。虽然使用这种方式可以实现指针的移动,但始终要求数据必须以数组的形式存储,一旦离开了这个特定的容器,则无法确定指针的行为。如果将算法与链表结合起来使用显然代码中的p1++p2--不适合链表

基于此,“不妨对指针进行抽象,让它针对不同的容器有不同的实现,而算法只关心它的指针接口”。显然,需要容器提供相应的接口函数,才能实现指针前移和后移,通常将这样的指针称为“迭代器”。从某种意义上来说,迭代器作为算法的接口是广义指针,而指针满足所有迭代器的要求。其优势在于对任何种类的容器都可以用同样的方法顺序遍历容器中的元素,而又不暴露容器的内部细节,迭代器接口的声明详见程序清单3.50

程序清单3.50  迭代器接口的声明

1   typedef void *iterator_t;                              // 定义迭代器类型

2    typedef void (*iterator_next_t)(iterator_t *p_iter);

3    typedef void (*iterator_prev_t)(iterator_t *p_iter);

4

5    // 迭代器接口(if 表示 interface由具体容器实现比如链表、数组等

6    typedef struct _iterator_if{

7        iterator_next_t  pfn_next;                          // 迭代器后移函数相当于p1++

8        iterator_prev_t  pfn_prev;                          // 迭代器前移函数相当于p2--

9    }iterator_if_t;

其中,p_iter指向的内容是由容器决定的,它既可以指向结点,也可以指向数据。无论是链表还是其它容器实现的pfn_next函数,其意义是一样的,其它函数同理。如果将迭代器理解为指向数据的指针变量,则pfn_next函数让迭代器指向容器的下一个数据,pfn_prev函数让迭代器指向容器的上一个数据

此时,应该针对接口编写一些获取或设置数值的方法。用于读取变量的方法通常称为“获取方法(getter)”,用于写入变量的方法通常称为“设置方法(setter)”。下面以双向链表为例,使用结构体指针作为dlist_iterator_if_get()的返回值,详见程序清单3.51

程序清单3.51  获取双向链表的迭代器接口(1)

1    static void __dlist_iterator_next(iterator_t *p_iter)           // 让迭代器指向容器的下一个数据

2    {

3        *p_iter = ((dlist_node_t *)*p_iter) -> p_next; 

4    }

5

6    static void __dlist_iterator_prev(iterator_t *p_iter)           // 让迭代器指向容器的上一个数据

7    {

8        *p_iter = ((dlist_node_t *)*p_iter) -> p_prev;

9    }

10

11  iterator_if_t *dlist_iterator_if_get (void)

12  {

13      static iterator_if_t iterator_if;

14      iterator_if.pfn_next  = __dlist_iterator_next;

15      iterator_if.pfn_prev  = __dlist_iterator_prev;

16      return &iterator_if;                                // 返回结构体变量地址&iterator_if

17  }

其调用形式如下:

 iterator_if_t *p_if = dlist_iterator_if_get();             // 获得链表的迭代器接口,即p_if = &iterator_if

注意,如果省略static,则iterator_if就成了一个局部变量。由于它将在函数执行完后失效,因此返回它的地址毫无意义。这里采用了直接访问结构体成员的方式对iterator_if_t类型的结构体赋值,显然不同模块之间应该尽可能避免这种方式,取而代之的是提供相应的接口,详见程序清单 3.52

程序清单 3.52 获取双向链表的迭代器接口(2)

1    void dlist_iterator_if_get(iterator_if_t *p_if)

2    {

3        p_if -> pfn_next  = __dlist_iterator_next;

4        p_if -> pfn_prev  = __dlist_iterator_prev;

5    } 

其调用形式如下:

iterator_if_t iterator_if;

dlist_iterator_if_get(&iterator_if);

由于iterator_if_t类型的结构体中只有两个函数指针,因此对函数指针的访问仅包含设置和调用,详见程序清单 3.53

程序清单 3.53 迭代器接口(iterator.h)

1    #pragma once;

2

3    typedef void *iterator_t;

4    typedef void(*iterator_next_t)(iterator_t *p_iter);

5    typedef void(*iterator_prev_t)(iterator_t *p_iter);

6

7    typedef struct _iterator_if{

8        iterator_next_t  pfn_next;                          // 调用迭代器后移的函数指针,相当于p1++

9        iterator_prev_t  pfn_prev;                          // 调用迭代器前移的函数指针,相当于p2--

10  }iterator_if_t;

11 

12  void iterator_if_init(iterator_if_t *p_if, iterator_next_t pfn_next, iterator_prev_t pfn_prev);

13  void iterator_next(iterator_if_t *p_if, iterator_t *p_iter);   // 迭代器后移函数,相当于++

14  void iterator_prev(iterator_if_t *p_if, iterator_t *p_iter);   // 迭代器前移函数,相当于-- 

这些函数的具体实现详见程序清单3.54

程序清单3.54  迭代器接口的实现

1    #include "iterator.h"

2

3    void iterator_if_init(iterator_if_t *p_if, iterator_next_t pfn_next, iterator_prev_t pfn_prev)

4    {

5        p_if -> pfn_next = pfn_next;

6        p_if -> pfn_prev = pfn_prev;

7    }

8

9    void iterator_next(iterator_if_t *p_if, iterator_t *p_iter)

10  {

11      p_if -> pfn_next(p_iter);

12  }

13

14  void iterator_prev(iterator_if_t *p_if, iterator_t *p_iter)

15  {

16      p_if -> pfn_prev(p_iter);

17  } 

现在可以直接调用iterator_if_init()实现dlist_iterator_if_get(),详见程序清单 3.55

程序清单 3.55 获取双向链表的迭代器接口(3)

1    void dlist_iterator_if_get(iterator_if_t *p_if)

2    {

3        iterator_if_init(p_if, __dlist_iterator_next, __dlist_iterator_prev);

4    }

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

全部0条评论

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

×
20
完善资料,
赚取积分