4. 队列与广度优先搜索

队列也是一组元素的集合,也提供两种基本操作:Enqueue(入队)将元素添加到队尾,Dequeue(出队)从队头取出元素并返回。就像排队买票一样,先来先服务,先入队的人也是先出队的,这种方式称为FIFO(First In First Out,先进先出),有时候队列本身也被称为FIFO。

下面我们用队列解决迷宫问题。程序如下:

例 12.4. 用广度优先搜索解迷宫问题

  1. #include <stdio.h>
  2.  
  3. #define MAX_ROW 5
  4. #define MAX_COL 5
  5.  
  6. struct point { int row, col, predecessor; } queue[512];
  7. int head = 0, tail = 0;
  8.  
  9. void enqueue(struct point p)
  10. {
  11. queue[tail++] = p;
  12. }
  13.  
  14. struct point dequeue(void)
  15. {
  16. return queue[head++];
  17. }
  18.  
  19. int is_empty(void)
  20. {
  21. return head == tail;
  22. }
  23.  
  24. int maze[MAX_ROW][MAX_COL] = {
  25. 0, 1, 0, 0, 0,
  26. 0, 1, 0, 1, 0,
  27. 0, 0, 0, 0, 0,
  28. 0, 1, 1, 1, 0,
  29. 0, 0, 0, 1, 0,
  30. };
  31.  
  32. void print_maze(void)
  33. {
  34. int i, j;
  35. for (i = 0; i < MAX_ROW; i++) {
  36. for (j = 0; j < MAX_COL; j++)
  37. printf("%d ", maze[i][j]);
  38. putchar('\n');
  39. }
  40. printf("*********\n");
  41. }
  42.  
  43. void visit(int row, int col)
  44. {
  45. struct point visit_point = { row, col, head-1 };
  46. maze[row][col] = 2;
  47. enqueue(visit_point);
  48. }
  49.  
  50. int main(void)
  51. {
  52. struct point p = { 0, 0, -1 };
  53.  
  54. maze[p.row][p.col] = 2;
  55. enqueue(p);
  56.  
  57. while (!is_empty()) {
  58. p = dequeue();
  59. if (p.row == MAX_ROW - 1 /* goal */
  60. && p.col == MAX_COL - 1)
  61. break;
  62. if (p.col+1 < MAX_COL /* right */
  63. && maze[p.row][p.col+1] == 0)
  64. visit(p.row, p.col+1);
  65. if (p.row+1 < MAX_ROW /* down */
  66. && maze[p.row+1][p.col] == 0)
  67. visit(p.row+1, p.col);
  68. if (p.col-1 >= 0 /* left */
  69. && maze[p.row][p.col-1] == 0)
  70. visit(p.row, p.col-1);
  71. if (p.row-1 >= 0 /* up */
  72. && maze[p.row-1][p.col] == 0)
  73. visit(p.row-1, p.col);
  74. print_maze();
  75. }
  76. if (p.row == MAX_ROW - 1 && p.col == MAX_COL - 1) {
  77. printf("(%d, %d)\n", p.row, p.col);
  78. while (p.predecessor != -1) {
  79. p = queue[p.predecessor];
  80. printf("(%d, %d)\n", p.row, p.col);
  81. }
  82. } else
  83. printf("No path!\n");
  84.  
  85. return 0;
  86. }

运行结果如下:

  1. 2 1 0 0 0
  2. 2 1 0 1 0
  3. 0 0 0 0 0
  4. 0 1 1 1 0
  5. 0 0 0 1 0
  6. *********
  7. 2 1 0 0 0
  8. 2 1 0 1 0
  9. 2 0 0 0 0
  10. 0 1 1 1 0
  11. 0 0 0 1 0
  12. *********
  13. 2 1 0 0 0
  14. 2 1 0 1 0
  15. 2 2 0 0 0
  16. 2 1 1 1 0
  17. 0 0 0 1 0
  18. *********
  19. 2 1 0 0 0
  20. 2 1 0 1 0
  21. 2 2 2 0 0
  22. 2 1 1 1 0
  23. 0 0 0 1 0
  24. *********
  25. 2 1 0 0 0
  26. 2 1 0 1 0
  27. 2 2 2 0 0
  28. 2 1 1 1 0
  29. 2 0 0 1 0
  30. *********
  31. 2 1 0 0 0
  32. 2 1 2 1 0
  33. 2 2 2 2 0
  34. 2 1 1 1 0
  35. 2 0 0 1 0
  36. *********
  37. 2 1 0 0 0
  38. 2 1 2 1 0
  39. 2 2 2 2 0
  40. 2 1 1 1 0
  41. 2 2 0 1 0
  42. *********
  43. 2 1 0 0 0
  44. 2 1 2 1 0
  45. 2 2 2 2 2
  46. 2 1 1 1 0
  47. 2 2 0 1 0
  48. *********
  49. 2 1 2 0 0
  50. 2 1 2 1 0
  51. 2 2 2 2 2
  52. 2 1 1 1 0
  53. 2 2 0 1 0
  54. *********
  55. 2 1 2 0 0
  56. 2 1 2 1 0
  57. 2 2 2 2 2
  58. 2 1 1 1 0
  59. 2 2 2 1 0
  60. *********
  61. 2 1 2 0 0
  62. 2 1 2 1 2
  63. 2 2 2 2 2
  64. 2 1 1 1 2
  65. 2 2 2 1 0
  66. *********
  67. 2 1 2 2 0
  68. 2 1 2 1 2
  69. 2 2 2 2 2
  70. 2 1 1 1 2
  71. 2 2 2 1 0
  72. *********
  73. 2 1 2 2 0
  74. 2 1 2 1 2
  75. 2 2 2 2 2
  76. 2 1 1 1 2
  77. 2 2 2 1 0
  78. *********
  79. 2 1 2 2 0
  80. 2 1 2 1 2
  81. 2 2 2 2 2
  82. 2 1 1 1 2
  83. 2 2 2 1 2
  84. *********
  85. 2 1 2 2 2
  86. 2 1 2 1 2
  87. 2 2 2 2 2
  88. 2 1 1 1 2
  89. 2 2 2 1 2
  90. *********
  91. 2 1 2 2 2
  92. 2 1 2 1 2
  93. 2 2 2 2 2
  94. 2 1 1 1 2
  95. 2 2 2 1 2
  96. *********
  97. (4, 4)
  98. (3, 4)
  99. (2, 4)
  100. (2, 3)
  101. (2, 2)
  102. (2, 1)
  103. (2, 0)
  104. (1, 0)
  105. (0, 0)

其实仍然可以像例 12.3 “用深度优先搜索解迷宫问题”一样用predecessor数组表示每个点的前趋,但我想换一种更方便的数据结构,直接在每个点的结构体中加一个成员表示前趋:

  1. struct point { int row, col, predecessor; } queue[512];
  2. int head = 0, tail = 0;

变量headtail是队头和队尾指针,head总是指向队头,tail总是指向队尾的下一个元素。每个点的predecessor成员也是一个指针,指向它的前趋在queue数组中的位置。如下图所示:

图 12.3. 广度优先搜索的队列数据结构

广度优先搜索的队列数据结构

为了帮助理解,我把这个算法改写成伪代码如下:

  1. 将起点标记为已走过并入队;
  2. while (队列非空) {
  3. 出队一个点p;
  4. if (p这个点是终点)
  5. break;
  6. 否则沿右、下、左、上四个方向探索相邻的点
  7. if (和p相邻的点有路可走,并且还没走过)
  8. 将相邻的点标记为已走过并入队,它的前趋就是刚出队的p点;
  9. }
  10. if (p点是终点) {
  11. 打印p点的座标;
  12. while (p点有前趋) {
  13. p = p点的前趋;
  14. 打印p点的座标;
  15. }
  16. } else
  17. 没有路线可以到达终点;

从打印的搜索过程可以看出,这个算法的特点是沿各个方向同时展开搜索,每个可以走通的方向轮流往前走一步,这称为广度优先搜索(BFS,Breadth First Search)。探索迷宫和队列变化的过程如下图所示。

图 12.4. 广度优先搜索

广度优先搜索

广度优先是一种步步为营的策略,每次都从各个方向探索一步,将前线推进一步,图中的虚线就表示这个前线,队列中的元素总是由前线的点组成的,可见正是队列先进先出的性质使这个算法具有了广度优先的特点。广度优先搜索还有一个特点是可以找到从起点到终点的最短路径,而深度优先搜索找到的不一定是最短路径,比较本节和上一节程序的运行结果可以看出这一点,想一想为什么。

习题

1、本节的例子直接在队列元素中加一个指针成员表示前趋,想一想为什么上一节的例 12.3 “用深度优先搜索解迷宫问题”不能采用这种方法表示前趋?

2、本节例子中给队列分配的存储空间是512个元素,其实没必要这么多,那么解决这个问题至少要分配多少个元素的队列空间呢?跟什么因素有关?