# 3.3.2表示队列

> 译者： [https://sicp.comp.nus.edu.sg/chapters/59](https://sicp.comp.nus.edu.sg/chapters/59)

突变体`set_head`和`set_tail`使我们能够使用对来构建不能单独使用`pair`，`head`和`tail`构建的数据结构。 本节说明如何使用对来表示称为队列的数据结构。 [部分3.3.3](60) 将展示如何表示称为表的数据结构。

_队列_是这样的序列，其中项目在一端插入（称为队列的_在后面_），而从另一端（在_在前面_）删除。 图 [3.18](59#fig_3.18) 显示了最初为空的队列，其中插入了`a`和`b`项目。 然后删除`a`，插入`c`和`d`，然后删除`b`。 因为项目总是按照插入顺序删除，所以有时将队列称为 _FIFO_ （先进先出）缓冲区。

<figure>![](img/d8b8c917838b05314f15f9b22ddd8e04.jpg)**[图3.18](59#fig_3.18)** 队列操作。</figure>

在数据抽象方面，我们可以将队列视为由以下一组操作定义的：

*   a constructor:

    `make_queue()`返回一个空队列（一个不包含任何项目的队列）。

*   two selectors:

    `empty_queue(queue)`测试队列是否为空。

    `front_queue(queue)`返回队列最前面的对象，如果队列为空，则指示错误。 它不会修改队列。

*   two mutators:

    `insert_queue(queue, item)`将项目插入队列的后面，并返回修改后的队列作为其值。

    `delete_queue(queue)`删除队列前面的项目，并返回修改后的队列作为其值，如果在删除之前队列为空，则发出错误消息。

因为队列是一系列项目，所以我们当然可以将其表示为普通列表。 队列的最前面将是列表的`head`，在队列中插入一个项目将等于在列表的末尾添加一个新元素，而从队列中删除一个项目将仅使用`tail` 列表中。 但是，这种表示方式效率低下，因为要插入项目，我们必须扫描列表直到到达末尾。 由于我们扫描列表的唯一方法是通过连续的`tail`操作，因此此扫描需要![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg)个步骤来生成![n](img/493731e423d5db62086d0b8705dda0c8.jpg)项目的列表。 对列表表示的简单修改通过允许执行队列操作以使其需要![%5CTheta%281%29](img/c3d9a11e35b38a5e6071a216846a0e6d.jpg)步骤来克服了该缺点。 也就是说，所需的步骤数与队列的长度无关。

列表表示的困难源于需要扫描以找到列表的末尾。 我们需要扫描的原因是，尽管将列表表示为成对链的标准方法很容易为我们提供了指向列表开头的指针，但它没有给我们提供指向结尾的轻松指针。 避免此缺点的修改是将队列表示为列表，以及一个额外的指针，该指针指示列表中的最后一对。 这样，当我们插入一个项目时，我们可以查询后方指针，从而避免扫描列表。

然后，将队列表示为一对指针`front_ptr`和`rear_ptr`，它们分别指示普通列表中的第一对和最后一对。 由于我们希望队列是可识别的对象，因此可以使用`pair`组合两个指针。 因此，队列本身将是两个指针的`pair`。 图 [3.19](59#fig_3.19) 说明了此表示。

<figure>![](img/4854a47f80703eae5c9f17175010c710.jpg)**[图3.19](59#fig_3.19)** 将队列实现为带有前后指针的列表。</figure>

为了定义队列操作，我们使用以下函数，这些函数使我们能够选择和修改队列的前后指针：

```js
function front_ptr(queue) {
    return head(queue);
}
function rear_ptr(queue) {
    return tail(queue);
}
function set_front_ptr(queue, item) {
    set_head(queue, item);
}
function set_rear_ptr(queue, item) {
    set_tail(queue, item);
}
```

现在，我们可以实现实际的队列操作。 如果队列的前指针为空列表，我们将认为该队列为空：

```js
function is_empty_queue(queue) {
    return is_null(front_ptr(queue));
}
```

`make_queue`构造函数返回一个初始为空的队列，其`head`和`tail`均为空列表的一对：

```js
function make_queue() {
    return pair(null, null);
}
```

要选择队列最前面的项目，我们返回由最前面的指针指示的对中的`head`：

```js
function front_queue(queue) {
    return is_empty_queue(queue)
        ? error(queue, "front_queue called with an empty queue:")
        : front_ptr(queue);
}
```

要在队列中插入项目，请按照图 [3.20](59#fig_3.20) 所示的方法进行操作。 我们首先创建一个新对，其`head`是要插入的项目，而`tail`是空列表。 如果队列最初是空的，则将队列的前后指针设置为该新对。 否则，我们将修改队列中的最后一对以指向新对，并将后指针设置为新对。

<figure>![](img/4e4048b90d8d7cf61fdba16926409c07.jpg)**[图3.20](59#fig_3.20)** 在图 <ref name="fig:queue-pointers">[3.19](59#fig_3.19)</ref> 的队列中使用`insert_queue(q, 'd')`的结果 。</figure>

```js
function insert_queue(queue, item) {
    const new_pair = pair(item, null);
    if (is_empty_queue(queue)) {
        set_front_ptr(queue, new_pair);
        set_rear_ptr(queue, new_pair);
    } else {
        set_tail(rear_ptr(queue), new_pair);
        set_rear_ptr(queue, new_pair);
    }
    return queue;
}
```

要删除队列最前面的项目，我们只需修改前端指针，使其现在指向队列中的第二个项目，就可以通过跟随第一个项目的`tail`指针找到它（参见图 [3.21](59#fig_3.21) ）： [[1]](59#footnote-1)

<figure>![](img/69cec488f60743ff957f7bd63eadb95e.jpg)**[图3.21](59#fig_3.21)** 的结果 在图 <ref name="fig:queue-insert">[3.20](59#fig_3.20)</ref> 的队列上使用`delete_queue(q)`。</figure>

```js
function delete_queue(queue) {
    if (is_empty_queue(queue)) {
        error(queue, "delete_queue called with an empty queue:");
    } else {
        set_front_ptr(queue, tail(front_ptr(queue)));
        return queue;
    }
}
```

<exercise>Ben Bitdiddle decides to test the queue implementation described above. He types in the functions to the JavaScript interpreter and proceeds to try them out:

```js
const q1 = make_queue();
```

```js
insert_queue(q1, "a");
```

```js
insert_queue(q1, "b");
```

```js
delete_queue(q1);
```

```js
delete_queue(q1);
```

<quote>It's all wrong!</quote> he complains. <quote>The interpreter's response shows that the last item is inserted into the queue twice. And when I delete both items, the second `b` is still there, so the queue isn't empty, even though it's supposed to be.</quote> Eva Lu Ator suggests that Ben has misunderstood what is happening. <quote>It's not that the items are going into the queue twice,</quote> she explains. <quote>It's just that the standard JavaScript printer doesn't know how to make sense of the queue representation. If you want to see the queue printed correctly, you'll have to define your own print function for queues.</quote> Explain what Eva Lu is talking about. In particular, show why Ben's examples produce the printed results that they do. Define a function `print_queue` that takes a queue as input and prints the sequence of items in the queue.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_59_1_div">Solution</button> <solution>```js
function last_pair(items) {
    return is_null(tail(items))
           ? items
           : last_pair(tail(items));
}
```</solution></exercise> <exercise>Instead of representing a queue as a pair of pointers, we can build a queue as a function with local state. The local state will consist of pointers to the beginning and the end of an ordinary list. Thus, the `make_queue` function will have the form

```js
function make_queue() {
    function front_ptr(...) ...
    function rear_ptr(...) ...
    //definitions of internal functions
    function dispatch(m) ...
    return dispatch;
}
```

Complete the definition of `make_queue` and provide implementations of the queue operations using this representation.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_59_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise><exercise>A _deque_ (<quote>double-ended queue</quote>) is a sequence in which items can be inserted and deleted at either the front or the rear. Operations on deques are the constructor `make_deque`, the predicate `is_empty_deque`, selectors `front_deque` `front-deque` and `rear_deque` and mutators `front_insert_deque`, `front_delete_deque`, `rear_insert_deque`, and `rear_delete_deque`. Show how to represent deques using pairs, and give implementations of the operations.[[2]](59#footnote-2) All operations should be accomplished in ![%5CTheta%281%29](img/c3d9a11e35b38a5e6071a216846a0e6d.jpg) steps.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#no_solution_59_1_div">Add solution</button>There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in [Github](https://github.com/source-academy/sicp).</exercise>

* * *

[[1]](59#footnote-link-1) If the first item is the final item in the queue, the front pointer will be the empty list after the deletion, which will mark the queue as empty; we needn't worry about updating the rear pointer, which will still point to the deleted item, because `empty-queue?` looks only at the front pointer.

[[2]](59#footnote-link-2) Be careful not to make the interpreter try to print a structure that contains cycles. (See exercise <ref name="ex:make-cycle">[3.13](58#ex_3.13)</ref>.)

