1.先看两个例子
1.1.例一:可查询的定时器
要实现一个定时器,通常会考虑的数据结构是用小顶堆、时间轮或红黑树。其中小顶堆是各定时器实现中最常用的,而本文主要对比讨论的是小顶堆和红黑树。
现在需要一个有一些不一样的定时器,我们给每个定时器定义了一个唯一的id,经常需要查询某一个定时器是否还在队列中,或者查询某一个定时器执行的次数等涉及查询的操作。这个时候小顶堆的查询操作需要O(N)的时间复杂度,而红黑树虽然总体而言更为理想,但到期操作却不如小顶堆。
因此希望能有一个既能在O(1)执行到期任务,又能查找的数据结构。
插入 | 删除 | 到期 | 查找 | |
小顶堆 | O(logN) | O(logN) | O(1) | O(N) |
红黑树 | O(logN) | O(logN) | O(logN) | O(logN) |
1.2.例二:支持热度排序的搜索功能
一般说到热度搜索,会考虑伸展树等数据结构。通常的做法是将搜索热度较高的数据尽可能地放到树高较低的节点上,使得这些数据能够以更少的步骤被搜索到。但伸展树实现复杂,需要讨论的情况较多,并且搜索效率不稳定。因此希望能有一个便于理解和实现、且搜索效率稳定的数据结构。
2.懒驱字典堆(lztrieap)的构成
从实质上看,懒驱字典堆其实就是此前文章中所描述的懒驱树(或者叫懒树、假想树)的一种,这里就不赘述其原理了,尽量缩短本文的长度,大家可以先移步:假想树(或懒树)——一种空间消耗与红黑树相同,但插入、删除和查找的速度更快的树_夏之夜风的博客-CSDN博客
lztrieap是我本人一直在用的一种懒驱树,这几天回头看自己此前写的懒驱树的问题,发现其中没有明确的涉及lztrieap的描述,因此特地写了这篇文章。
2.1.节点构成
lztrieap是基于一个小顶堆构建的懒驱树(大顶堆也一样),每个节点存储4个字段:key、weight、left、right,分别代表键值、权重、左孩子、右孩子。当key值为整型或浮点型等确定比特长度的数据时,处理方式和长度不定的字符串等会略有不同,这里以key值为32位整型进行举例说明。
2.2.插入操作
向懒驱字典堆插入一对key和weight的方法和将key值插入懒驱字典树一样,根据key的二进制值来检索路径。例如插入的值为20230309,其表示二进制为0000 0001 0011 0100 1011 0000 1010 0101。从低位向前看(反过来也行):最低位为1,则代表第一步向右走;次低位位0,则代表第二部向左走;接下来时向右、向左、向左、向右、向左、向右、……当由于懒驱树的特性,不需要走完这32步,在过程中碰到任意一个节点就会停下。
而懒驱字典堆则额外多了一步堆的操作,那就是需要比较途径节点的权值weight,若插入的权值weight值小于节点的weight值,则需要进行交换:swap(插入的key和weight对,节点的key和weight对)。
如此构建出的懒驱字典堆同时具备了字典树和小顶堆的特性,插入、删除、查找的时间复杂度和字典树相同,为O(K)。若key为字符串,则K≤字符串的长度。在当前例子中,key为32位整型,则K≤32。
2.3.其他操作
查找:和字典树的查找一样,通过路径找到目标,时间复杂度O(K)。
删除:和小顶堆的删除一样,找到目标后替换到叶子节点删除,时间复杂度O(K)。
到期:和小顶堆一样,直接取根节点的权值weight,时间复杂度O(1)。
出堆:调用删除,删除根节点,时间复杂度O(K)。
插入 | 删除 | 到期 | 查找 | |
小顶堆 | O(logN) | O(logN) | O(1) | O(N) |
红黑树 | O(logN) | O(logN) | O(logN) | O(logN) |
懒驱字典堆 | O(K) | O(K) | O(1) | O(K) |
3.懒驱字典堆所支持的功能
简单来说,懒驱字典堆就是结合了字典树和小顶堆的功能,进行横向对比后小顶堆、平衡树、懒驱字典堆的所支持的功能如“功能比较”表所示:
(注意,其中的✔和x并不代表该数据结构支持或不支持对应的功能,而是指该数据结构相对于表中其他的数据结构而言是否具备优势。例如平衡树也可以取出最大最小值,但查找最小值的时间复杂度O(logN)较小顶堆的O(1)会慢;例如小顶堆也可以支持查找功能,但查找对应值需要对节点进行遍历,时间复杂度O(N)较红黑树的O(logN)慢。)
find | min or max | topk | range | 前缀或后缀查找 | |
小顶堆 | × | ✔ | ✔ | × | × |
红黑树 | ✔ | × | ✔ | ✔ | × |
懒驱字典堆 | ✔ | ✔ | ✔ | × | ✔ |
4.性能对比
生成百万个随机数进行insert(或push),然后erase其中的五分之一(或全部pop出来),以及find其中的所有值。记录各操作所需的时间,结果如“性能比较”表所示(小顶堆和红黑树在代码执行中选取的是c++标准库stl中提供的容器priority_queue和map):
insert | erase | find | push | pop | min or max | |
priority_queue | × | × | × | 16ms | 16ms | 0ms |
map | 891ms | 94ms | 16ms | × | × | × |
lztrieap | 295ms | 42ms | 2ms | 156ms | 172ms | 0ms |
lztrieap(配内存池) | 78ms | 16ms | 2ms | 31ms | 46ms | 0ms |
由于在比较过程中,发现懒驱字典堆的性能和小顶堆相差较大,但按理来说两者的操作相近,不应该会相差太多。后来发现主要的性能都在申请和释放内存上了,而priority_queue则是直接申请的一块大内存,因此写了一个内存池进行内存管理。
插入、删除和搜索速度:未配备内存池的懒驱字典堆插入和删除性能和map相近,查找性能优于map;而配了内存池的懒驱字典堆插入、删除和操作速度都优于map。
入堆、出堆的速度:小顶堆的性能远高于未配备内存池的懒驱字典堆。在给懒驱字典堆配备了内存池后,虽然依旧慢于小顶堆,但来到了同一数量级上。两者所相差的效率依旧是在于内存分配上,这里用的内存池还是比较保守的,速度理论上还能再提升。
取最大最小值的速度:因为都是直接读取根节点的值,因此小顶堆和懒驱字典树的速度相同。
5.回归例子
例一所描述的定时器其实已经解决的了,因为懒驱字典堆拥有小顶堆的一切特性,能够实现定时器的功能,且还能高效地进行检索功能。
例二所描述的支持热度排序的的搜索功能同样也能用懒驱字典堆来实现。此时字典堆的权值weight就代表了搜索热度,初始插入时全部初始化为0。每检索一次就将权值weight加一,并和父节点进行比较,若权值大于父节点则进行如下操作:
1.若判断父节点中key下一步的方向就是当前节点的方向,则直接交换两个节点的key和weight
2.否则需要进行懒驱树的驱赶操作,将当前节点的值写入父节点,驱赶父节点中的key和weight
当然也可以继续再递归和原本的爷爷节点比较权值,但没有这个必要,因为爷爷weight大概率比父亲的weight大,并且如果真的热度高自然会继续上位。
6.源码
这里提供的是以整型或浮点为key值的lztrieap源码:
/*
wangxin 20230308
这里实现的是一个小顶堆,大顶堆也不用另外再实现,输入输出值全部用值域范围内的最大值去减一下就是了
*/
#ifndef __lztrieapInt_h__
#define __lztrieapInt_h__
#include "heapmemory.h"
template <class KeyInt, class WeightType>
class lztrieapInt
{
struct tree_node
{
KeyInt key;
WeightType weight;
tree_node *left;
tree_node *right;
tree_node()
:key(0)
, weight()
, left(nullptr)
, right(nullptr)
{
}
};
public:
lztrieapInt()
:m_keybits(sizeof(KeyInt) << 3)
, m_root(nullptr)
, m_size(0)
, m_hmnodes()
{
}
~lztrieapInt()
{
clear();
}
void insert(KeyInt key, WeightType weight)
{
unsigned long step = 0;
tree_node **pnode = &m_root;
for (unsigned long i = 0; i < m_keybits; ++i)
{
if (*pnode == nullptr)
{
*pnode = m_hmnodes.create();
(*pnode)->key = key;
(*pnode)->weight = weight;
++m_size;
return;
}
else
{
if (key == (*pnode)->key)
{
(*pnode)->weight = weight;
return;
}
if (weight < (*pnode)->weight)
{
std::swap(key, (*pnode)->key);
std::swap(weight, (*pnode)->weight);
}
if ((key >> step) & 1)
{
pnode = &((*pnode)->right);
}
else //key > value
{
pnode = &((*pnode)->left);
}
}
++step;
}
}
void pop()
{
erase_node(&m_root);
}
bool top(WeightType &weight)
{
if (m_root == nullptr) return false;
weight = m_root->weight;
return true;
}
WeightType top()
{
if (m_root == nullptr) return 0;
return m_root->weight;
}
void erase(KeyInt key)
{
tree_node **pnode = find_pnode(key);
erase_node(pnode);
}
bool exists(KeyInt key)
{
tree_node **pnode = find_pnode(key);
return pnode && *pnode;
}
unsigned long size()
{
return m_size;
}
void clear()
{
remove(&m_root);
m_root = nullptr;
m_size = 0;
}
private:
inline tree_node **find_pnode(KeyInt key)
{
unsigned long step = 0;
tree_node **pnode = &m_root;
for (unsigned long i = 0; i < m_keybits; ++i)
{
if (*pnode == nullptr) return nullptr;
else
{
if (key == (*pnode)->key) return pnode;
if ((key >> step) & 1) pnode = &(*pnode)->right;
else pnode = &(*pnode)->left;
}
++step;
}
return nullptr;
}
inline void erase_node(tree_node **pnode)
{
if (pnode == nullptr || *pnode == nullptr) return;
//继续向下遍历,不断地将孩子中值较小的节点交换上来
tree_node **pchild = get_min_pchild(*pnode);
while (pchild && *pchild)
{
std::swap((*pnode)->key, (*pchild)->key);
std::swap((*pnode)->weight, (*pchild)->weight);
pnode = pchild;
pchild = get_min_pchild(*pnode);
}
tree_node *node = *pnode;
*pnode = nullptr;
m_hmnodes.destory(node);
--m_size;
}
inline void remove(tree_node **pnode)
{
if (pnode)
{
tree_node *node = *pnode;
if (node)
{
*pnode = nullptr;
if (node->left)
{
remove(&node->left);
}
if (node->right)
{
remove(&node->right);
}
m_hmnodes.destory(node);
}
}
}
inline tree_node **get_min_pchild(tree_node *node)
{
if (node->left && node->right)
return node->left->key < node->right->key ? &node->left : &node->right;
else if (node->left)
return &node->left;
else if (node->right)
return &node->right;
else
return nullptr;
}
//获取最高位的1的位置
inline unsigned long highest_digit_1(KeyInt v)
{
if (m_keybits == 64) return highest_digit_1_64bit(v);
else return highest_digit_1_32bit(v);
}
inline unsigned long highest_digit_1_32bit(KeyInt v)
{
#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const char log_table_256[256] =
{
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
LT(5), LT(6), LT(6), LT(7), LT(7), LT(7), LT(7),
LT(8), LT(8), LT(8), LT(8), LT(8), LT(8), LT(8), LT(8)
};
unsigned r;// r will be lg(v)
unsigned long t, tt;// temporaries
if (tt = v >> 16)
{
r = (t = tt >> 8) ? 24 + log_table_256[t] : 16 + log_table_256[tt];
}
else
{
r = (t = v >> 8) ? 8 + log_table_256[t] : log_table_256[v];
}
return r;
}
inline unsigned long highest_digit_1_64bit(KeyInt v)
{
#define LT(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n
static const char log_table_256[256] =
{
0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4,
LT(5), LT(6), LT(6), LT(7), LT(7), LT(7), LT(7),
LT(8), LT(8), LT(8), LT(8), LT(8), LT(8), LT(8), LT(8)
};
unsigned r;// r will be lg(v)
unsigned long t, tt, ttt;// temporaries
if (ttt = v >> 32)
{
if (tt = ttt >> 16)
{
r = (t = tt >> 8) ? 56 + log_table_256[t] : 48 + log_table_256[tt];
}
else
{
r = (t = ttt >> 8) ? 40 + log_table_256[t] : 32 + log_table_256[ttt];
}
}
else
{
if (tt = v >> 16)
{
r = (t = tt >> 8) ? 24 + log_table_256[t] : 16 + log_table_256[tt];
}
else
{
r = (t = v >> 8) ? 8 + log_table_256[t] : log_table_256[v];
}
}
return r;
}
private:
unsigned long m_keybits;
tree_node *m_root;
unsigned long m_size;
heapmemory<tree_node> m_hmnodes;
};
#endif //__lztrieapInt_h__
7.附加的内存池代码
这个内存池是随便写的,主要是用于测试:
/*
wangxin 20230803
固定类型的内存池
*/
#ifndef __heapmemory_h__
#define __heapmemory_h__
#include <queue>
template <class _Type>
class heapmemory
{
struct node
{
_Type data;
node *next = this + 1;
};
public:
heapmemory(unsigned int uCellSize = 1024)
:m_uCellSize(uCellSize)
, m_queueCells()
, m_pFront(nullptr)
, m_pBack(nullptr)
{
}
~heapmemory()
{
while (!m_queueCells.empty())
{
delete[] m_queueCells.front();
m_queueCells.pop();
}
}
_Type *create()
{
if (++m_uUsedCount > m_uTotalCount)
{
m_uTotalCount += m_uCellSize;
node *pCell = new node[m_uCellSize];
m_queueCells.push(pCell);
if (m_pFront == nullptr) m_pFront = pCell;
if (m_pBack) m_pBack->next = pCell;
m_pBack = pCell + m_uCellSize - 1;
m_pBack->next = nullptr;
m_uCellSize <<= 1;
}
node *pNew = m_pFront;
m_pFront = m_pFront->next;
return (_Type*)pNew;//因为_Type data是结构体node的第一个元素
}
void destory(_Type *p)
{
--m_uUsedCount;
node *pFree = (node*)p;//因为_Type data是结构体node的第一个元素s
pFree->next = m_pFront;
m_pFront = pFree;
}
private:
unsigned int m_uCellSize;
std::queue<node*> m_queueCells;
node *m_pFront;
node *m_pBack;
unsigned int m_uUsedCount;
unsigned int m_uTotalCount;
};
#endif //__heapmemory_h__