数据结构是一个容器,用于将一些数据组织到单个对象中。我们已经见过了几个数据结构,比如apstring是一些字符组成,而apvector是一组相同类型(可以是任意数据类型)的元素组成。
有序集是由一些项组成的集合,它有两个决定性的属性:
**有序性**:集合中的元素都有一个相应的索引。我们可以通过这些索引确定集合中的元素。
**唯一性**:集合中每个元素只能出现一次。向集合中添加一个已经存在的元素是没有效果的。
此外,我们实现的有序集还有下面一个属性:
**大小任意**:随着我们向集合中添加元素,它会扩充以容纳新元素。apstring和apvector都是有序的;每个元素都有一个索引,我们可以通过索引来确定元素。但是我们见到的数据结构都不具有唯一性和大小任意这两个属性。
要满足唯一性,我们编写的add函数必须先查找以确定集合中是否存在要添加的元素。集合随着添加元素扩张这一特点可以利用apvector的resize函数实现。
下面是Set类定义的开始部分:
~~~
class Set {
private:
apvector<apstring> elements;
int numElements;
public:
Set (int n);
int getNumElements () const;
apstring getElement (int i) const;
int find (const apstring& s) const;
int add (const apstring& s);
};
Set::Set (int n)
{
apvector<apstring> temp (n);
elements = temp;
numElements = 0;
}
~~~
实例变量包括字符串的向量和记录集合中有多少元素的整型数。一定要记住集合中的元素数numElement与apvector的大小不是一个东西。通常前者会小一些。
Set的构造函数接受一个参数,该参数是apvector的初始大小。元素个数初始值总是0。
getNumElements和getElement是私有实例变量的访问函数。 numElements是只读变量,所以我们只提供了get函数而没有提供set函数。
~~~
int Set::getNumElements () const
{
return numElements;
}
~~~
为什么我们必须阻止客户程序修改getNumElements呢? 因为这是该类型的不变式,客户程序怎么能破坏不变式呢。我们看下Set其余的成员函数,看你能否说服自己它们都维护了不变式。
当我们使用[]操作符访问apvector时,它会检查并确认索引值大于等于0且小于向量的长度。不过要访问集合的元素,我们需要更强的条件验证。index必须小于元素数,元素数可能是个比向量长度小的值。
~~~
apstring Set::getElement (int i) const
{
if (i < numElements) {
return elements[i];
} else {
cout << "Set index out of range." << endl;
exit (1);
}
}
~~~
如果getElement得到的索引值超出了范围,它会打印错误信息(我承认,并不是最有用的信息)后退出。 find和add是比较有趣的函数。到目前为止,遍历和查找的模式还是老样子:
~~~
int Set::find (const apstring& s) const
{
for (int i=0; i<numElements; i++) {
if (elements[i] == s) return i;
}
return -1;
}
~~~
现在就剩下add了。像add这样的函数的返回类型一般是void,不过在这个例子中,更有意义的可能是返回元素的索引。
~~~
int Set::add (const apstring& s)
{
// 如果元素已经在集合中,返回其索引
int index = find (s);
if (index != -1) return index;
// 如果apvector满了,将它的大小调整为原来的2倍
if (numElements == elements.length()) {
elements.resize (elements.length() * 2);
}
// 添加新元素并返回其索引
index = numElements;
elements[index] = s;
numElements++;
return index;
}
~~~
这里有个技巧,numElements会以两种方式使用:其一就是表示集合中的元素数目,其二是用作下一个要添加的元素的索引。
可能要花点时间才能相信这行得通,但是考虑一下:当元素数目为0时,下一个要加入元素的索引也是0。当元素数目等于向量的长度时,这就说明向量已经满了,要加入新元素必须先通过resize分配更多的空间。
下面是一个Set对象的状态图,该对象初始包含2个元素的空间: ![enter image description here](https://box.kancloud.cn/2015-09-02_55e688db32326.jpg)
现在我们可以使用Set类来记录在文件中找到的城市。在main函数中我们以2为初始大小创建Set对象:
~~~
Set cities (2);
~~~
然后在processLine函数中我们把两个城市添加到Set中,并保存返回的索引值。
~~~
int index1 = cities.add (city1);
int index2 = cities.add (city2);
~~~
我修改了processLine函数,使它以城市对象为第二个参数。
- 第1章 编程之路
- 1.1 什么是编程语言
- 1.2 什么是程序
- 1.3 什么是调试
- 1.4 形式语言与自然语言
- 1.5 第一个程序
- 1.6 术语表
- 第2章 变量和类型
- 2.1 更多的输出
- 2.2 值
- 2.3 变量
- 2.4 赋值
- 2.5 输出变量
- 2.6 关键字
- 2.7 操作符
- 2.8 操作顺序
- 2.9 操作符
- 2.10 组合
- 2.11 术语表
- 第3章 函数
- 3.1 浮点数
- 3.2 double到int的转换
- 3.3 数学函数
- 3.4 函数组合
- 3.5 添加新函数
- 3.6 定义与使用
- 3.7 多函数编程
- 3.8 参数与参数值
- 3.9 参数和变量的局部性
- 3.10 多参函数
- 3.11 有返回值的函数
- 3.12 术语表
- 第4章 条件和递归
- 4.1 取模操作符
- 4.2 条件执行
- 4.3 选择执行
- 4.4 链式条件
- 4.5 嵌套条件
- 4.6 return语句
- 4.7 递归
- 4.8 无穷递归
- 4.9 递归函数的栈图
- 4.10 术语表
- 第5章 有返回值的函数
- 5.1 返回值
- 5.2 程序开发
- 5.3 组合
- 5.4 重载
- 5.5 布尔值
- 5.6 布尔变量
- 5.7 逻辑操作符
- 5.8 布尔函数
- 5.9 从main函数返回
- 5.10 深入递归
- 5.11 思路跳跃
- 5.12 又一个例子
- 5.13 术语表
- 第6章 迭代
- 6.1 多次赋值
- 6.2 迭代
- 6.3 while语句
- 6.4 制表
- 6.5 二维表
- 6.6 封装和泛化
- 6.7 函数
- 6.8 再说封装
- 6.9 局部变量
- 6.10 再说泛化
- 6.11 术语表
- 第7章 字符串那些事儿
- 7.1 字符串的容器
- 7.2 apstring变量
- 7.3 从字符串中提取字符
- 7.4 字符串长度
- 7.5 遍历
- 7.6 一个运行时错误
- 7.7 find函数
- 7.8 我们自己的find版本
- 7.9 循环与计数
- 7.10 增量与减量操作符
- 7.11 字符串连接
- 7.12 apstring是可变的
- 7.13 apstring是可比较的
- 7.14 字符分类
- 7.15 其他apstring函数
- 7.16 术语表
- 第8章 结构体
- 8.1 复合值
- 8.2 Point对象
- 8.3 访问实例变量
- 8.4 对结构体的操作
- 8.5 作为参数的结构
- 8.6 传值调用
- 8.7 传引用调用
- 8.8 矩形
- 8.9 作为返回值的结构
- 8.10 按引用传递其他类型
- 8.11 获取用户输入
- 8.12 术语表
- 第9章 再谈结构体
- 9.1 Time结构体
- 9.2 printTime函数
- 9.3 对象函数
- 9.4 纯函数
- 9.5 const参数
- 9.6 修改函数
- 9.7 填充函数
- 9.8 哪个最佳?
- 9.9 增量开发vs高屋建瓴
- 9.10 泛化
- 9.11 算法
- 9.12 术语表
- 第10章 向量
- 10.1 元素访问
- 10.2 向量的复制
- 10.3 for循环
- 10.4 向量的长度
- 10.5 随机数
- 10.6 统计
- 10.7 随机数的向量
- 10.8 计数
- 10.9 检查其他值
- 10.10直方图
- 10.11一次遍历的方案
- 10.12随机种子
- 10.13术语表
- 第11章 成员函数
- 11.1 对象和函数
- 11.2 print
- 11.3 隐式变量访问
- 11.4 另一个例子
- 11.5 再一个例子
- 11.6 更复杂的例子
- 11.8 初始化还是构造?
- 11.7 构造函数
- 11.9 最后一个例子
- 11.10 头文件
- 11.11 术语表
- 第12章 对象的向量
- 12.1 组合
- 12.2 纸牌对象(Card)
- 12.3 printCard函数
- 12.4 equals函数
- 12.5 isGreater函数
- 12.6 纸牌的向量
- 12.7 printDeck函数
- 12.8 查找
- 12.9 二分查找
- 12.10 牌堆与子牌堆
- 12.11 术语表
- 第13章 基于向量的对象
- 13.1 枚举类型
- 13.2 switch语句
- 13.3 牌堆
- 13.4 另一个构造函数
- 13.5 Deck成员函数
- 13.6 洗牌
- 13.7 排序
- 13.8 子牌堆
- 13.9 洗牌与发牌
- 13.10 归并排序
- 13.11 术语表
- 第14章 类与不变式
- 14.1 私有数据和私有类
- 14.2 什么是类?
- 14.3 复数
- 14.4 访问函数(Accessor functions)
- 14.5 输出
- 14.6 复数相关函数(一)
- 14.7 复数相关函数(二)
- 14.8 不变式
- 14.9 先决条件
- 14.10 私有函数
- 14.11 术语表
- 第15章 文件输入/输出与apmatrix类
- 15.1 流
- 15.2 文件输入
- 15.3 文件输出
- 15.4 解析输入
- 15.5 解析数字
- 15.6 集合数据结构Set
- 15.7 apmatrix类
- 15.8 距离矩阵
- 15.9 一个更合理的距离矩阵
- 15.10 术语表