goSTL/blog/数据结构STL——golang实现位图bitmap.md

10 KiB
Raw Blame History

github仓库存储地址https://github.com/hlccd/goSTL

概述

Bit-map的基本思想就是用一个bit位来标记某个元素对应的Value而Key即是该元素。由于采用了Bit为单位来存储数据因此可以极大的节省存储空间

原理

对于计算机来说,它可以分配的最小单位是一个字节即8位,一位有且仅有1和0两种表示可能。而如果要表示0~7八个数字在某一个集合内是否存在比如表示{035}在集合中是否存在可以使用一个字节进行存储即对应位为1的表示存在为0的表示不存在

1 0 0 1 0 1 0 0

而如果要表示超过8个的元素是否存在的话仅仅使用一个字节是远远不够的。

为了解决这个问题,可以选择使用包含更多字节的基本元素,比如uint64选用uint64而不是int64的原因是因为在计算机内部存储数字采用补码的形式对于一个数字如果不是非负整数的话它需要有一位去标注这个数字是否为负数而在使用bitmap的时候使用基本类型仅仅是为了使用它的位至于是否是负数并不重要既然如此舍弃掉正负数标志位就可以多利用一位进行表示。

除此之外,还可以选择多个基本元素进行叠加,即多组混合表示,以表示{03581415}为例:

1 0 0 1 0 1 0 0
1 0 0 0 0 0 1 1

即一第一行为07第二行及以后为n*8+07进行表示而对于golang实现来说可以选择用uint64的切片进行表示即在换算时候对于一个数字num它所属的行从0开始


num/64

它所属的列从0开始


num%64

用处

bitmap由于其本身的有序性和唯一性一来可以实现快速排序:即对于一组无需且稠密的数字可以将其放入bitmap中然后再遍历获取出来从而使得其元素是单调递增的而来可以实现去重由于bitmap是使用bit进行表示一个元素是否存在其中而bit有且仅有1和0两种情况所以在一个位置插入多个元素时它表示的都是1从而可以实现插入多个保留一个的去重三来可以极大的节省存储空间由于对基本类型数组的下标赋予了一定的含义导致可以利用其含义来表示一个数字而不是去存储一个数字即要表示一个2^16的数字时候原本需要16位现在只需要1位即可实现。

实现

结构定义

由上文可知所要使用的bitmap内部其实是存储的uint64的切片同时增加一个zero

type bitmap struct {
	bits []uint64
}
接口集合
type bitmaper interface {
	Insert(num uint)         //在num位插入元素
	Delete(num uint)         //删除第num位
	Check(num uint) (b bool) //检查第num位是否有元素
	All() (nums []uint)      //返回所有存储的元素的下标
	Clear()                  //清空
}

New

创建部分主要做的就是返回一个初始化后的bitmap指针

func New() (bm *bitmap) {
	return &bitmap{
		bits: make([]uint64, 0, 0),
	}
}

Insert

对于bitmap的插入来说会出现两种情况第一种是要插入的位恰好可以在位图中容纳此时将对应位设为1即可并不需要对位图进行扩容实现起来十分简单找到对应位即可第二章就是要插入的位超过了位图可容纳的范围次数就需要一个扩容策略对其进行扩容以保证插入的值在位图内可以存储。

扩容策略

对于bitmap扩容来说它的扩容主要是由于新要插入的位超出了位图所能表示的位从而需要对位图进行扩增以满足其范围而扩容则可以采取两种策略

  1. 固定扩容:对于要扩容的位图,进行一个固定的量进行扩容,该方案的好处实现简单,但坏处也明显,如果固定扩容后还是不足以覆盖还需要再次扩容,同时如果扩容太大也会需要更多的空间;
  2. 按需扩容对于要扩容的位图按照它需要扩容的范围来进行扩容该方案的好处是几乎可以恰好容纳需要存储的位但如果存在连续的几个较短的扩容出现时即先扩容到64->128>256>512>1024这种情况时需要连续的扩容复制会浪费一定的性能。

对此,我们可以考虑将两种方案结合起来,即当要增加的范围并不太大的时候,牺牲一定量的空间进行固定扩容,从而避免连续的小范围扩容多次出现降低性能,而对于大范围扩容时,则使用按需扩容,以此来提高单次扩容速度。

本次实现中,固定扩容采用一次增加2^10个uint64

func (bm *bitmap) Insert(num uint) {
   //bm不存在时直接结束
   if bm == nil {
      return
   }
   //开始插入
   if num/64+1 > uint(len(bm.bits)) {
      //当前冗余量小于num位,需要扩增
      var tmp []uint64
      //通过冗余扩增减少扩增次数
      if num/64+1 < uint(len(bm.bits)+1024) {
         //入的位比冗余的多不足2^16即1024*64时,则新增1024个uint64
         tmp = make([]uint64, len(bm.bits)+1024)
      } else {
         //直接增加到可以容纳第num位的位置
         tmp = make([]uint64, num/64+1)
      }
      //将原有元素复制到新增的切片内,并将bm所指向的修改为扩增后的
      copy(tmp, bm.bits)
      bm.bits = tmp
   }
   //将第num位设为1即实现插入
   bm.bits[num/64] ^= 1 << (num % 64)
}

Delete

对于位图的删除会出现三种情况第一种是要删除的范围不在位图范围内直接忽略即可第二种是在位图范围内且位图尾部存在连续为0的uint64时此时就需要执行缩容策略

缩容策略

对于缩容策略,同样采取三种方案:

  1. 固定缩容从尾部删除固定数量的为0的组该方案删除的量是固定的优点在于尾部量很多时可以慢慢减少剩下的可当作冗余量避免多次增加缺点是缓慢减少需要一定时间开销
  2. 按需缩容:当存在需要删除的组时,直接进行删除,优点的空间利用率更高,缺点是冗余基本没有,同时删除过于频繁
  3. 折半删除当需要删除的组超过一半时删掉一半即可该方案删除切合slice底层实现但在普遍场景来说可能并不会经常使用。

为此,可以选择将三种结合起来,当总组数很长时,且需要删除的量过多,可以使用固定缩容,留出一定余量,当删除量不多时,且需要删除的量超过了总组数的一半时,在进行按需删除,即把要删除的全部删完。

为了避免固定扩容和固定缩容循环出现对两者数值进行错位由于固定扩容选择的是1024所以固定缩容选为256。

func (bm *bitmap) Delete(num uint) {
	//bm不存在时直接结束
	if bm == nil {
		return
	}
	//num超出范围,直接结束
	if num/64+1 > uint(len(bm.bits)) {
		return
	}
	//将第num位设为0
	bm.bits[num/64] &^= 1 << (num % 64)
	if bm.bits[len(bm.bits)-1] == 0 {
		//最后一组为0,可能进行缩容
		//从后往前遍历判断可缩容内容是否小于总组数
		i := len(bm.bits) - 1
		for ; i >= 0; i-- {
			if bm.bits[i] == 0  && i!=len(bm.bits)-1024{
				continue
			} else {
				//不为0或到1024个时即可返回
				break
			}
		}
		if i <= len(bm.bits)/2 || i==len(bm.bits)-1024 {
			//小于总组数一半或超过1023个,进行缩容
			bm.bits = bm.bits[:i+1]
		}
	} else {
		return
	}
}

Check

检查第num位是否存在于位图种不存在返回false存在返回true超出范围也返回false。

func (bm *bitmap) Check(num uint) (b bool) {
   //bm不存在时直接返回false并结束
   if bm == nil {
      return false
   }
   //num超出范围,直接返回false并结束
   if num/64+1 > uint(len(bm.bits)) {
      return false
   }
   //判断第num是否为1,为1返回true,否则为false
    if bm.bits[num/64]&(1<<(num%64)) > 0 {
      return true
   }
   return false
}

All

返回所有在位图种存在的元素的下标,同时保证其为单调递增序列。

func (bm *bitmap) All() (nums []uint) {
	//对要返回的集合进行初始化,以避免返回nil
	nums=make([]uint,0,0)
	//bm不存在时直接返回并结束
	if bm == nil {
		return nums
	}
	//分组遍历判断某下标的元素是否存在于位图中,即其值是否为1
	for j := 0; j < len(bm.bits); j++ {
		for i := 0; i < 64; i++ {
			if bm.bits[j]&(1<<i) > 0 {
				//该元素存在,添加入结果集合内
				nums = append(nums, uint(j*64+i))
			}
		}
	}
	return nums
}

Clear

清空初始化位图。

func (bm *bitmap) Clear() {
   if bm == nil {
      return
   }
   bm.bits = make([]uint64, 0, 0)
}

使用示例

package main

import (
	"fmt"
	"github.com/hlccd/goSTL/data_structure/bitmap"
)
func main() {
	var nums []uint
	bm:=bitmap.New()
	bm.Insert(1)
	bm.Insert(2)
	bm.Insert(3)
	bm.Insert(64)
	bm.Insert(128)
	bm.Insert(256)
	bm.Insert(320)
	nums=bm.All()
	for i:=0;i< len(nums);i++{
		fmt.Println(nums[i])
	}
	bm.Delete(320)
	fmt.Println()
	nums=bm.All()
	for i:=0;i< len(nums);i++{
		fmt.Println(nums[i])
	}
	bm.Delete(256)
	fmt.Println()
	nums=bm.All()
	for i:=0;i< len(nums);i++{
		fmt.Println(nums[i])
	}
	bm.Delete(128)
	fmt.Println()
	nums=bm.All()
	for i:=0;i< len(nums);i++{
		fmt.Println(nums[i])
	}
	bm.Clear()
	fmt.Println()
	nums=bm.All()
	for i:=0;i< len(nums);i++{
		fmt.Println(nums[i])
	}
}

结果

1 2 3 64 128 256 320

1 2 3 64 128 256

1 2 3 64 128

1 2 3 64