zoukankan      html  css  js  c++  java
  • golang 实现Bit数组

    Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型。集合用map类型来表示虽然非常灵活,但我们可以以一种更好的形式来表示它。例如在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。(译注:这里再补充一个例子,比如我们执行一个http下载任务,把文件按照16kb一块划分为很多块,需要有一个全局变量来标识哪些块下载完成了,这种时候也需要用到bit数组)

    一个bit数组通常会用一个无符号数或者称之为“字”的slice来表示,每一个元素的每一位都表示集合里的一个值。当集合的第i位被设置时,我们才说这个集合包含元素i。下面的这个程序展示了一个简单的bit数组类型,并且实现了三个函数来对这个bit数组来进行操作:

    // An IntSet is a set of small non-negative integers.
    // Its zero value represents the empty set.
    type IntSet struct {
        words []uint64
    }
    
    // Has reports whether the set contains the non-negative value x.
    func (s *IntSet) Has(x int) bool {
        word, bit := x/64, uint(x%64)
        return word < len(s.words) && s.words[word]&(1<<bit) != 0
    }
    
    // Add adds the non-negative value x to the set.
    func (s *IntSet) Add(x int) {
        word, bit := x/64, uint(x%64)
        for word >= len(s.words) {
            s.words = append(s.words, 0)
        }
        s.words[word] |= 1 << bit
    }
    
    // UnionWith sets s to the union of s and t.
    func (s *IntSet) UnionWith(t *IntSet) {
        for i, tword := range t.words {
            if i < len(s.words) {
                s.words[i] |= tword
            } else {
                s.words = append(s.words, tword)
            }
        }
    }

    因为每一个字都有64个二进制位,所以为了定位x的bit位,我们用了x/64的商作为字的下标,并且用x%64得到的值作为这个字内的bit的所在位置。UnionWith这个方法里用到了bit位的“或”逻辑操作符号|来一次完成64个元素的或计算。(在练习6.5中我们还会程序用到这个64位字的例子。)

    当前这个实现还缺少了很多必要的特性,我们把其中一些作为练习题列在本小节之后。但是有一个方法如果缺失的话我们的bit数组可能会比较难混:将IntSet作为一个字符串来打印。这里我们来实现它,让我们来给上面的例子添加一个String方法,类似2.5节中做的那样:

    // String returns the set as a string of the form "{1 2 3}".
    func (s *IntSet) String() string {
        var buf bytes.Buffer
        buf.WriteByte('{')
        for i, word := range s.words {
            if word == 0 {
                continue
            }
            for j := 0; j < 64; j++ {
                if word&(1<<uint(j)) != 0 {
                    if buf.Len() > len("{") {
                        buf.WriteByte(' ')
                    }
                    fmt.Fprintf(&buf, "%d", 64*i+j)
                }
            }
        }
        buf.WriteByte('}')
        return buf.String()
    }

    这里留意一下String方法,是不是和3.5.4节中的intsToString方法很相似;bytes.Buffer在String方法里经常这么用。当你为一个复杂的类型定义了一个String方法时,fmt包就会特殊对待这种类型的值,这样可以让这些类型在打印的时候看起来更加友好,而不是直接打印其原始的值。fmt会直接调用用户定义的String方法。这种机制依赖于接口和类型断言,在第7章中我们会详细介绍。

    现在我们就可以在实战中直接用上面定义好的IntSet了:

    var x, y IntSet
    x.Add(1)
    x.Add(144)
    x.Add(9)
    fmt.Println(x.String()) // "{1 9 144}"
    
    y.Add(9)
    y.Add(42)
    fmt.Println(y.String()) // "{9 42}"
    
    x.UnionWith(&y)
    fmt.Println(x.String()) // "{1 9 42 144}"
    fmt.Println(x.Has(9), x.Has(123)) // "true false"

    这里要注意:我们声明的String和Has两个方法都是以指针类型*IntSet来作为接收器的,但实际上对于这两个类型来说,把接收器声明为指针类型也没什么必要。不过另外两个函数就不是这样了,因为另外两个函数操作的是s.words对象,如果你不把接收器声明为指针对象,那么实际操作的是拷贝对象,而不是原来的那个对象。因此,因为我们的String方法定义在IntSet指针上,所以当我们的变量是IntSet类型而不是IntSet指针时,可能会有下面这样让人意外的情况:

    fmt.Println(&x)         // "{1 9 42 144}"
    fmt.Println(x.String()) // "{1 9 42 144}"
    fmt.Println(x)          // "{[4398046511618 0 65536]}"

    在第一个Println中,我们打印一个*IntSet的指针,这个类型的指针确实有自定义的String方法。第二Println,我们直接调用了x变量的String()方法;这种情况下编译器会隐式地在x前插入&操作符,这样相当远我们还是调用的IntSet指针的String方法。在第三个Println中,因为IntSet类型没有String方法,所以Println方法会直接以原始的方式理解并打印。所以在这种情况下&符号是不能忘的。在我们这种场景下,你把String方法绑定到IntSet对象上,而不是IntSet指针上可能会更合适一些,不过这也需要具体问题具体分析

    上面实现的add方法和String方法也许有些人不太理解,我刚开始也不理解,重新复习了下位运算了,原来是这么简单的骚操作:

    先来简单复习下位运算  左移动

    1 <<  1   0000 0001  ->  0000 0010  === 2

    1 <<  3   0000 0001  ->  0000 1000  === 8

    &(位与):比较二进制数相对应的每一位,相对的位均为1,则对应位输出 1,相对应有一位为0或无则为0

    8 & 9 : 1000 & 1001 ==> 1000 ;16&8:10000 & 1000==>0

    |(位或):比较二进制数相对应的每一位,相对的位有一个为1,则对应位输出1,相对应的均为0则为0

    8 | 9:1000 & 1001 ==> 1001 ; 16&8: 10000 & 1000 ==> 11000 (24)

    ^(位异或):比较二进制数相对应的每一位,相对的位相同,则对应位输出0,相对应的位不同则为1

    8 ^ 9 : 1000 ^ 1001 ⇒ 0001 ; 16&8: 10000 ^ 1000 ==> 11000(24)

    2 |   1<<3  :

    0000 0010   |    0000 1000  ====   0000 1010  

    如果还没有理解 请看下面这个数组

    64*0+bit
    0=>000000000000000000000000000000000000000000000000001001000010100

    0=》1001000010100 对应数字是  4628

    即0=》4628

    ------------------------------------------------------------------------------------------------------------

    64*1+bit
    1=>0000000000000000000000000000000000000000000000000010000000101000

    1=》1001000010100 对应数字是  4628

    即1=》4628

    -----------------------------------------------------------------------------------------------------------

    [4628,4628]

    0=》1001000010100 对应数字是  4628

    保存了 4位bit  分别是 [2,4,9,12]

    1=》1001000010100 对应数字是  4628

    保存了 4位bit  分别是 [64*1+2,64*1+4,64*1+9,64*1+12]

    如果还不懂的  就复习一下 位运算,在来看看本篇幅文章

  • 相关阅读:
    JAVA中handleEvent和action的区别
    Hessian的使用以及理解
    Java基础中的RMI介绍与使用
    Callable与Runable接口 submit与execute区别
    XXL-JOB原理--定时任务框架简介(一)
    11.并发包阻塞队列之LinkedBlockingQueue
    并发队列ConcurrentLinkedQueue和阻塞队列LinkedBlockingQueue用法
    正确实现用spring扫描自定义的annotation
    自贡进入“刷脸卡”时代 人脸识别支付“黑科技”现身自流井老街
    谷歌最新研究:量子计算机能在8小时内破解2048位RSA加密
  • 原文地址:https://www.cnblogs.com/sunlong88/p/13382191.html
Copyright © 2011-2022 走看看