# 2.3.3示例：表示集

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

在前面的示例中，我们为两种复合数据对象构建了表示形式：有理数和代数表达式。 在这些示例之一中，我们可以选择在构造时或选择时简化（减少）表达式，但除此之外，选择基于列表的这些结构表示很简单。 当我们转向集合的表示时，表示的选择不是那么明显。 确实，存在许多可能的表示，并且它们在几种方式上彼此明显不同。

非正式地，集合只是不同对象的集合。 为了给出更精确的定义，我们可以采用数据抽象的方法。 也就是说，我们通过指定要在集合上使用的操作来定义&lt;quote&gt;集合&lt;/quote&gt;。 它们是`union_set`，`intersection_set`，`is_element_of_set`和`adjoin_set`。 函数`is_element_of_set`是确定给定元素是否为集合成员的谓词。 函数`adjoin_set`将一个对象和一个集合作为参数，并返回一个集合，其中包含原始集合的元素以及邻接的元素。 函数`union_set`计算两个集合的并集，这是包含出现在任一自变量中的每个元素的集合。 函数`intersection_set`计算两个集合的交集，该集合仅包含同时出现在两个参数中的元素。 从数据抽象的角度来看，我们可以自由设计以与上述解释一致的方式实现这些操作的任何表示形式。 [[1]](37#footnote-1)

## 设置为无序列表

表示集合的一种方法是将其表示为元素列表，其中没有元素出现一次以上。 空集由空列表表示。 在此表示形式中，`is_element_of_set`与 [2.3.1](35) 一节的功能`member`相似。 它使用`is_equal`而不是`===`，因此set元素不必是原始值：

```js
function is_element_of_set(x, set) {
   return ! is_null(set) && 
          ( is_equal(x, head(set)) || 
            is_element_of_set(x, tail(set)) );
}
```

使用此代码，我们可以编写`adjoin_set`。 如果要邻接的对象已经在集合中，则只返回集合。 否则，我们使用`pair`将对象添加到表示集合的列表中：

```js
function adjoin_set(x, set) {
    return is_element_of_set(x, set)
           ? set
           : pair(x, set);
}
```

对于`intersection_set`，我们可以使用递归策略。 如果我们知道如何形成`set2`与`set1`的`tail`的交集，则只需决定是否在其中包括`set1`的`head`。 但这取决于`head(set1)`是否也包含在`set2`中。 这是结果函数：

```js
function intersection_set(set1, set2) {
    return is_null(set1) || is_null(set2)
           ? null
           : is_element_of_set(head(set1), set2)
             ? pair(head(set1),
                    intersection_set(tail(set1), set2))
             : intersection_set(tail(set1), set2);
}
```

在设计表示形式时，我们应该关注的问题之一是效率。 考虑设置操作所需的步骤数。 由于它们都使用`is_element_of_set`，因此此操作的速度对整个设置实现的效率有重大影响。 现在，为了检查对象是否是集合的成员，`is_element_of_set`可能必须扫描整个集合。 （在最坏的情况下，对象原来不在集合中。）因此，如果集合中包含![n](img/493731e423d5db62086d0b8705dda0c8.jpg)个元素，则`is_element_of_set`可能要执行![n](img/493731e423d5db62086d0b8705dda0c8.jpg)个步骤。 因此，所需的步骤数随着![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg)的增加而增加。 使用该操作的`adjoin-set`所需的步数也随着![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg)增长。 对于要对`set1`的每个元素进行`is_element_of_set`检查的`intersection_set`，所需步数随所涉及集合的大小的乘积而增加，或者对于两组大小![n](img/493731e423d5db62086d0b8705dda0c8.jpg)的![%5CTheta%28n%5E%7B2%7D%29](img/0ad655df80097a441005e441b5d6c09a.jpg)乘积而增加。 `union_set`也是一样。

<exercise>Implement the `union_set` operation for the unordered-list representation of sets.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_1_div">Solution</button> <solution>```js
function union_set(set1, set2) {
    return is_null(set1)
           ? set2
           : adjoin_set(head(set1), 
                 union_set(tail(set1), set2));
}
```</solution></exercise> <exercise>We specified that a set would be represented as a list with no duplicates. Now suppose we allow duplicates. For instance, the set ![%5C%7B1%2C2%2C3%5C%7D](img/caace753e5218e6de96983656e6e552c.jpg) could be represented as the list `list(2, 3, 2, 1, 3, 2, 2)`. Design functions `is_element_of_set`, `adjoin_set`, `union_set`, and `intersection_set` that operate on this representation. How does the efficiency of each compare with the corresponding function for the non-duplicate representation? Are there applications for which you would use this representation in preference to the non-duplicate one?<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_2_div">Solution</button><solution>The functions `is_element_of_set` and `intersection_set` remain unchanged. Here is the new implementation of `adjoin_set` and `union_set`.

```js
function adjoin_set(x, set) {
  return pair(x, set);
}
function union_set(set1, set2) {
  return append(set1, set2);
}
```

In the version with no duplicates, the required number of steps for `is_element_of_set` and `adjoin_set` has an order of growth of ![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg), where ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) is the number of element occurrences in the given representation, and the required number of steps for `intersection_set` and `union_set` has an order of growth of ![O%28n%20m%29](img/223fa189cdcaf00dab297ef226f5d0b7.jpg), where ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) is the number of element occurrences in the representation of the first set and ![m](img/20ddd8181c2e0d0fb893637e8572d475.jpg) is the number of element occurrences in the representation of the second set. In the version that allows duplicates, the number of steps for `adjoin_set` shrinks to ![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg), and the number of steps for `union_set` shrinks to ![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg). However, note that the number of element occurrences may be much larger in the second version, because many duplicates may accumulate. For applications where duplicate elements are rare, the version that allows duplicates is preferrable.</solution></exercise>

## 设置为有序列表

加快设置操作速度的一种方法是更改​​表示形式，以便按递增顺序列出设置元素。 为此，我们需要某种比较两个对象的方法，以便可以说哪个更大。 例如，我们可以按字典顺序比较符号，或者可以同意一种为对象分配唯一编号的方法，然后通过比较相应的编号来比较元素。 为了使讨论简单，我们将仅考虑集合元素为数字的情况，以便我们可以使用`&gt;`和`&lt;`比较元素。 我们将通过按升序列出其元素来表示一组数字。 上面的第一个表示形式允许我们以任何顺序列出元素来表示集合![%5C%7B1%2C3%2C6%2C10%5C%7D](img/3b27ac6685404df8a289711e13dfaea5.jpg)，而我们的新表示形式仅允许列表`list(1, 3, 6, 10)`。

`is_element_of_set`中显示了排序的一个优点：在检查项目是否存在时，我们不再需要扫描整个集合。 如果达到的集合元素大于要查找的项目，则我们知道该项目不在集合中：

```js
function is_element_of_set(x,set) {
    return ! is_null(set) && 
           ( x === head(set)) 
             ? true
             : x < head(set)
               ? false
               : is_element_of_set(x, tail(set));
}
```

这样可以节省多少步骤？ 在最坏的情况下，我们要查找的项目可能是集合中最大的项目，因此步骤数与无序表示的步骤数相同。 另一方面，如果我们搜索许多不同大小的项目，则可以预期有时我们将能够在列表开头附近的某个点停止搜索，而在其他时候，我们仍然需要检查大部分列表。 平均而言，我们应该期望检查集合中大约一半的项目。 因此，所需的平均步骤数将约为![n%2F2](img/45b674d84609611c72c96c828d639c3c.jpg)。 这仍然是![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg)的增长，但平均而言，与之前的实现相比，它的确为我们节省了2倍的步数。

通过`intersection_set`我们获得了更令人印象深刻的加速。 在无序表示中，此操作需要![%5CTheta%28n%5E2%29](img/e86789a30174e5928ea8d6cd8afb2845.jpg)步骤，因为我们对`set1`的每个元素执行了`set2`的完整扫描。 但是，通过有序表示，我们可以使用更巧妙的方法。 首先比较两组的初始元素`x1`和`x2`。 如果`x1`等于`x2`，则给出交集的一个元素，其余交集为两组`tail`的交集。 但是，假设`x1`小于`x2`。 由于`x2`是`set2`中最小的元素，因此我们可以立即得出结论，`x1`无法出现在`set2`中的任何位置，因此不会出现在交点中。 因此，交点等于`set2`与`set1`的`tail`的交点。 类似地，如果`x2`小于`x1`，则交集由`set1`与`set2`的`tail`的交集给出。 这是函数：

```js
function intersection_set(set1, set2) {
    if (is_null(set1) || is_null(set2)) {
        return null;
    } else {
      const x1 = head(set1);
      const x2 = head(set2);
      return x1 === x2
             ? pair(x1, intersection_set(tail(set1),
                                        tail(set2)))
             : x1 < x2 
	       ? intersection_set(tail(set1), set2)
               : intersection_set(set1,
                                  tail(set2));
    }
}
```

要估计此过程所需的步骤数，请注意，在每个步骤上，我们都将交集问题简化为计算较小集合的交集-从`set1`或`set2`或两者中删除第一个元素。 因此，所需的步骤数最多是`set1`和`set2`大小的总和，而不是无序表示形式的大小的乘积。 这是![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg)的增长，而不是![%5CTheta%28n%5E2%29](img/e86789a30174e5928ea8d6cd8afb2845.jpg)的增长，即使对于中等规模的设备，这也是相当可观的加速。

<exercise>Give an implementation of `adjoin_set` using the ordered representation. By analogy with `is_element_of_set` show how to take advantage of the ordering to produce a function that requires on the average about half as many steps as with the unordered representation.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_3_div">Solution</button> <solution>```js
function adjoin_set(x, set) {
    return is_null(set)
           ? list(x)
           : x === head(set) 
             ? set
             : x < head(set)
               ? pair(x, set)
               : pair(head(set), 
                      adjoin_set(x, tail(set)));
}
```</solution></exercise> <exercise>Give a ![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg) implementation of `union_set` for sets represented as ordered lists.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_4_div">Solution</button> <solution>```js
function union_set(set1, set2) {
    if (is_null(set1)) {
        return set2;
    } else if (is_null(set2)) {
       	return set1;
    } else {
      const x1 = head(set1);
      const x2 = head(set2);
      return x1 === x2
             ? pair(x1, union_set(tail(set1),
                                  tail(set2)))
             : x1 < x2 
               ? pair(x1, union_set(tail(set1), set2))
               : pair(x2, union_set(set1, tail(set2)));
    }
}
```

```js
union_set(
   adjoin_set(10, adjoin_set(20, adjoin_set(30, null))),
   adjoin_set(10, adjoin_set(15, adjoin_set(20, null))));
```</solution></exercise> 

## 设置为二叉树

通过以树的形式排列set元素，我们可以做得比有序列表更好。 树的每个节点都拥有该集合的一个元素，该元素在该节点处称为&lt;quote&gt;条目&lt;/quote&gt;，并具有指向两个其他（可能为空）节点中每个节点的链接。 &lt;quote&gt;左侧的&lt;/quote&gt;链接指向小于该节点处元素的元素，&lt;quote&gt;右侧的&lt;/quote&gt;链接指向该节点处大于元素的元素。 图 [2.16](37#fig_2.16) 显示了一些代表集合![%5C%7B1%2C3%2C5%2C7%2C9%2C11%5C%7D](img/e417521fa0bf2417c26ad01959ed422d.jpg)的树。 同一集合可以由树以多种不同方式表示。 有效表示的唯一要求是，左子树中的所有元素都小于节点条目，而右子树中的所有元素都更大。 图2.16 代表集合![%5C%7B%201%2C3%2C5%2C7%2C9%2C11%20%5C%7D](img/a00c03a140ebedf2b7ea5d7894f5f39c.jpg)的各种二叉树。

树表示的优点是：假设我们要检查集合中是否包含数字![x](img/40779fc60a53ff2b70f832ec10cade09.jpg)。 我们首先将![x](img/40779fc60a53ff2b70f832ec10cade09.jpg)与顶部节点中的条目进行比较。 如果![x](img/40779fc60a53ff2b70f832ec10cade09.jpg)小于此值，我们知道我们只需要搜索左子树即可； 如果![x](img/40779fc60a53ff2b70f832ec10cade09.jpg)更大，我们只需要搜索正确的子树即可。 现在，如果树是&lt;quote&gt;平衡的，则&lt;/quote&gt;这些子树中的每一个大约都是原始树大小的一半。 因此，一步，我们将搜索大小为![n](img/493731e423d5db62086d0b8705dda0c8.jpg)的树的问题减少为搜索大小为![n%2F2](img/45b674d84609611c72c96c828d639c3c.jpg)的树的问题。 由于树的大小在每一步都减半，因此我们应该期望搜索大小为![n](img/493731e423d5db62086d0b8705dda0c8.jpg)的树所需的步数随着![%5CTheta%28%5Clog%0A%20%20%20%20%20%20%20%20n%29](img/a99d6f5bb241672ff25fbb72c6ba06e1.jpg)的增加而增加。 [[2]](37#footnote-2) 对于大型集，这将比以前的表示形式有明显的提速。

我们可以使用列表来表示树。 每个节点将是三个项目的列表：该节点处的条目，左子树和右子树。 空列表的左或右子树将指示那里没有子树连接。 我们可以通过以下功能描述这种表示形式： [[3]](37#footnote-3)

```js
function entry(tree) {
   return head(tree);
}
function left_branch(tree) {
   return head(tail(tree));
}
function right_branch(tree) {
   return head(tail(tail(tree)));
}
function make_tree(entry,left,right) {
   return list(entry,left,right);
}
```

现在，我们可以使用上述策略编写`is_element_of_set`函数：

```js
function is_element_of_set(x, set) {
    return ! is_null(set) &&
           ( x === entry(set) ||
             ( x < entry(set)
               ? is_element_of_set(x, left_branch(set))
               : is_element_of_set(x, right_branch(set))
             )
           );
}
```

将项目附加到集合的方法类似，也需要![%5CTheta%28%5Clog%20n%29](img/b3291b1e09d9706bcea64c5e1f45d456.jpg)步骤。 为了邻接项`x`，我们将`x`与节点条目进行比较，以确定是将`x`添加到右侧还是左侧分支，并将`x`邻接到适当的分支上，我们将这个新构造的 分支与原始条目和另一个分支一起。 如果`x`等于条目，我们只返回该节点。 如果要求我们将`x`邻接到一棵空树，则会生成一棵以`x`为入口并且右，左分支为空的树。 这是函数：

```js
function adjoin_set(x,set) {
    return is_null(set)
           ? make_tree(x, null, null)
           : x === entry(set)
             ? set
             : x < entry(set)
               ? make_tree(entry(set),
                     adjoin_set(x, left_branch(set)),
                     right_branch(set))
               : make_tree(entry(set),
                     left_branch(set),
                     adjoin_set(x, right_branch(set)));
}
```

上面的权利要求，可以以对数步长执行搜索树的假设是，假设树是&lt;quote&gt;平衡的，&lt;/quote&gt;是平衡的，即每棵树的左和右子树具有大约相同数量的树。 元素，以便每个子树包含其父元素的大约一半。 但是我们如何确定我们建造的树木将达到平衡？ 即使我们从平衡树开始，用`adjoin_set`添加元素也可能会产生不平衡的结果。 由于新邻接元素的位置取决于该元素与集合中已有项目的比较方式，因此我们可以预期，如果我们随机添加元素，则树平均趋向于平衡。 但这不是保证。 例如，如果我们从一个空集开始并按顺序与数字1到7相邻，则最终得到如图 [2.17](37#fig_2.17) 所示的高度不平衡的树。 在此树中，所有左子树都是空的，因此与简单的有序列表相比没有任何优势。 解决此问题的一种方法是定义一个将任意树转换为具有相同元素的平衡树的操作。 然后，我们可以在每隔`adjoin_set`次操作后执行此转换，以使我们的设置保持平衡。 还有其他方法可以解决此问题，其中大多数方法涉及设计新的数据结构，可以在![%5CTheta%28%5Clog%20n%29](img/b3291b1e09d9706bcea64c5e1f45d456.jpg)步骤中完成搜索和插入操作。 [[4]](37#footnote-4) 

<figure>![](img/c8e4b767aecf26cd13e710c330fb4fa5.jpg)**[图2.17](37#fig_2.17)** 按顺序邻接1至7产生的不平衡树。</figure>

<exercise>Each of the following two functions converts a binary tree to a list.

```js
function tree_to_list_1(tree) {
    return is_null(tree)
           ? null
           : append(tree_to_list_1(left_branch(tree)),
                    pair(entry(tree),
                         tree_to_list_1(right_branch(tree))));
}
```

```js
function tree_to_list_2(tree) {
    function copy_to_list(tree, result_list) {
        return is_null(tree)
               ? result_list
               : copy_to_list(left_branch(tree),
                     pair(entry(tree),
                          copy_to_list(right_branch(tree),
                              result_list)));
    }
    return copy_to_list(tree, null);
}
```

1.  这两个函数是否对每棵树产生相同的结果？ 如果没有，结果如何不同？ 图 [2.16](37#fig_2.16) 中的树会为这两个函数生成什么列表？
2.  将具有![n](img/493731e423d5db62086d0b8705dda0c8.jpg)元素的平衡树转换为列表所需的步骤数，这两个函数的增长顺序是否相同？ 如果没有，哪一个增长速度会更慢？

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_5_div">Solution</button> <solution>1.  这两个过程产生相同的结果。 对于图 [2.16](37#fig_2.16) 中的树，结果始终为`list(1, 3, 5, 7, 9, 11)`。
2.  具有![n](img/493731e423d5db62086d0b8705dda0c8.jpg)元素的平衡树的高度为![O%28%5Clog%7Bn%7D%29](img/8e521644623d7ad5b1a512ebd98982ff.jpg)和![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg)节点。 为了使用功能`tree_to_list_1`将树转换为列表，我们调用`tree_to_list_1` ![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg)次。 我们在树的每个节点上调用`append`，但在每个级别上，我们将`append`与第一个参数中的组合![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg)元素一起应用。 因此，`tree_to_list_1`的运行时具有![O%28n%5Clog%7Bn%7D%29](img/ed28352b4adbd39103483ae35a7b74a6.jpg)的增长顺序。 代替`append`的是，函数`tree_to_list_2`不再在每个节点上调用`pair`，因此`tree_to_list_2`的增长顺序为![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg)。</solution></exercise> <exercise>The following function `list_to_tree` converts an ordered list to a balanced binary tree. The helper function `partial_tree` takes as arguments an integer ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) and list of at least ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) elements and constructs a balanced tree containing the first ![n](img/493731e423d5db62086d0b8705dda0c8.jpg) elements of the list. The result returned by `partial_tree` is a pair (formed with `pair`) whose `head` is the constructed tree and whose `tail` is the list of elements not included in the tree.

```js
function list_to_tree(elements) {
   return head(partial_tree(elements,length(elements)));
}
function partial_tree(elts, n) {
    if (n === 0) {
       return pair(null,elts);
    } else {
       const left_size = math_floor((n - 1) / 2);
       const left_result = partial_tree(elts, left_size);
       const left_tree = head(left_result);
       const non_left_elts = tail(left_result);
       const right_size = n - (left_size + 1);
       const this_entry = head(non_left_elts);
       const right_result = partial_tree(tail(non_left_elts),
                                         right_size);
       const right_tree = head(right_result);
       const remaining_elts = tail(right_result);
       return pair(make_tree(this_entry, 
                             left_tree, 
                             right_tree),
                   remaining_elts);
    }
}
```

1.  写一个简短的段落，尽可能清楚地说明`partial_tree`的工作方式。 在`list(1, 3, 5, 7, 9, 11)`列表中绘制`list_to_tree`生成的树。
2.  `list_to_tree`转换![n](img/493731e423d5db62086d0b8705dda0c8.jpg)元素列表所需的步骤数量的增长顺序是什么？

<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_6_div">Solution</button> <solution>1.  The function `partial_tree(elts, n)` returns a pair whose head is a balanced tree for the first ![%5Clfloor%20%28n%20-%201%29%20%2F%202%20%5Crfloor](img/8b5a463b9f96c20dbb89e6ccb3ce38ee.jpg) elements of `elts`, and whose tail is the list containing the remaining elements of `elts`. It works by calling itself recursively, to construct the left subtree and right subtree, and then makes the tree, and the required return pair. Thus, the overall function `list_to_tree` just needs to call `partial_tree` with the given list and its length, and return the head of the result.The tree for `list(1, 3, 5, 7, 9, 11)` is the tree on the right in Figure [2.16](37#fig_2.16).
2.  函数`list_to_tree`的运行时的增长顺序为![O%28n%29](img/8c0b5910698fedce2f3e3be706272099.jpg)，因为对于结果树的每个节点，只需要恒定数量的工作即可。</solution></exercise> <exercise>Use the results of exercises <ref name="ex:tree-to-list">[2.63](37#ex_2.63)</ref> and  <ref name="ex:list-to-tree">[2.64](37#ex_2.64)</ref> to give ![%5CTheta%28n%29](img/592c476de27cabe6879d3a7c537da592.jpg) implementations of `union_set` and `intersection_set` for sets implemented as (balanced) binary trees.[[5]](37#footnote-5)<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_7_div">Solution</button> <solution>```js
function union_set_as_tree(set1, set2) {
    const list1 = tree_to_list_2(set1);
    const list2 = tree_to_list_2(set2);
    return list_to_tree(union_set(list1, list2));
}
```

```js
function intersection_set_as_tree(set1, set2) {
    const list1=tree_to_list_2(set1);
    const list2=tree_to_list_2(set2);
    return list_to_tree(intersection_set(list1, list2));
}
```</solution></exercise> 

## 集合和信息检索

我们研究了使用列表表示集合的选项，并了解了选择数据对象表示形式如何对使用数据的程序的性能产生重大影响。 专注于集合的另一个原因是，这里讨论的技术在涉及信息检索的应用程序中一次又一次地出现。

考虑一个包含大量个人记录的数据库，例如公司的人事档案或会计系统中的交易。 典型的数据管理系统花费大量时间来访问或修改记录中的数据，因此需要一种有效的方法来访问记录。 通过标识每个记录的一部分以用作标识_密钥_来完成此操作。 密钥可以是可以唯一标识记录的任何内容。 对于人事档案，它可能是员工的ID号。 对于会计系统，它可能是交易号。 无论键是什么，当我们将记录定义为数据结构时，我们都应包括一个`key`选择器函数，该函数检索与给定记录关联的键。

现在，我们将数据库表示为一组记录。 为了找到具有给定键的记录，我们使用函数`lookup`，该函数将键和数据库作为参数，并返回具有该键的记录；如果没有这样的记录，则返回false。 功能`lookup`的实现方式几乎与`is_element_of_set`相同。 例如，如果记录集被实现为无序列表，我们可以使用

```js
function lookup(given_key, set_of_records) {
    return ! is_null(set_of_records) &&
           ( is_equal(given_key, key(head(set_of_records)))
             ? head(set_of_records)
             : lookup(given_key, tail(set_of_records))
           );
}
```

当然，比起无序列表，还有更好的方式来表示大型集合。 通常必须通过基于树的方法（例如前面讨论的二进制树表示）来实现其中必须随机访问&lt;quote&gt;记录的信息检索系统。 在设计这样的系统时，数据抽象方法可能会很有帮助。 设计人员可以使用简单，直接的表示形式（例如无序列表）创建初始实现。 这将不适用于最终的系统，但在提供用于测试系统其余部分的&lt;quote&gt;快速且肮脏的&lt;/quote&gt;数据库方面可能很有用。 稍后，可以将数据表示形式修改为更复杂。 如果根据抽象选择器和构造函数访问数据库，则表示形式的这种更改将不需要对系统的其余部分进行任何更改。&lt;/quote&gt;

<exercise>Implement the `lookup` function for the case where the set of records is structured as a binary tree, ordered by the numerical values of the keys.<button class="btn btn-secondary solution_btn" data-toggle="collapse" href="#solution_37_8_div">Solution</button> <solution>```js
function lookup(given_key, tree_of_records) {
    if (is_null(tree_of_records)) {
        return null;
    } else {
        const this_entry = entry(tree_of_records);
        const this_key = key(this_entry);
        return given_key === this_key 
               ? this_entry
               : given_key < this_key
                 ? lookup(given_key, 
                          left_branch(tree_of_records))
                 : lookup(given_key, 
                          right_branch(tree_of_records));
    }
}
```</solution></exercise> 

* * *

[[1]](37#footnote-link-1) If we want to be more formal, we can specify <quote>consistent with the interpretations given above</quote> to mean that the operations satisfy a collection of rules such as these:

*   对于任何集合`S`和任何对象`x`，`is_element_of_set(x,S)`为true（非正式地：&lt;quote&gt;将对象附加到集合将产生一个包含对象&lt;/quote&gt;的集合）。
*   对于任何`S`和`T`集以及任何对象`x`，`is_element_of_set(x,union_set(S,T))`等于`is_element_of_set(x,S) || is_element_of_set(x,T)`（非正式地： &lt;quote&gt;`union(S,T)`的元素是`S`或[[ `T`&lt;/quote&gt; ）。
*   对于任何对象`x`，`is_element_of_set(x,null)`为假（非正式地：&lt;quote&gt;没有对象是空集&lt;/quote&gt;的元素）。

 [[2]](37#footnote-link-2) Halving the size of the problem at each step is the distinguishing characteristic of logarithmic growth, as we saw with the fast-exponentiation algorithm of section <ref name="sec:exponentiation">[1.2.4](15)</ref> and the half-interval search method of section <ref name="sec:proc-general-methods">[1.3.3](21)</ref>.

[[3]](37#footnote-link-3) We are representing sets in terms of trees, and trees in terms of lists—in effect, a data abstraction built upon a data abstraction. We can regard the functions `entry`, `left_branch`, `right_branch`, and `make_tree` as a way of isolating the abstraction of a <quote>binary tree</quote> from the particular way we might wish to represent such a tree in terms of list structure.

[[4]](37#footnote-link-4) Examples of such structures include _B-trees_ and _red-black trees_. There is a large literature on data structures devoted to this problem. See Cormen, <citation>Leiserson, and Rivest 1990</citation>.

[[5]](37#footnote-link-5) Exercises <ref name="ex:tree-to-list">[2.63](37#ex_2.63)</ref>–<ref name="ex:tree-ops">[2.65](37#ex_2.65)</ref> are due to Paul Hilfinger.

