Radix压缩字典树的原理以及Go语言实现代码

Radix树

Radix树,即基数树,也称压缩字典树,是一种提供key-value存储查找的数据结构。radix tree常用于快速查找的场景中,例如:redis中存储slot对应的key信息、内核中使用radix tree管理数据结构、大多数http的router通过radix管理路由。Radix树在Trie Tree(字典树)的原理上优化过来的。因此在介绍Radix树的特点之首先简单介绍一下Trie Tree。

Trie树

Trie Tree,即字典树。Trie Tree的原理是将每个key拆分成一个个字节,然后对应到每个分支上,分支所在的节点对应为从根节点到当前节点的拼接出的key的值。下图由四个字符串:haha、hehe、god、goto,形成的一个Trie树的示例:
在这里插入图片描述

利用上图中构造的Trie Tree,我们可以很方便的检索一个单词是否出现在原来的字符串集合中。例如,我们检索单词:hehe,那么我们从根节点开始,然后逐个单词进行对比,途径结点:h-e-h-e,就定位到要检索的字符串了。从中可以看出,Trie Tree查找效率是非常高的,达到了O(K),其中K是字符串的长度。

Radix树特点

虽然Trie Tree具有比较高的查询效率,但是从上图可以看到,有许多结点只有一个子结点。这种情况是不必要的,不但影响了查询效率(增加了树的高度),主要是浪费了存储空间。完全可以将这些结点合并为一个结点,这就是Radix树的由来。Radix树将只有一个子节点的中间节点将被压缩,使之具有更加合理的内存使用和查询的效率。下图是采用Radix树后的示例:
在这里插入图片描述

与hash表的对比

  • 查询效率
    Radix树的插入、查询、删除操作的时间复杂度都为O(k)。hash表的安查询效率:hash函数计算效率+O(1),当然与若hash函数若选取不合理,则会造成冲突,会造成hash表的查询效率降低。 总总体上来说两者查询性能基本相等(hash函数的效率基本上等于O(k))。
  • 空间效率
    从空间使用来说,hash占优,因为Radix树的每个节点的节点负载较高,Radix树的结点需要有指向子结点的指针。
  • 其他方面
    hash表的缺点有:数据量增长的情况下可能需要rehash。Radix树不会存在rehash的情况,但是若数据数据密集,则会造成Radix树退化为Trie树,会降低空间使用效率以及查询效率。
  • 取舍建议
    如果数据稀疏,Radix树可以更好的节省空间,这种情况下使用Radix树会更佳。如果数据量小但数据密集,则建议选择hash表。

Radix树的Go语言版本

本文使用Go语言来实现Radix树,目前代码已经上传到github中,下载地址

在该代码中使用二叉树来实现Radix树,相对与四叉树的版本,本版本在存储空间的使用率方面会更好一点,但会牺牲一定的查询效率。
若追求查询效率,请使用多叉树来实现。

使用二叉树来处理字符串时,将字符串看作bit数组,然后逐个bit来对比两这个bit数组,将bit数组前面相同的部分保存到父结点,
将两个bit数组的后面的部分分别保存到left子结点以及right子结点中。例如:

god:  0110-0111  0110-1111  0110-0100
goto: 0110-0111  0110-1111  0111-0100  0110-1111

从上例可以看到,二者在地20位处不相等,因此:

  • 0110-0111 0110-1111 011:被划分到父节点。
  • 0-0100:被划分到left子结点。
  • 1-0100 0110-1111:被划分到right子结点。

定义

首先给出Radix树的定义:

type RaxNode struct {
	bit_len int
	bit_val []byte
	left 	*RaxNode
	right 	*RaxNode
	val interface{}
}
type Radix struct {
	root [256]*RaxNode
}
func NewRaxNode() *RaxNode {
	nd := &RaxNode{}
	nd.bit_len = 0
	nd.bit_val = nil
	nd.left = nil
	nd.right = nil
	nd.val = nil
	return nd
}

将数据添加到Radix树

//添加元素
func (this *Radix)Set(key string, val interface{}) {
	data := []byte(key)
	root := this.root[data[0]]
	if root == nil {
		//没有根节点,则创建一个根节点
		root = NewRaxNode()
		root.val = val
		root.bit_val = make([]byte, len(data))
		copy(root.bit_val, data)
		root.bit_len = len(data) * 8
		this.root[data[0]] = root
		return
	} else if root.val == nil &&  root.left == nil && root.right == nil {
		//只有一个根节点,并且是一个空的根节点,则直接赋值
		root.val = val
		root.bit_val = make([]byte, len(data))
		copy(root.bit_val, data)
		root.bit_len = len(data) * 8
		this.root[data[0]] = root
		return
	}

	cur := root
	blen := len(data) * 8
	for bpos := 0; bpos < blen && cur != nil; {
		bpos, cur = cur.pathSplit(data, bpos, val)
	}
}
func (this *RaxNode)pathSplit(key []byte, key_pos int, val interface{}) (int, *RaxNode) {
	//与path对应的key数据(去掉已经处理的公共字节)
	data := key[key_pos/8:]
	//key以bit为单位长度(包含开始字节的字节内bit位的偏移量)
	bit_end_key := len(data) * 8
	//path以bit为单位长度(包含开始字节的字节内bit位的偏移量)
	bit_end_path := key_pos % 8 + int(this.bit_len)
	//当前的bit偏移量,需要越过开始字节的字节内bit位的偏移量
	bpos := key_pos % 8
	for ; bpos < bit_end_key && bpos < bit_end_path; {
		ci := bpos / 8
		byte_path := this.bit_val[ci]
		byte_data := data[ci]

		//起始字节的内部偏移量
		beg := 0
		if ci == 0 {
			beg = key_pos % 8
		}
		//终止字节的内部偏移量
		end := 8
		if  ci == bit_end_path / 8 {
			end = bit_end_path % 8
			if end == 0 {
				end = 8
			}
		}

		if beg != 0 || end != 8 {
			//不完整字节的比较,若不等则跳出循环
			//若相等,则增长bpos,并继续比较下一个字节
			num := GetPrefixBitLength2(byte_data, byte_path, beg, end)
			bpos += num
			if num < end - beg {
				break
			}
		} else if byte_data != byte_path {
			//完整字节比较,num为相同的bit的个数,bpos增加num后跳出循环
			num := GetPrefixBitLength2(byte_data, byte_path, 0, 8)
			bpos += num
			break
		} else {
			//完整字节比较,相等,则继续比较下一个字节
			bpos += 8
		}
	}

	//当前字节的位置
	char_index := bpos / 8
	//当前字节的bit偏移量
	bit_offset := bpos % 8
	//剩余的path长度
	bit_last_path := bit_end_path - bpos
	//剩余的key长度
	bit_last_data := bit_end_key - bpos

	//key的数据有剩余
	//若path有子结点,则继续处理子结点
	//若path没有子结点,则创建一个key子结点
	var nd_data *RaxNode = nil
	var bval_data byte
	if bit_last_data > 0 {
		//若path有子结点,则退出本函数,并在子结点中进行处理
		byte_data := data[char_index]
		bval_data = GET_BIT(byte_data, bit_offset)
		if bit_last_path == 0 {
			if bval_data == 0 && this.left != nil {
				return key_pos + int(this.bit_len), this.left
			} else if bval_data == 1 && this.right != nil {
				return key_pos + int(this.bit_len), this.right
			}
		}

		//为剩余的key创建子结点
		nd_data = NewRaxNode()
		nd_data.left = nil
		nd_data.right = nil
		nd_data.val = val
		nd_data.bit_len = bit_last_data
		nd_data.bit_val = make([]byte, len(data[char_index:]))
		copy(nd_data.bit_val, data[char_index:])

		//若bit_offset!=0,说明不是完整字节,
		//将字节分裂,并将字节中的非公共部分分离出来,保存到子结点中
		if bit_offset != 0 {
			byte_tmp := CLEAR_BITS_LOW(byte_data, bit_offset)
			nd_data.bit_val[0] = byte_tmp
		}
	}

	//path的数据有剩余
	//创建子节点:nd_path结点
	//并将数据分开,公共部分保存this结点,其他保存到nd_path结点
	var nd_path *RaxNode = nil
	var bval_path byte
	if bit_last_path > 0 {
		byte_path := this.bit_val[char_index]
		bval_path = GET_BIT(byte_path, bit_offset)

		//为剩余的path创建子结点
		nd_path = NewRaxNode()
		nd_path.left = this.left
		nd_path.right = this.right
		nd_path.val = this.val
		nd_path.bit_len = bit_last_path
		nd_path.bit_val = make([]byte, len(this.bit_val[char_index:]))
		copy(nd_path.bit_val, this.bit_val[char_index:])

		//将byte_path字节中的非公共部分分离出来,保存到子结点中
		if bit_offset != 0 {
			byte_tmp := CLEAR_BITS_LOW(byte_path, bit_offset)
			nd_path.bit_val[0] = byte_tmp
		}

		//修改当前结点,作为nd_path结点、nd_data结点的父结点
		//多申请一个子节,用于存储可能出现的不完整字节
		bit_val_old := this.bit_val
		this.left = nil
		this.right = nil
		this.val = nil
		this.bit_len = this.bit_len - bit_last_path //=bpos - (key_pos % 8)
		this.bit_val = make([]byte, len(bit_val_old[0:char_index])+1)
		copy(this.bit_val, bit_val_old[0:char_index])
		this.bit_val = this.bit_val[0:len(this.bit_val)-1]

		//将byte_path字节中的公共部分分离出来,保存到父结点
		if bit_offset != 0 {
			byte_tmp := CLEAR_BITS_HIGH(byte_path, 8-bit_offset)
			this.bit_val = append(this.bit_val, byte_tmp)
		}
	}

	//若path包含key,则将val赋值给this结点
	if bit_last_data == 0 {
		this.val = val
	}
	if nd_data != nil {
		if bval_data == 0 {
			this.left  = nd_data
		} else {
			this.right = nd_data
		}
	}
	if nd_path != nil {
		if bval_path == 0 {
			this.left  = nd_path
		} else {
			this.right = nd_path
		}
	}
	return len(key) * 8, nil
}

Radix树的查询

func (this *Radix)Get(key string) interface{}{
	data := []byte(key)
	blen := len(data) * 8
	cur := this.root[data[0]]

	var iseq bool
	for bpos := 0; bpos < blen && cur != nil; {
		iseq, bpos = cur.pathCompare(data, bpos)
		if iseq == false {
			return nil
		}
		if bpos >= blen {
			return cur.val
		}

		byte_data := data[bpos/8]
		bit_pos := GET_BIT(byte_data, bpos%8)
		if bit_pos == 0 {
			cur = cur.left
		} else {
			cur = cur.right
		}
	}

	return nil
}
func (this *RaxNode)pathCompare(data []byte, bbeg int) (bool, int) {
	bend := bbeg + int(this.bit_len)
	if bend > len(data) * 8 {
		return false, len(data) * 8
	}

	//起始和终止字节的位置
	cbeg := bbeg / 8; cend := bend / 8
	//起始和终止字节的偏移量
	obeg := bbeg % 8; oend := bend % 8
	for bb := bbeg; bb < bend; {
		//获取两个数组的当前字节位置
		dci := bb / 8
		nci := dci - cbeg

		//获取数据的当前字节以及循环步长
		step := 8
		byte_data := data[dci]
		if dci == cbeg && obeg > 0 {
			//清零不完整字节的低位
			byte_data = CLEAR_BITS_LOW(byte_data, obeg)
			step -= obeg
		}
		if dci == cend && oend > 0 {
			//清零不完整字节的高位
			byte_data = CLEAR_BITS_HIGH(byte_data, 8-oend)
			step -= 8-oend
		}

		//获取结点的当前字节,并与数据的当前字节比较
		byte_node := this.bit_val[nci]
		if byte_data != byte_node {
			return false, len(data)*8
		}

		bb += step
	}

	return true, bend
}

Radix树的删除

func (this *Radix)Delete(key string) {
	data := []byte(key)
	blen := len(data) * 8
	cur := this.root[data[0]]

	var iseq bool
	var parent *RaxNode = nil
	for bpos := 0; bpos < blen && cur != nil; {
		iseq, bpos = cur.pathCompare(data, bpos)
		if iseq == false {
			return
		}

		if bpos >= blen {
			//将当前结点修改为空结点
			//若parent是根节点,不能删除
			cur.val = nil
			if parent == nil {
				return
			}

			//当前结点是叶子结点,先将当前结点删除,并将当前结点指向父结点
			if cur.left == nil && cur.right == nil {
				if parent.left == cur {
					parent.left = nil
				} else if parent.right == cur {
					parent.right = nil
				}
				bpos -= int(cur.bit_len)
				cur = parent
			}

			//尝试将当前结点与当前结点的子节点进行合并
			cur.pathMerge(bpos)
			return
		}

		byte_data := data[bpos/8]
		bit_pos := GET_BIT(byte_data, bpos%8)
		if bit_pos == 0 {
			parent = cur
			cur = cur.left
		} else {
			parent = cur
			cur = cur.right
		}
	}
}
func (this *RaxNode)pathMerge(bpos int) bool {
	//若当前结点存在值,则不能合并
	if this.val != nil {
		return false
	}

	//若当前结点有2个子结点,则不能合并
	if this.left != nil && this.right != nil {
		return false
	}

	//若当前结点没有子结点,则不能合并
	if this.left != nil && this.right != nil {
		return false
	}

	//获取当前结点的子结点
	child := this.left
	if this.right != nil {
		child = this.right
	}

	//判断当前结点最后一个字节是否是完整的字节
	//若不是完整字节,需要与子结点的第一个字节进行合并
	if bpos % 8 != 0 {
		char_len := len(this.bit_val)
		char_last := this.bit_val[char_len-1]
		char_0000 := child.bit_val[0]
		child.bit_val = child.bit_val[1:]
		this.bit_val[char_len-1] = char_last | char_0000
	}

	//合并当前结点以及子结点
	this.val = child.val
	this.bit_val = append(this.bit_val, child.bit_val...)
	this.bit_len += child.bit_len
	this.left = child.left
	this.right = child.right

	return true
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

go lang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值