数据对象和数据结构
数据对象是一组实例或值,比如布尔值、整型、自然数。
数据对象的一个实例要么是不可再分的原子,要么是另一个数据对象,称呼为元素。
数据结构是一个数据对象,同时这个对象的实例以及构成实例的元素都存在着联系,而且这个联系由函数来规定。
数组描述
STL提供了一个基于数组的类vector。数组的长度是按需要动态增加的,如果实施插入操作时vector已满,那么vector的容量将按照原容量的50%~100%增加。
数据结构最常见的四种操作,分别是:访问、插入、删除、查找。
数组的优势在于访问。
双指针
链式描述
单向链表
每一个节点都明确包含另一个相关节点的位置信息,这个信息被称为链或指针。
每个节点只有一个链被称为单向链表
删除和插入都需要直到目标位置的前一个节点,所以在forward_list中的这两个操作是insert_after(p,t)
和erase_after(p)
。
头节点指的是在链表前面增加一个节点。有了这个节点,空表就不做特殊情况来处理了。这样,每个链表都至少包括了一个节点。
除了单向链表外还有单向循环链表、双向链表与双向循环链表。
链表的优势在于插入和删除。
链表的访问比较复杂,所以没有和数组一样的索引方式,只能通过迭代器或者front、back方法得到元素。
应用
桶排序和基数排序
桶排序和基数排序中的桶的大小未知,所以用链表的话合适一些(虽然vector实现也很方便)。
并查集
等价类
假定一个具有n个元素的集合U=1,2,…,n和一个具有r个关系的集合 R = ( i 1 , j 1 ) , ( i 2 , j 2 ) , . . . , ( i r , j r ) R=(i_1,j_1),(i_2,j_2),...,(i_r,j_r) R=(i1,j1),(i2,j2),...,(ir,jr).
关系R是一个等价关系,当且仅当它具有这些性质:
- 自反性:对于所有的 a ∈ U a\in U a∈U,有 ( a , a ) ∈ R (a,a)\in R (a,a)∈R;
- 对称性: ( a , b ) ∈ R (a,b)\in R (a,b)∈R,当且仅当 ( b , a ) ∈ R (b,a)\in R (b,a)∈R;
- 传递性:若 ( a , b ) ∈ R (a,b)\in R (a,b)∈R且 ( b , c ) ∈ R (b,c)\in R (b,c)∈R,则有 ( a , c ) ∈ R (a,c)\in R (a,c)∈R。
所谓等价类,指的是相互等价的元素的最大集合。等价关系把集合U划分为互不相交的等价类。
在离线等价类问题中,已知n和R,确定所有的等价类。
在在线等价类问题中,初始时有n个元素,每个元素都属于一个独立的等价类。需要执行以下操作:
combine(a,b)
把包含a和b的两个等价类合并成一个等价类/把两个不想交的集合合并为一个集合;find(theElement)
,确定元素theElement在哪一个类,目的是对给定的两个元素,确定是否属于同一个类。。它对同一个类的元素返回相同的结果。
本节主要关注在线等价类的问题,这类问题又称为并查集问题。
并查集
并查集(union-find)是一种数据结构,用于解决元素分组问题,用于管理一系列不相交的集合。支持上述提到的两个操作combine/union和find。
// 基于数组的并查集实现
int* equivClass, // 等价类数组
n; // 元素个数
void initialize(int numberOfElements) {
// 数组的索引是成员的id号,最开始每个成员都自成一个等价类
// 数组中的元素表示该成员所属类的代表成员
n = numberOfElements;
equivClass = new int[n+1]; // 个人感觉没什么意义,大概是为了和链表表示保持统一
for (int e = 1; e <= n; e++)
equivClass[e] = e;
}
void unite(int classA, int classB) {
// 合并类classA和classB
// 假设类classA != classB
// 进行循环,对每一个属于classB的成员改变其类为classA
for (int k = 1; k <= n; k++)
if (equivClass[k] == classB)
equivClass[k] = classA;
}
int find(int theElement) {
// 查找具有成员theElement的类
return equivClass[theElement];
接下来我们介绍用链表表示的并查集。
优势在于如果一个等价类对应一个链表,那么合并操作的时间复杂度就可以降低。因为在一个等价类中,可以沿着链表的指针找到所有的元素,而不必检查所有equivClass的值。
// 链表实现
struct equivNode {
int equivClass, // 元素类标识符
size, // 类的元素个数
next; // 类中指向下一个元素的指针
};
equivNode* node;
int n;
void initialize(int numberOfElements) {
// 用每个类的一个元素,初始化numberOfElements
n = numberOfElements;
node = new equivNode[n+1]; // 需要多一个位置来表示尾节点
for (int e = 1; e <= n; e++) {
node[e].equivClass = e;
node[e].next = 0;
node[e].size = 1;
}
}
void swap(int classA, int classB) {
equivNode temp = node[classA];
node[classA] = node[classB];
node[classB] = temp;
}
void unite(int classA, int classB) {
// 使classA成为较小的类
if (node[classA].size > node[classB].size)
swap(classA, classB);
// 改变较小类中所有元素的equivClass值
int k;
for (k = classA; node[k].next != 0; k = node[k].next)
node[k].equivClass = classB;
node[k].equivClass = classB;
// 在链表classB的首元素之后插入链表classA,就是插入在首元素之后,第二个元素之前
// 修改新链表的大小
node[classB].size += node[classA].size;
node[k].next = node[classB].next;
node[classB].next = classA;
}
int find(int theElement) {
return node[theElement].equivClass;
}