Go位集合相关操作bitset库安装使用

目录
  • 简介
  • 安装
  • 使用
  • 为什么要使用它?
  • 总结
  • 一点闲话
  • 参考

简介

我们都知道计算机是基于二进制的,位运算是计算机的基础运算。位运算的优势很明显,CPU 指令原生支持、速度快。基于位运算的位集合在有限的场景中替换集合数据结构可以收到意想不到的效果。bitset库实现了位集合及相关操作,不妨拿来即用。

安装

本文代码使用 Go Modules。

创建目录并初始化:

$ mkdir -p bitset && cd bitset
$ go mod init github.com/darjun/go-daily-lib/bitset

安装bitset库:

$ go get -u github.com/bits-and-blooms/bitset

使用

位集合的基本操作有:

  • 检查位(Test):检查某个索引是否为 1。类比检查元素是否在集合中
  • 设置位(Set):将某个索引设置为 1。类比向集合添加元素
  • 清除位(Clear):将某个索引清除,设置为 0。类比从集合中删除元素
  • 翻转位(Flip):如果某个索引为 1,则设置为 0,反之设置为 1
  • 并(Union):两个位集合执行并操作。类比集合的并
  • 交(Intersection):两个位集合执行交操作。类比集合的交

位集合一般用于小数值的非负整数的场景中。就拿游戏中简单的签到举例吧,很多游戏都有签到活动,短的有 7 天的,长的有 30 天。这种就很适合使用位集合。每个位的值表示其索引位置对应的那天有没有签到。

type Player struct {
  sign *bitset.BitSet
}
func NewPlayer(sign uint) *Player {
  return &Player{
    sign: bitset.From([]uint64{uint64(sign)}),
  }
}
func (this *Player) Sign(day uint) {
  this.sign.Set(day)
}
func (this *Player) IsSigned(day uint) bool {
  return this.sign.Test(day)
}
func main() {
  player := NewPlayer(1) // 第一天签到
  for day := uint(2); day <= 7; day++ {
    if rand.Intn(100)&1 == 0 {
      player.Sign(day - 1)
    }
  }
  for day := uint(1); day <= 7; day++ {
    if player.IsSigned(day - 1) {
      fmt.Printf("day:%d signed\n", day)
    }
  }
}

bitset 提供了多种创建 BitSet 对象的方法。

首先 bitset.BitSet 零值可用,如果一开始不知道有多少元素,可以使用这种方式创建:

var b bitset.BitSet

BitSet 在设置时自动调整大小。如果事先知道长度,创建 BitSet 时可传入此值,能有效避免自动调整的开销:

b := bitset.New(100)

bitset 结构支持链式调用,大部分方法返回自身的指针,所以可以这样写:

b.Set(10).Set(11).Clear(12).Flip(13);

注意,bitset 的索引是从 0 开始的。

记得之前在网上看过一道题:

一个农夫带着一只狼、一头羊和一颗白菜来到河边。他需要用船把他们带到对岸。然而,这艘船只能容下农夫本人和另外一种东西(要么是狼,要么是羊,要么是白菜)。如果农夫不在场的话,狼就会吃掉羊,羊会吃掉白菜。请为农夫解决这个问题。

这其实是一个状态搜索的问题,用回溯法就能解决。农夫、狼、羊、白菜都有两个状态,即在河左岸(假设刚开始农夫所处的是左岸)还是河右岸。这里实际上还有个船的状态,由于船一定和农夫的状态是一致的,就不用额外考虑了。这些状态我们很容易用位集合来表示:

const (
  FARMER = iota
  WOLF
  SHEEP
  CABBAGE
)

编写一个函数来判断状态是否合法。有两种状态不合法:

  • 狼和羊在同一边,并且不和农夫在同一边。此时狼会吃掉羊
  • 羊和白菜在同一边,并且不和农夫在同一边。此时羊会吃掉白菜
func IsStateValid(state *bitset.BitSet) bool {
  if state.Test(WOLF) == state.Test(SHEEP) &&
    state.Test(WOLF) != state.Test(FARMER) {
    // 狼和羊在同一边,并且不和农夫在同一边
    // 狼会吃掉羊,非法
    return false
  }
  if state.Test(SHEEP) == state.Test(CABBAGE) &&
    state.Test(SHEEP) != state.Test(FARMER) {
    // 羊和白菜在同一边,并且不和农夫在同一边
    // 羊会吃掉白菜,非法
    return false
  }
  return true
}

接下来编写搜索函数:

func search(b *bitset.BitSet, visited map[string]struct{}) bool {
  if !IsStateValid(b) {
    return false
  }
  if _, exist := visited[b.String()]; exist {
    // 状态已遍历
    return false
  }
  if b.Count() == 4 {
    return true
  }
  visited[b.String()] = struct{}{}
  for index := uint(FARMER); index <= CABBAGE; index++ {
    if b.Test(index) != b.Test(FARMER) {
      // 与农夫不在一边,不能带上船
      continue
    }
    // 带到对岸去
    b.Flip(index)
    if index != FARMER {
      // 如果 index 为 FARMER,表示不带任何东西
      b.Flip(FARMER)
    }
    if search(b, visited) {
      return true
    }
    // 状态恢复
    b.Flip(index)
    if index != FARMER {
      b.Flip(FARMER)
    }
  }
  return false
}

主函数:

func main() {
  b := bitset.New(4)
  visited := make(map[string]struct{})
  fmt.Println(search(b, visited))
}

初始时,所有状态为 0,都到对岸之后所有状态为 1,故b.Count() == 4表示都已到达对岸了。由于搜索是盲目的,可能会无限循环:这次农夫将羊带到对岸,下次又将其从对岸带回来了。所以我们需要做状态去重。bitset.String()返回当前位集合的字符串表示,我们以此来判断状态是否重复。

for 循环依次尝试带各种物品,或什么也不带。驱动查找过程。

如果想得到农夫正确的动作序列,可以给 search 加一个参数,记录每一步的操作:

func search(b *bitset.BitSet, visited map[string]struct{}, path *[]*bitset.BitSet) bool {
  // 记录路径
  *path = append(*path, b.Clone())
  if b.Count() == 4 {
    return true
  }
  // ...
  *path = (*path)[:len(*path)-1]
  return false
}
func main() {
  b := bitset.New(4)
  visited := make(map[string]struct{})
  var path []*bitset.BitSet
  if search(b, visited, &path) {
    PrintPath(path)
  }
}

如果找到解,打印之:

var names = []string{"农夫", "狼", "羊", "白菜"}
func PrintState(b *bitset.BitSet) {
  fmt.Println("=======================")
  fmt.Println("河左岸:")
  for index := uint(FARMER); index <= CABBAGE; index++ {
    if !b.Test(index) {
      fmt.Println(names[index])
    }
  }
  fmt.Println("河右岸:")
  for index := uint(FARMER); index <= CABBAGE; index++ {
    if b.Test(index) {
      fmt.Println(names[index])
    }
  }
  fmt.Println("=======================")
}
func PrintMove(cur, next *bitset.BitSet) {
  for index := uint(WOLF); index <= CABBAGE; index++ {
    if cur.Test(index) != next.Test(index) {
      if !cur.Test(FARMER) {
        fmt.Printf("农夫将【%s】从河左岸带到河右岸\n", names[index])
      } else {
        fmt.Printf("农夫将【%s】从河右岸带到河左岸\n", names[index])
      }
      return
    }
  }
  if !cur.Test(FARMER) {
    fmt.Println("农夫独自从河左岸到河右岸")
  } else {
    fmt.Println("农夫独自从河右岸到河左岸")
  }
}
func PrintPath(path []*bitset.BitSet) {
  cur := path[0]
  PrintState(cur)
  for i := 1; i < len(path); i++ {
    next := path[i]
    PrintMove(cur, next)
    PrintState(next)
    cur = next
  }
}

运行结果:

=======================
河左岸:
农夫


白菜

河右岸:
=======================
农夫将【羊】从河左岸带到河右岸
=======================
河左岸:

白菜

河右岸:
农夫

=======================
农夫独自从河右岸到河左岸
=======================
河左岸:
农夫

白菜

河右岸:

=======================
农夫将【狼】从河左岸带到河右岸
=======================
河左岸:
白菜

河右岸:
农夫


=======================
农夫将【羊】从河右岸带到河左岸
=======================
河左岸:
农夫

白菜

河右岸:

=======================
农夫将【白菜】从河左岸带到河右岸
=======================
河左岸:

河右岸:
农夫

白菜
=======================
农夫独自从河右岸到河左岸
=======================
河左岸:
农夫

河右岸:

白菜
=======================
农夫将【羊】从河左岸带到河右岸
=======================
河左岸:

河右岸:
农夫


白菜
=======================

即农夫操作过程为:将羊带到右岸->独自返回->将白菜带到右岸->再将羊带回左岸->带上狼到右岸->独自返回->最后将羊带到右岸->完成。

为什么要使用它?

似乎直接使用位运算就可以了,为什么要使用 bitset 库呢?

因为通用,上面列举的两个例子都是很小的整数值,如果整数值超过 64 位,我们必然要通过切片来存储。此时手写操作就非常不便了,而且容易出错。

库的优势体现在:

  • 足够通用
  • 持续优化
  • 大规模使用

只要对外提供的接口保持不变,它可以一直优化内部实现。虽然我们也可以做,但是费时费力。而且有些优化涉及到比较复杂的算法,自己实现的难度较高且易错。

有一个很典型的例子,求一个 uint64 的二进制表示中 1 的数量(popcnt,或 population count)。实现的方法有很多。

最直接的,我们一位一位地统计:

func popcnt1(n uint64) uint64 {
  var count uint64
  for n > 0 {
    if n&1 == 1 {
      count++
    }
    n >>= 1
  }
  return count
}

考虑空间换时间,我们可以预先计算 0-255 这 256 个数字的二进制表示中 1 的个数,然后每 8 位计算一次,可能将计算次数减少到之前的 1/8。这也是标准库中的做法:

const pop8tab = "" +
  "\x00\x01\x01\x02\x01\x02\x02\x03\x01\x02\x02\x03\x02\x03\x03\x04" +
  "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" +
  "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" +
  "\x01\x02\x02\x03\x02\x03\x03\x04\x02\x03\x03\x04\x03\x04\x04\x05" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" +
  "\x02\x03\x03\x04\x03\x04\x04\x05\x03\x04\x04\x05\x04\x05\x05\x06" +
  "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" +
  "\x03\x04\x04\x05\x04\x05\x05\x06\x04\x05\x05\x06\x05\x06\x06\x07" +
  "\x04\x05\x05\x06\x05\x06\x06\x07\x05\x06\x06\x07\x06\x07\x07\x08"
func popcnt2(n uint64) uint64 {
  var count uint64
  for n > 0 {
    count += uint64(pop8tab[n&0xff])
    n >>= 8
  }
  return count
}

最后是 bitset 库中的算法:

func popcnt3(x uint64) (n uint64) {
  x -= (x >> 1) & 0x5555555555555555
  x = (x>>2)&0x3333333333333333 + x&0x3333333333333333
  x += x >> 4
  x &= 0x0f0f0f0f0f0f0f0f
  x *= 0x0101010101010101
  return x >> 56
}

对以上三种实现进行性能测试:

goos: windows
goarch: amd64
pkg: github.com/darjun/go-daily-lib/bitset/popcnt
cpu: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
BenchmarkPopcnt1-8         52405             24409 ns/op
BenchmarkPopcnt2-8        207452              5443 ns/op
BenchmarkPopcnt3-8       1777320               602 ns/op
PASS
ok      github.com/darjun/go-daily-lib/bitset/popcnt    4.697s

popcnt3 相对 popcnt1 有 40 倍的性能提升。在学习上我们可以自己尝试造轮子,以此来加深自己对技术的理解。但是在工程上,通常更倾向于使用稳定的、高效的库。

总结

本文借着 bitset 库介绍了位集合的使用。并且应用 bitset 解决了农夫过河问题。最后我们讨论了为什么要使用库?

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue

一点闲话

我发现人的惰性实在是太可怕了。虽然这半年来没写文章一开始是因为工作上的原因,后来单纯是因为惯性,因为懒。而且总是“装着很忙”来逃避需要花时间、花精力的事情。在这种想动又不想动的角逐之间,时间就这么一晃而过。

我们总是在抱怨没有时间,没有时间。但仔细想想,仔细算算,我们花在刷短视频,玩游戏上的时间其实并不少。

上周我在阮一峰老师的周刊上看到一篇文章《人生不短》,看了之后深有触动。人总该有所坚持,生活才有意义。

参考

以上就是Go位集合相关操作bitset库安装使用的详细内容,更多关于Go位集合bitset库的资料请关注我们其它相关文章!

(0)

相关推荐

  • Go压缩位图库roaring安装使用详解

    目录 简介 安装 使用 基本操作 迭代 并行操作 写入与读取 64 位版本 存储格式 概览 Cookie Header Descriptive Header Offset Header Container array bitmap/bitset run 手撸解析代码 总结 简介 集合是软件中的基本抽象.实现集合的方法有很多,例如 hash set.tree等.要实现一个整数集合,位图(bitmap,也称为 bitset 位集合,bitvector 位向量)是个不错的方法.使用 n 个位(bit)

  • Golang配置管理库 Viper的教程详解

    目录 一.Viper 是什么? 二.安装 Viper 三.Viper 有什么作用 四.Viper demo 可供参考 注意 五.总结 一.Viper 是什么? Viper 是应用程序的完整配置的管理工具,用于在应用程序中工作,可以处理所有类型的配置需求和格式. 二.安装 Viper go get github.com/spf13/viper 三.Viper 有什么作用 设置默认值 读取 JSON.TOML.YAML(YML).HCL.envfile 和 Java properties 属性配置文

  • Golang详细讲解常用Http库及Gin框架的应用

    目录 1. Http标准库 1.1 http客户端 1.2 自定义请求头 1.3 检查请求重定向 1.4 http服务器性能分析 2. JSON数据处理 2.1 实体序列化 2.2 处理字段为小写下划线 2.3 省略空字段 2.4 反序列化 3. 自然语言处理 3.1 使用Map处理 3.2 定义实体处理 4. http框架 4.1 gin 4.1.1 启动服务 4.1.2 middleware 4.1.3 设置请求ID 1. Http标准库 1.1 http客户端 func main() {

  • Go标准库http与fasthttp服务端性能对比场景分析

    目录 1. 背景 2. 性能测试 3. 对结果的简要分析 4. 优化途径 1. 背景 Go初学者学习Go时,在编写了经典的“hello, world”程序之后,可能会迫不及待的体验一下Go强大的标准库,比如:用几行代码写一个像下面示例这样拥有完整功能的web server: // 来自https://tip.golang.org/pkg/net/http/#example_ListenAndServe package main import ( "io" "log"

  • Go语言Zap库Logger的定制化和封装详解

    目录 前言 Go 语言原生的Logger Go 语言原生Logger的缺点 Zap 日志库 Zap 的使用方法 安装zap 设置 Logger 定制 Zap 的 Logger 日志切割 封装 Logger 总结 前言 日志无论对于程序还是程序员都非常重要,有多重要呢,想要长期在公司健健康康的干下去就得学会阶段性划水,阶段性划水的一大关键的就是干活快过预期但是装作...不对,这个开头不对劲,下面重来. 日志无论对于程序还是程序员都非常重要,程序员解决问题的快慢除了经验外,就是看日志能不能有效地记录

  • Go位集合相关操作bitset库安装使用

    目录 简介 安装 使用 为什么要使用它? 总结 一点闲话 参考 简介 我们都知道计算机是基于二进制的,位运算是计算机的基础运算.位运算的优势很明显,CPU 指令原生支持.速度快.基于位运算的位集合在有限的场景中替换集合数据结构可以收到意想不到的效果.bitset库实现了位集合及相关操作,不妨拿来即用. 安装 本文代码使用 Go Modules. 创建目录并初始化: $ mkdir -p bitset && cd bitset $ go mod init github.com/darjun/

  • Python集合基本概念与相关操作实例分析

    本文实例讲述了Python集合基本概念与相关操作.分享给大家供大家参考,具体如下: 集合的概念 集合是无序可变,元素不能重复.实际上,集合底层是字典实现,集合的所有元素都是字典 中的"键对象",因此是不能重复的且唯一的. 集合创建和删除 使用{}创建集合对象,并使用 add()方法添加元素 >>> a = {3,5,7} >>> a {3, 5, 7} >>> a.add(9) >>> a {9, 3, 5, 7}

  • Python中的wordcloud库安装问题及解决方法

    今天下载wordcloud的时候出现了很多问题,在此总结总结 1.问题一:You are using pip version 19.0.3, however version 20.0.2 is available-问题 解决方法: 打开cmd输入如下命令 python -m pip install -U pip 2.问题二:error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual 解决方法: 方法1(不

  • Python imgaug库安装与使用教程(图片加模糊光雨雪雾等特效)

    目录 简介 安装 Overview 特效 Project 结构 程序 参考的源代码(来源于网络) 简易变换 试效果 使用 模糊光雨雪雾 else 重命名00001.jpg 重命名1.jpg 效果图 简介 imgaug:机器学习实验中的图像增强库,特别是卷积神经网络.支持以多种不同方式增强图像.关键点/地标.边界框.热图和分割图. 安装 在anaconda prompt里进行 pip install imgaug 看了几篇文章,出错的话可以先安装依赖库shapely Overview 特效 官网网

  • pycharm第三方库安装失败的问题及解决经验分享

    一.报错信息:[file][Default Settint]---Project Interpreter 点击搜索suds安装模块报错 解决:依据上图提示找到C:\Program Files\JetBrains\PyCharm 2017.2.3\helpers\packaging_tool.py 文件的192行和109行 将do_install函数和do_uninstall函数修改为如下格式 def do_install(pkgs): try: try: from pip._internal i

  • Python第三方库安装缓慢的解决方法

    前言 一般情况下,我们在命令行中使用pip install 库名的方法安装python第三方库.但由于一些众所周知的原因,这种方法下载速度较慢,容易error,有时候不得不需要去官网手动安装,十分繁琐. 解决方法 使用pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 库名 命令,在清华镜像开源网站上下载第三方库. 可以看到下载速度有了飞速提升. 注意 这种方法不是万能的,在遇到版本等问题时依然会报错. 总结 到此这篇关于Python第

  • Python中matplotlib库安装失败的经验总结(附pycharm配置anaconda)

    目录 1. 首先检查自己pip是否最新: 2. 先试着装库,看看自己缺什么: 2.1 from version:none 2.2 numpy>=1.71 etc. 2.3 pillow缺少zlib环境 2.4 Cannot found pip.ini 3 安装完成 补充:pycharm配置anaconda 总结 由于学习需要安装matplotlib库,阅读网上教程后一直出现各种各样的错误,以下为我的经验总结: 声明:本人python版本为3.8.0,pycharm为2021.2 1. 首先检查自

  • Python黑魔法库安装及操作字典示例详解

    目录 1. 安装方法 2. 简单示例 3. 兼容字典的所有操作 4. 设置返回默认值 5. 工厂函数自动创建key 6. 序列化的支持 7. 说说局限性 本篇文章收录于<Python黑魔法手册>v3.0 第七章,手册完整版在线阅读地址:Python黑魔法手册 3.0 文档 字典是 Python 中基础的数据结构之一,字典的使用,可以说是非常的简单粗暴,但即便是这样一个与世无争的数据结构,仍然有很多人 "用不惯它" . 也许你并不觉得,但我相信,你看了这篇文章后,一定会和我一

  • 简述Docker 安装influxDB分布式时间序列数据库及相关操作

    influxDB简介 influxDB是一个分布式时间序列数据库.cAdvisor仅仅显示实时信息,但是不存储监视数据.因此,我们需要提供时序数据库用于存储cAdvisor组件所提供的监控信息, 以便显示除实时信息之外的时序数据. influxDB安装 拉取镜像 docker pull tutum/influxdb 启动容器 #18083=>8083 WEB端口 8086=>8086 数据端口 docker run --name is_influx_db -p 18083:8083 -p 80

  • python对json的相关操作实例详解

    本文实例分析了python对json的相关操作.分享给大家供大家参考,具体如下: 什么是json: JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.易于人阅读和编写.同时也易于机器解析和生成.它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集.JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C, C+

随机推荐