mirror of
https://gitee.com/Lamdonn/varch.git
synced 2025-12-08 01:36:42 +08:00
255 lines
9.3 KiB
Markdown
255 lines
9.3 KiB
Markdown
## 介绍
|
||
|
||
set集合是逻辑上离散的容器,与vector、list容器不太相同,set的访问索引是离散的并不一定是来连续的,在插入的时候就进行了排序和索引去重。
|
||
varch的set容器,也是具备的集合的基本属性,在底层实现上采用了**红黑树**,增删操作的效率比vector的高,而且也支持随机访问,随机访问的时间复杂度比list要好很多。同样作为链式结构,set集合也支持迭代器。
|
||
|
||
## 接口
|
||
|
||
### 创建和删除set对象
|
||
```c
|
||
set_t set_create(int dsize);
|
||
void set_delete(set_t set);
|
||
#define set(type) // 为了更简便的使用,对set_create套一层宏定义
|
||
#define _set(set) // 对set_delete套一层宏定义,并在set删除后置为空
|
||
```
|
||
其中**set_t**为set的结构体,创建方法则会返回一个空的set对象,创建失败则返回NULL,其中`dsize`传入数据的大小。删除方法则是删除传入的set对象。创建方法和删除应该成对使用,创建出来在结束使用应该删除掉。
|
||
```c
|
||
void test(void)
|
||
{
|
||
set_t set = set(int); // 定义并创建一个int型的set
|
||
_set(set); // 成对使用,用完即删除
|
||
}
|
||
```
|
||
|
||
### set的插入和移除
|
||
```c
|
||
void* set_insert(set_t set, int index, void* data);
|
||
int set_erase(set_t set, int index);
|
||
```
|
||
set有着较好插入和移除的效率,不用数据移位只需修改链表指向。
|
||
插入的方法是添加指定index并将数据复制到这个索引(在其中,data传入NULL时则只是开辟空间,不进行赋值),在插入index的过程中会进行查重,保证index的唯一性,插入成功后返回插入后的数据的地址,插入失败则是返回NULL。而移除则是移除指定索引的数据,成功返回1,失败返回0。
|
||
|
||
### set数据的读写
|
||
```c
|
||
void* set_data(set_t set, int index);
|
||
void* set_error(set_t set);
|
||
#define set_at(set, type, i)
|
||
```
|
||
`set_data`方法就是根据索引来获取数据的地址,返回的则是指定的数据的地址,`set_error()`则是失败。而`set_at`则是在`set_data`的基础上加多类型,`set_data`具备读写保护机制,因为返回的是`set_error()`而不是NULL,所以在使用`set_at`方法`i`写错了就会修改`set_error()`指向的内容,而不会导致奔溃。
|
||
set的随机访问和连续地址的数组或者非连续地址的list都不太一样,数组是可以直接定位到指定索引的地址,而链表要链式一步步指向进行随机访问。set采用了**红黑树**,二分查找就可以很快的访问到指定索引。
|
||
|
||
```c
|
||
void test(void)
|
||
{
|
||
set_t set = set(int);
|
||
int i;
|
||
|
||
for (i = 0; i < 100; i++)
|
||
{
|
||
set_insert(set, i, &i);
|
||
}
|
||
i = -100; set_insert(set, i, &i);
|
||
i = 1024; set_insert(set, i, &i);
|
||
|
||
printf("set[6] = %d\r\n", set_at(set, int, 6));
|
||
printf("set[-100] = %d\r\n", set_at(set, int, -100));
|
||
printf("set[1024] = %d\r\n", set_at(set, int, 1024));
|
||
|
||
set_at(set, int, 6) = 11111;
|
||
printf("set[6] = %d\r\n", set_at(set, int, 6));
|
||
|
||
_set(set);
|
||
}
|
||
```
|
||
结果:
|
||
```
|
||
set[6] = 6
|
||
set[-100] = -100
|
||
set[1024] = 1024
|
||
set[6] = 11111
|
||
```
|
||
|
||
### set的大小和和数据大小
|
||
```c
|
||
int set_size(set_t set);
|
||
int set_dsize(set_t set);
|
||
```
|
||
set的`size`很好理解,也就是像数组那样的大小,`dsize`也就是创建时候传入的数据的大小。
|
||
```c
|
||
void test(void)
|
||
{
|
||
set_t set = set(int);
|
||
int i;
|
||
|
||
for (i = 0; i < 100; i++)
|
||
{
|
||
set_insert(set, i, &i);
|
||
}
|
||
printf("size = %d, data size = %d\r\n", set_size(set), set_dsize(set));
|
||
|
||
_set(set);
|
||
}
|
||
```
|
||
结果:
|
||
```
|
||
size = 100, data size = 4
|
||
```
|
||
|
||
### set查找
|
||
```c
|
||
int set_find(set_t set, int index);
|
||
```
|
||
这个方法其实套`set_data`实现,只是find成功返回1失败返回0。
|
||
|
||
### set迭代器
|
||
|
||
```c
|
||
void set_it_init(set_t set, int orgin);
|
||
void* set_it_get(set_t set, int *out_index);
|
||
```
|
||
|
||
set也支持内置的迭代器,但主要set的迭代器用于遍历。因为向list要遍历的时候是知道索引从0开始逐一递增遍历的。但是set是离散型的index,无法通过这种逐一递增的方式进行遍历,所以这里给定了两个迭代器函数用于遍历set。
|
||
`set_it_init`初始化迭代器,`orgin`指定为`SET_HEAD`或者`SET_TAIL`,就分别是正向迭代和反向迭代。
|
||
`set_it_get`获取迭代,更新迭代位置,`*out_index`为输出的index(当前所在的index,也可以传入NULL不接收),返回迭代位置的数据。
|
||
通过`set_size`来把控迭代次数。
|
||
|
||
```c
|
||
void test(void)
|
||
{
|
||
set_t set = set(int);
|
||
int i, index;
|
||
void *data;
|
||
|
||
i = -100; set_insert(set, i, &i);
|
||
i = 1024; set_insert(set, i, &i);
|
||
i = 0; set_insert(set, i, &i);
|
||
i = 7; set_insert(set, i, &i);
|
||
i = -2; set_insert(set, i, &i);
|
||
i = -2; set_insert(set, i, &i);
|
||
|
||
set_at(set, int, 3) = 100;
|
||
|
||
set_it_init(set, SET_HEAD);
|
||
i = set_size(set);
|
||
while (i--)
|
||
{
|
||
data = set_it_get(set, &index);
|
||
printf("set[%d] = %d\r\n", index, *(int *)data);
|
||
}
|
||
|
||
_set(set);
|
||
}
|
||
```
|
||
|
||
结果:
|
||
```
|
||
set[-100] = -100
|
||
set[-2] = -2
|
||
set[0] = 0
|
||
set[7] = 7
|
||
set[1024] = 1024
|
||
```
|
||
|
||
## 源码解析
|
||
|
||
### set结构体
|
||
|
||
set容器的所有结构体都是隐式的,也就是不能直接访问到结构体成员的,这样子的方式保证了模块的独立与安全,防止外部调用修改结构体的成员导致set存储结构的破坏。所以set解析器只留了唯一一个set的声明在头文件,然后结构体的定义都在源文件。只能使用set容器提供的方法对set对象进行操作。
|
||
set类型声明
|
||
```c
|
||
typedef struct SET *set_t;
|
||
```
|
||
使用时候,只是用`set_t`即可。
|
||
|
||
```c
|
||
/* type of set */
|
||
typedef struct SET
|
||
{
|
||
NODE* root; /* root node */
|
||
NODE* nil; /* nil node */
|
||
NODE* iterator; /* iterator of set */
|
||
int orgin; /* iterator orgin */
|
||
int size; /* set size */
|
||
int dsize; /* data size */
|
||
} SET;
|
||
```
|
||
`SET`结构体中包含了6个成员,root(红黑树根节点),nil(红黑树nil结点),iterator(迭代器当前指向的结点),orgin(迭代器的起始位置),size(set的大小,也就是set红黑树的结点总数),dsize(每个数据的大小)。
|
||
```c
|
||
/* set node type define */
|
||
typedef struct NODE
|
||
{
|
||
struct NODE *parent;
|
||
struct NODE *left;
|
||
struct NODE *right;
|
||
int color;
|
||
int index;
|
||
} NODE;
|
||
#define data(node) ((node)+1) /* data of node */
|
||
```
|
||
在`NODE`结构体当中,显式的成员包含形成树结构的`parent`、`left`、`right`指向结点,红黑树的结点颜色`color`,索引`index`。数据域就和`list`的数据域一样,跟在node空间的后面,动态分配大小。
|
||
|
||
### 红黑树
|
||
|
||
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。
|
||
set的存储结构就完全是红黑树,这里不细说红黑树了。
|
||
|
||
### 迭代器
|
||
|
||
前面说到`set`内置了迭代器,那这个迭代器是怎么回事呢?
|
||
看下源码:
|
||
```c
|
||
void set_it_init(set_t set, int orgin)
|
||
{
|
||
if (!set) return;
|
||
set->orgin = (orgin==SET_HEAD)?SET_HEAD:SET_TAIL;
|
||
set->iterator = (set->orgin==SET_HEAD)?(NODE*)node_min(set, set->root):(NODE*)node_max(set, set->root); // 根据起始点,将迭代器迭代到根节点的最小结点或者最大结点
|
||
}
|
||
void* set_it_get(set_t set, int *out_index)
|
||
{
|
||
NODE *node;
|
||
if (!set) return NULL;
|
||
node = set->iterator;
|
||
set->iterator = (set->orgin==SET_HEAD)?node_next(set, set->iterator):node_prev(set, set->iterator); // 根据迭代方向,选择往下一个迭代还是上一个迭代
|
||
if (out_index) *out_index = node->index; // 输出迭代的index
|
||
return data(node);
|
||
}
|
||
```
|
||
而在红黑树中,node的prev和next是怎么实现的呢?
|
||
红黑树中,当前结点比其左节点的所有结点都大,比右节点的任意结点都小
|
||
所以在获取下一个结点时候,就是找比当前结点大的最小的结点,那么寻找的优先级就有如下:
|
||
1、右分支的结点,而在看右分支的结点的时候,右分支的最小结点就在右分支的最左边
|
||
2、父节点,看父节点也要区分当前结点是父节点的左结点还是右节点,要是父节点的左节点的话,那父节点就比当前结点大,就直接返回父节点;要是父节点的右结点的话,那父节点是比当前结点要小的,所以要返回"爷结点"才比当前结点大并且是最小的。
|
||
同理,反过来就是prev的逻辑
|
||
怎么结束迭代呢?就是通过set的size控制迭代的次数就行了,一直迭代下去的话,最终就是一直在nil的位置。
|
||
```c
|
||
static NODE* node_next(set_t set, NODE* node)
|
||
{
|
||
if (node->right != set->nil) // 有右分支
|
||
{
|
||
node = node->right; // 先到右节点
|
||
node = node_min(set, node); // 再到右分支的最左边(最小结点)
|
||
}
|
||
else // 没有右分支
|
||
{
|
||
if (node == node->parent->left) node = node->parent; // 当前结点是父结点的节点,返回父节点就OK了
|
||
else node = node->parent->parent; // 当前结点是父节点的右结点,就需要返回“爷结点”
|
||
}
|
||
return node;
|
||
}
|
||
|
||
static NODE* node_prev(set_t set, NODE* node)
|
||
{
|
||
if (node->left != set->nil)
|
||
{
|
||
node = node->left;
|
||
node = node_max(set, node);
|
||
}
|
||
else
|
||
{
|
||
if (node == node->parent->right) node = node->parent;
|
||
else node = node->parent->parent;
|
||
}
|
||
return node;
|
||
}
|
||
```
|