设计一个合理的数据结构,使用尽量少的时间复杂度和空间复杂度,支持一些常见操作。此外,尽可能多的支持扩展操作。
在本项目中,我将实现一个名为LinearTable
的数据结构,来支持一些常见操作。对于最基本的操作,可以使用deque
的实现思路解决,但当需要做到任意中间位置插入元素的时候,大部分线性连接结构的性能将会大幅下降至 O ( n ) O(n) O(n)。因此,本项目会采用更为灵活的树形结构。尽管在做一些首尾操作,任意取值的时候,树形结构难以做到 O ( 1 ) O(1) O(1)复杂度,但 O ( log n ) O(\log n) O(logn)往往也是可以接受的,牺牲部分的维护时间,来换取更为灵活的结构,实现中间位置插入的便捷,是合理的。
项目需要实现一个线性表,表中的元素是具有位置关系的,在用二叉树结构实现时,我们不妨用树中序遍历的顺序来维护元素之间的位置关系。即,在树上做一次中序遍历,就可以得到线性表从头到尾的序列。这样的二叉树就是二叉搜索树。使用二叉搜索树可以便捷的插入、删除、查找、修改元素,但是复杂度取决于树的深度,为了让二叉搜索树保持稳定且高效,应当想办法使其保持平衡。
通过维护一些额外信息,来调整二叉搜索树的构型,使之具有 O ( log n ) O(\log n) O(logn)的深度,这样这棵树就成为了平衡二叉搜索树,其众多操作都将具有 O ( log n ) O(\log n) O(logn)的优秀复杂度。平衡树有很多不同的实现方法,本项目中,我将选择无旋Treap作为LinearTable
的基础结构,在其基础上实现各种常见操作。
无旋Treap是范浩强在研究数据结构时,对Treap的性质进行深究,提出的一种不需要旋转操作的Treap平衡树。
一种将二叉搜索树(tree)和堆(heap)性质结合起来的树形结构。也叫做笛卡尔树。Treap的每个节点都包含至少三个信息,val
表示存储元素的值,rnd
是用于调节树形态的随机因子,loc
表示储存元素在中序遍历中的相对位置。
定义:首先,定义空树是Treap。对于节点 x x x令其左右孩子节点分别为 l , r l,r l,r,那么以 x x x为根的子树是Treap,当且仅当 x x x的左右子树都是Treap,且满足 ∀ u ∈ s u b t r e e ( l ) , ∀ v ∈ s u b t r e e ( r ) \forall u\in subtree(l),\forall v\in subtree(r) ∀u∈subtree(l),∀v∈subtree(r),有 u l o c < x l o c < v l o c u_{loc} < x_{loc} < v_{loc} uloc<xloc<vloc,并且满足 x r n d ≤ l r n d x_{rnd} \leq l_{rnd} xrnd≤lrnd 且 x r n d ≤ r r n d x_{rnd} \leq r_{rnd} xrnd≤rrnd。
rnd
值都不同,Treap的形态是唯一的。证明:首先,只有一个节点或者没有节点的Treap,它们的形态是固定的。
然后,因为rnd
值不同,且Treap的rnd
满足堆性质,根节点必定是rnd
最大的节点。根节点左右两个孩子节点的子树分别代表着中序遍历中,被根节点所储存的元素分隔开的左半区间和右半区间。因此,只要左右子树的形态是唯一的,即可得到该Treap的形态是唯一的。
因此可由数学归纳法证得性质1。
rnd
随机取定的情况下,一棵具有 n n n个节点的Treap的期望深度是 O ( log n ) O(\log n) O(logn)。证明:由rnd
的随机性可知 n n n个点中每个点成为根的概率是相同的,因此可以看作从 n n n个数中随机选择一个作为分割,所以Treap的期望深度等价于随机快速排序的递归深度,相关证明见《算法导论》。
一种维护Treap的方法是和其他平衡树一样,基于旋转。但是由于Treap的特殊性质,可以不使用旋转来维护,取而代之的是分裂与合并。
将当前的Treap分裂成两棵Treap,第一棵Treap包含原树中序遍历的前k个元素,第二棵Treap包含剩下的元素。
仅对按照元素大小维护顺序的Treap有意义,将当前的Treap分裂成两棵Treap,第一棵Treap包含原树小于等于k的所有元素,第二棵Treap包含剩下的元素。
合并两棵Treap,合并之后的Treap的中序遍历等价于先遍历合并前的第一个Treap,再遍历第二个Treap得到的序列。
上述操作的复杂度都是 O ( d e p t h ) O(depth) O(depth)的, d e p t h depth depth表示树的深度。
使用分裂与合并,可以实现很多操作,包括但不限于查找中序遍历中的第k个元素、在指定排名之前插入新的元素、维护和查询某个子树代表的区间的统计信息。
需要实现四个类,分别是node
,Treap
,LinearTable
,Iterator
。
是数据的集合,存储一系列的信息,仅含有用于删除自身及其子孙的递归成员函数:
node *l,*r,*fa;//左右孩子,亲节点。
int rnd,val,max_val,siz;//随机因子,储存值,子树中的最大储存值,子树总大小
void del()// delete the subtree of this
{
if(l != NULL) l->del();
if(r != NULL) r->del();
delete this;
}
是用于管理node的类,仅含有成员root
用来指向树的根节点。该类含有大量的方法,来管理node类的一系列资源。
private:
node *root;
public:
Treap():root(NULL){}
Treap(node *rot):root(rot){}
~Treap(){Clear();}
node * Root()const {return root;}
inline int size()const {return root->siz;}
bool Empty()const {return root->siz==0;}
void Clear(){if(root!=NULL)root->del();root=NULL;}
void ResetRoot(){root=NULL;}
int Rand()const {return (rand()<<16)|(rand());}
void swap(Treap &x);//swap their root
node *New(int v) const;//create a new node with rand 'rnd'
void Pushup(node *x) const; //update size, father, and max_val
void Split_val(node *x,int k,node *&l,node *&r); // split the tree by value
void Split_rank(node *x,int k,node *&l,node *&r); // split the tree by rank
void Merge(Treap &x); //contact x behind this ,and clear x;
node* Merge(node *x,node *y); //merge two treaps and return the root
node* Findkth(node *x,int k);// return the k-th node
node* Insert(int k,int v);// insert a node valued v behind the k-th node
void Insert(int k,Treap &mid);//insert a treap valued v behind the k-th node
node* Sorted_insert(int val);//insert and keep sorted
node* Push_front(int v); //merge a new node in front of the treap
node* Push_back(int v); //merge a new node in back of the treap
bool Delete(int k); //delete the k-th node
void Erase(node *x);//erase subtree of a node
bool Erase_interval(int L,int R); //erase a interval [L,R)
bool Erase_front(int erase_size); //erase some elements from the front
bool Erase_back(int erase_size);//erase some elements from the back
node* Succ(node *x) const; //
node* Pred(node *x) const; //
node* Copy(node *x) const // deep_copy
int Max_interval(int L,int R) //max value of [L,R)
//********************************************* Contructers
Treap(const std::vector<int> &vec);//支持用vector构造一个Treap
template<class Iterator>
Treap(Iterator first, Iterator last);//支持用迭代器构造一个Treap
Treap(int n,int val);//支持构造一个含有n个val的Treap
是LinearTable
的迭代器,支持 O ( log n ) O(\log n) O(logn)的随机访问。
为了方便操作,设计其含有三个私有成员,分别表示其所在的Treap,其所在Treap上的节点,以及该节点的rank。
private:
Treap* _belong;
int _rank;
node* _node;
public:
Iterator();
~Iterator();
Iterator(Treap *belong,int rk,node *nod):_belong(belong),_rank(rk),_node(nod){}
Iterator(const Iterator & it);
Iterator & operator = (const Iterator & it);
inline int rank()const {return _rank;}
inline node* Node()const {return _node;}
inline Treap* belong()const {return _belong;}
void set_rank(int x){_rank=x;}
void set_belong(Treap *x){_belong=x;}
void swap(Iterator &x);//与另一个迭代器发生交换
inline void update_node(node* x);
bool operator <(Iterator x)const {return _rank<x.rank();}
bool operator <=(Iterator x)const {return _rank<=x.rank();}
bool operator >(Iterator x)const {return _rank>x.rank();}
bool operator >=(Iterator x)const {return _rank>=x.rank();}
int& operator [](int x);
int& operator *() const;{return _node->val;}
Iterator operator +(int x);
Iterator operator -(int x);
Iterator operator ++();
Iterator operator ++(int);
Iterator operator --();
Iterator operator --(int);
bool operator ==(Iterator x);
bool operator !=(Iterator x);
int operator -(Iterator x);
是本次PJ的目标,其内部至少包含一个Treap和begin,end两个迭代器,还包含其size。
具有可拓展性,未来可以在内部添加其他的索引来维护额外的拓展操作。
private:
Treap _tree;
Iterator _begin,_end;
int _size;
public:
LinearTable();
LinearTable(const LinearTable &x):_tree(x._tree.Copy(x._tree.Root()));
~LinearTable();
int size()const {return _size;}
bool empty()const {return _size==0;}
Iterator begin()const {return _begin;}
Iterator end()const {return _end;}
void clear();
inline void update_size();
inline void update_begin_end();
void swap(LinearTable &x);
void merge(LinearTable &x);
int front()const;
int back()const;
int& operator [](int k);
bool erase(Iterator it);
bool erase(Iterator first,Iterator last);//区间删除
//****************************************** insert
void insert(Iterator it,int val);
void sorted_insert(int val);//维持有序的插入
void insert(Iterator it,int n,int val);//批量插入
template <class iterator>
void insert(Iterator it,iterator first, iterator last);//批量插入
//****************************************** push & pop
void push_front(int x);
void push_front(int n,int val);//批量插入
template <class iterator>
void push_front(iterator first,iterator last);//批量插入
bool pop_front();
bool pop_front(int pop_size);//批量删除
void push_back(int x);
void push_back(int n,int val);//批量插入
template <class iterator>
void push_back(iterator first,iterator last);//批量插入
bool pop_back();
bool pop_back(int pop_size);//批量删除
//********************************************Extra
int max(Iterator first,Iterator last);//区间最值
int max();
void sort();//排序
void sorted_merge(LinearTable &x);//合并有序表
//******************************************** Constucters
LinearTable(int n,int val);
template<class iterator>
LinearTable(iterator first,iterator end);
LinearTable
基础功能功能 | 原理 | 复杂度 |
---|---|---|
构造与析构 | 构造时初始化好首尾迭代器/析构时清除掉Treap的空间 | O ( 1 ) O(1) O(1)/ O ( n ) O(n) O(n) |
front() | 直接返回begin迭代器所在节点的值 | O ( 1 ) O(1) O(1) |
push_front(int x) | 新建一个节点tmp ,然后Treap上面merge(tmp,root) ,再调整begin迭代器 |
O ( log n ) O(\log n) O(logn) |
pop_front() | Treap分裂出第一个节点然后删除掉,再调整begin迭代器 | O ( log n ) O(\log n) O(logn) |
back() | 返回end迭代器前面的前驱节点的值 | O ( log n ) O(\log n) O(logn) |
push_back(int x) | 把当前end迭代器的值改为x ,然后新建一个节点合并到Treap后面,再调整end迭代器 |
O ( log n ) O(\log n) O(logn) |
pop_back() | 调整end迭代器,Treap分裂出最后一个节点然后删除掉 | O ( log n ) O(\log n) O(logn) |
size() | 直接返回size | O ( 1 ) O(1) O(1) |
empty() | size==0 | O ( 1 ) O(1) O(1) |
swap(LinearTable &B) | 交换Treap的指针,并维护好交换后的begin/end | O ( 1 ) O(1) O(1) |
clear() | 清空所有数据,重新初始化begin/end | O ( n ) O(n) O(n) |
功能 | 原理 | 复杂度 |
---|---|---|
LinearTable(n,val) | 利用单调栈思想线性构造出一棵有n个值为val的节点的Treap | O ( n ) O(n) O(n) |
LinearTable(first,last) | 遍历迭代器,利用单调栈思想线性构造出一棵含有迭代器范围内元素的Treap,支持任何带有自增遍历操作的迭代器。不妨设其遍历一步的均摊复杂度是 O ( k ) O(k) O(k)。 | O ( k n ) O(kn) O(kn) |
iterator ++/– | 在树上寻找前驱后继,单次复杂度 O ( log n ) O(\log n) O(logn),完整遍历均摊复杂度 O ( 1 ) O(1) O(1) | O ( log n ) O(\log n) O(logn) |
iterator +/- | 计算出运算后的迭代器所在节点的排名,直接在Treap上findkth之后,构造一个迭代器返回 | O ( log n ) O(\log n) O(logn) |
push_back(n,val) | 批量添加,利用单调栈思想线性构造出一棵有n个值为val的节点的Treap然后再merge到Treap前面 | O ( n + log n ) O(n+\log n) O(n+logn) |
push_back(first, last) | 与上面类似 | O ( n + log n ) O(n+\log n) O(n+logn) |
push_front(n,val) | 与上面类似 | O ( n + log n ) O(n+\log n) O(n+logn) |
push_front(first, last) | 与上面类似 | O ( n + log n ) O(n+\log n) O(n+logn) |
pop_front/back(size) | 批量删除,直接分裂出将要删除的部分,然后释放掉内存。 | O ( log n ) O(\log n) O(logn) |
insert(iterator,val) | 在iterator处把树分裂开,中间塞一个新节点mid,然后(l,mid,r)三部分合并起来 | O ( log n ) O(\log n) O(logn) |
insert(iterator,n,val) | 先线性构造一棵有n个值为val的节点的Treap然后再执行上一条类似的操作 | O ( n + log n ) O(n+\log n) O(n+logn) |
insert(iterator,first,last) | 与上一条的方法类似,支持任何带有自增遍历操作的迭代器 | O ( n + log n ) O(n+\log n) O(n+logn) |
erase(iterator) | 在iterator处把树分裂成三部分(l,mid,r),mid就是将要删除的节点,删掉之后再merge(l,r) | O ( log n ) O(\log n) O(logn) |
erase(first, last) | 利用[first,last)把树分裂成三部分(l,mid,r),mid就是将要删除的区间,删掉之后再merge(l,r) | O ( log n ) O(\log n) O(logn) |
深/浅拷贝 | 深拷贝需要把Treap所管理的节点完整递归的复制一份。浅拷贝只转移资源的所有者,即调整Treap的root指针。 | O ( n ) / O ( 1 ) O(n)/O(1) O(n)/O(1) |
功能 | 原理 | 复杂度 |
---|---|---|
max(first, last) | 在Treap的每个节点上用max_val 变量维护节点子树的最值,查询区间最值的时候,先把原树分裂成三部分(l,mid,r),其中mid节点的max_val 就代表了[first,last)区间的最值。 |
O ( log n ) O(\log n) O(logn) |
max() | 考虑到Treap中end节点的影响,不能直接返回根的max_val ,因此对上面函数直接调用max(begin,end)。 |
O ( log n ) O(\log n) O(logn) |
sort() | 把每个元素抽离出来调用std::sort,然后线性构造新的Treap | O ( n log n ) O(n\log n) O(nlogn) |
sorted_insert(val) | 通过Treap基于权值的分裂,可以将新值插入到合适的位置 | O ( log n ) O(\log n) O(logn) |
sorted_merge(table) | 合并两个有序线性表,采用遍历归并+线性构造 | O ( n ) O(n) O(n) |
在LinearTable中维护了begin
和end
迭代器,便于使用时通过首尾迭代器遍历所有元素。
同时,使得操作front()变得更加方便,不再需要到树中去查询,可以直接返回值。
随机迭代器可以方便的访问LinearTable的各个元素。而实现随机迭代器的时候,我在迭代器中储存了三个私有成员,分别是belong
,rank
,node
。
belong
用来存迭代器所在的Treap的地址,这样在实现迭代器加法的时候,可以直接计算出加法之后的元素的排名,然后在对应的Treap进行findkth,就可以构造出运算之后的迭代器,实现一个 O ( log n ) O(\log n) O(logn)的随机访问。
rank
用来存迭代器当前访问的元素在Treap里面的排名,这方便了迭代器之间的距离运算,可以实现 O ( 1 ) O(1) O(1)计算距离。
node
用来存迭代器当前访问元素的节点地址,可以通过node直接返回节点储存的元素的引用。
迭代器自增的操作单次是 O ( log n ) O(\log n) O(logn)的,但是遍历所有元素的均摊复杂度是 O ( 1 ) O(1) O(1)的,这是因为遍历过程相当于遍历整棵树。
批量加入,删除元素,如果在平衡树上朴素做,是 O ( n log n ) O(n\log n) O(nlogn)的。但是,在Treap上可以充分运用其可以被单调栈线性构造的特性,实现 O ( n + log n ) O(n+\log n) O(n+logn)的批量插入。2而删除,则可以用分裂操作配合清除函数,实现 O ( log n ) O(\log n) O(logn)。
在模拟单调栈的线性构造过程中,事实上不需要额外的空间,可以直接把树的最右链视为栈,维护一个栈顶指针即可,下面是代码
求取区间最值的朴素方法是枚举区间每一个元素,这样就不可避免的达到 O ( n ) O(n) O(n)的复杂度。如果每个元素的信息仅存在于自身,那么需要查询一个区间的最值信息的时候,无法避免遍历每个元素。
因此,很多数据结构在处理区间问题的时候,或通过维护额外的辅助节点来加速区间查询(如:线段树),或通过节点上的额外空间维护结构中的某些区间信息以加速区间查询(如:平衡树)。但是这些结构都不可避免的需要做到信息的及时转递。当需要修改某个节点的信息的时候,为了保证下次区间最值查询的正确性,必须把所有存有该节点的信息的辅助空间进行更新,才能维护最值信息的正确性。因此,本项目中的LinearTable
也是这样,倘若一个节点的信息被修改了,需要向上更新其所有祖先节点的max_val
信息,需要 O ( log n ) O(\log n) O(logn)的复杂度。
而,在该项目的某个功能实现里,与上面提到的向上更新环节发生了冲突,这就是取元素引用LinearTable[]
功能。该功能的要求是返回元素的左值引用。实现这个功能,只能是在Treap中找到某个节点,然后返回这个节点储存的元素的引用。这是十分危险的,因为在使用中,取得引用之后便可以对其肆意修改,而且修改的过程对LinearTable
来说是不可见的。如果在使用的过程中对LinearTable[i]
返回的引用做了修改操作,那么LinearTable
没有办法立即对修改的值进行向上更新,此时的数据结构中,约有 O ( log n ) O(\log n) O(logn)的节点信息是可能错误的。
这里看来,区间最值维护与元素引用这两个功能之间是存在冲突的。
事实上,是存在避免冲突的办法的:
如果要保证区间最值的正确性,可以让LinearTable[]
返回常引用,再提供modify(loc,val)
函数来进行单个元素的修改,但是这样无法通过onlinejudge上的base_operation测试。
或者,采用延迟更新的策略,在取引用的功能中加入一个标记tag
,记录一下此次取引用可能会修改到哪个元素。在LinearTable接下来的每个操作中,先检测tag
,如果存在tag
,则对上一次可能被修改的元素进行信息的向上更新。这需要进一步的构思和设计,这不仅会增大代码量,还会增加每次操作的负担,降低程序效率。
此项目暂时保留此冲突,未对其进行解决。因此,在一个实例未使用过引用的情况下,本项目的LinearTable
能够进行正确的区间最值查询;在实例使用过引用的情况下,区间最值查询可能出现错误。
测试名称 | 测试内容 | 时间 | 空间 | 提交编号 |
---|---|---|---|---|
Base_Operation | front() &back()/push_front() & push_back()/pop_front() &pop_back()/[]/clear()//swap()/size()/empty() | 2ms | 3MB | ad5cc9320783 |
Base_Operation_0.5 | 同上,数量级1e5 | 54ms | 4MB | 4f91eeedd435 |
Base_Operation_Plus | 同上,数量级1e7 | 6247ms | 8MB | 5f8400ee09ff |
测试名称 | 测试内容 | 时间 | 空间 | 提交编号 | 文件夹 |
---|---|---|---|---|---|
数据结构PJ-中间插值 | 基本操作+insert(it,n,val)+push_back(n,val) 数量级1e6 | 840ms | 37.23MB | R43876751 | midinsert |
数据结构PJ-基本操作 | 基本操作 数量级1e5 | 49ms | 3MB | R43859089 | fundamental |
本次课程项目是一个开放性较强的项目,给出了大量的数据操作。但是某些数据操作往往存在一定的冲突,常常会出现顾此失彼的情况。设计一个数据结构去高效的支持所有的操作,是非常困难的一件事情,因此在设计过程中需要作出取舍。在现实生活中,数据结构往往会有不同的应用场景,在不同的情形下,应当对数据特征和支持的操作进行分析,给出合适的设计,而不是大费周章去设计一个所谓的万能数据结构来支持所有的操作。况且,一种数据结构,支持的操作越多,往往常数越容易增大,代码复杂性越容易上升。
FHQ-Treap学习笔记 - 万万没想到 的博客 ↩︎
题解 P5854 【模板】笛卡尔树的线性构造 ↩︎