C#中BitArray类

Posted 苏州程序大白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#中BitArray类相关的知识,希望对你有一定的参考价值。

C#中BitArray类

简介

BitArray类用于以紧凑的方式表示"位的集合"(sets of bits). 虽然我们能把位的集合存储在常规数组内, 但是如果采用专门为位的集合设计的数据结构就能创建更加有效率的程序. 本章将会介绍如何使用这种数据结构, 并且将讨论一些利用位的集合所解决的问题. 此外, 本章节还包含二进制数、按位运算符以及位移(bit shift)运算符的内容。

1、素数问题

先来看一个最终会用BitArray类来解决的问题. 这个问题就是如何找到素数. 在公元前三世纪, 古希腊哲学家埃拉托色尼(Eratosthenes)发现了一种找素数的方法, 这种方法被称为是埃拉托色尼筛法(the sieve of Eratosthenes). 该方法会不断筛选掉是其他数字整数倍的那些数, 直到最后剩下的数都是素数为止. 例如, 假设要确定出前100 个整数集合内的素数. 这里会先从2 开始, 它是第一个素数. 接着从头到尾遍历整数集合, 把所有是2 倍数的整数都移除掉. 然后, 移动到下一个素数3. 还是此从头到尾遍历整数集合, 把所有是 3 倍数的整数都移除掉. 再随后移动到素数5, 继续如此往复操作. 当操作全部结束时, 所有留下的就都是素数了.
我们将首先使用常规数组来解决这个问题, 主要的方法规则与本节后面要介绍的使用BitArray解决问题的方法类似, 首先要初始化一个由100个元素组成的数组, 每个元素都设置为值1. 接着从索引2开始(因为2是第一个素数), 检查每个后续数组索引的值是1还是0. 如果值为1, 则检查它是否为2的倍数. 如果是, 则该索引处的值设置为0, 直到检查完全部元素. 然后再从索引3开始, 使用3来检查值为1的索引与3的倍数关系, 依此类推, 直至每个索引都完成检查.
这里会借助先前章节开发的自定义CArray类. 第一件事就是创建一个筛选素数的方法. 代码如下所示:

//放在CArray类中
public void GenPrimes() 
{ 
    //从索引2开始, 遍历数组
    for (int outer = 2; outer <= arr.GetUpperBound(0); outer++) 
        //每轮内层循环的起点是当前的outer+1索引处, 也就是不需要检查自身的索引
        for (int inner = outer + 1; inner <= arr.GetUpperBound(0); inner++) 
            //如果元素值为1, 表示还没有被素数检查排除, 则进行检查
            if (arr[inner] == 1) 
                if ((inner % outer) == 0) 
                    //如果可以被当前outer整除,说明该索引数字除了1和自身, 还有其他因数, 不是素数
                    arr[inner] = 0; 
} 

然后再增加一个用来显示筛选出的素数的方法 :

//放在CArray类中    
public void ShowPrimes()
{
    Console.WriteLine("素数筛选结果 :");
    for (int i = 2; i <= arr.GetUpperBound(0); i++)
        if (arr[i] == 1)
            Console.Write(i + " ");
}

用如下代码测试下筛选方法的效果 :

static void Main()
{
    int size = 100;
    CArray primes = new CArray(size);
    for (int i = 0; i < size; i++)
        primes.Insert(1);
    primes.GenPrimes();
    primes.ShowPrimes();
    Console.ReadLine();
}

运行结果如下 :
在这里插入图片描述
上述代码说明了如何利用数组实现埃拉托色尼筛法, 但是既然数组中的每个元素不是0 就是1, 所以我们建议使用位(bit)来解决问题. 接下里的内容将讨论如何利用BitArray类来实现埃拉托色尼筛法以及其他可以借助位的集合来解决的问题。

2、位和位操作

考虑到大多数VB. NET 程序员并不熟悉在与位有关的工作, 在介绍BitArray类之前有必要先讨论一下如何在VB. NET 中使用位. 本小节将研究如何在VB. NET 中操作位, 其中主要是介绍如何用按位运算符来操作字节(byte)值。

3、二进制数制系统

在介绍如何处理Byte值之前, 首先来了解二进制系统. 二进制数是由0 和1 组成的字符串, 它把基数为十进制的数表示成二进制的数. 在计算机中, 用二进制00000000表示数字0. 而用二进制表示的整数1 则是00000001. 下面用二进制表示了从0 到9 的整数:
00000000—0d (字母d代表十进制数字)
00000001—1d
00000010—2d
00000011—3d
00000100—4d
00000101—5d
00000110—6d
00000111—7d
00001000—8d
00001001—9d
把二进制数转化为等价的十进制数的最好方法就是采用下列方案. 对于每一个二进制数字, 如果第一个位置上的数字为1, 那么就表示成. 如果第二个位置上也为1, 则表示成. 如此反复继续下去。
二进制数 : 00101010
等价于 即
我们通常会用八位的形式来显示二进制值, 八位就是一个字节(byte). 在一个字节可以表示的最大数是255, 它的二进制形式是 : 即
大于 255 的数必须存储在16 位中. 例如, 二进制表示的 256 就是00000001 00000000. 尽管并没有规定书写的时候必须每八位之间增加一个空格分隔, 但这种写法是一种业界惯例。

4、按位运算符和位移运算符

对二进制数而言不能使用常规的算术运算符, 而是使用按位运算符(And,Or, Not)或位移运算符(<<, >>和>>>)进行操作. 本节会说明这些运算符的工作原理. 此外, 后续的小节还会通过VB. NET 应用程序来举例说明它们的用法.
首先来讨论按位运算符. 这些都是大多数程序员早已熟悉的逻辑运算符——它们用来组合逻辑表达式从而计算出一个布尔值. 而对于二进制数而言, 按位运算符用来对两个二进制数进行按位比较, 从而产生一个新的二进制数. 当处理二进制数时, 1代表true, 0代表false. 为了说明按位运算符是如何进行按位操作的, 可参见下方真值表(truth table). 真值表内每行的前两列是参与按位运算的两个数, 第三列是运算的结果.
下表展示And 运算符的真值表 :
在这里插入图片描述
与之对应的位值表如下所示 :
在这里插入图片描述
再看看Or运算符的真值表 :
在这里插入图片描述
与之对应的位值表如下所示 :
在这里插入图片描述
最后还有Xor(异或)运算符. 因为在计算机程序执行的逻辑操作中不会用到这种运算符, 所以很少有人知道这种按位运算符. 当两个位用 Xor运算符进行比较时, 如果两个运算数中只有一个为1, 那么结果位就为1(两位值不相等, 就得1, 两位值相等, 就得0). 下面是Xor的真值表:
在这里插入图片描述
根据真值表表述的按位计算规则, 就可以通过按位运算符把二进制数组合为新的二进制数. 下面是一些实例:
00000001 And 00000000 -> 00000000
00000001 And 00000001 -> 00000001
00000010 And 00000001 -> 00000000
00000000 Or 00000001 -> 00000001
00000001 Or 00000000 -> 00000001
00000010 Or 00000001 -> 00000011
00000000 Xor 00000001 -> 00000001
00000001 Xor 00000000 -> 00000001
00000001 Xor 00000001 -> 00000000
现在来看看VB. NET 视窗应用程序是如何更好的演示按位运算符的工作原理的

5、按位运算符的应用

本节会举例说明按位运算符是如何在C#中利用视窗应用程序工作的, 视窗应用程序对两个数值进行按位运算. 首先来看一下此应用程序的用户界面, 这对理解应用程序的工作原理大有帮助:
在这里插入图片描述
具体操作是先录入两个整数值, 并且由用户选择其中一种按位运算符的按纽. 随后, 每个整数值都会以位的形式显示出来, 连同还会显示出相应按位操作的位串结果. 下面是一个对1和2 进行按位与操作的实例:
在这里插入图片描述
窗体功能逻辑代码如下所示 :

//原文代码不能直接运行, 已修正
using System;
using System.Text;
using System.Windows.Forms;

public partial class BitwiseCalculator : Form {
    public BitwiseCalculator()
    {
        InitializeComponent();
    }

    private StringBuilder ConvertBits(int val)
    {
        //掩码, 10000000 00000000 00000000 00000000
        int dispMask = 1 << 31;
        //4个8位加三个空格, 长度一共是35
        StringBuilder bitBuffer = new StringBuilder(35);
        for (int i = 1; i <= 32; i++) {
            if ((val & dispMask) == 0)
                bitBuffer.Append("0");
            else
                bitBuffer.Append("1");
            //假设输入的是-1, -1<<1后得到-2,可推出, 按位操作符, 操作的也是补码
            val <<= 1;
            if ((i % 8) == 0)
                bitBuffer.Append(" ");
        }
        return bitBuffer;
    }

    //公共计算方法
    void Calculator(string opt)
    {
        int val1 = int.Parse(tbox_num1.Text);
        int val2 = int.Parse(tbox_num2.Text);
        lab_bit1.Text = ConvertBits(val1).ToString();
        lab_bit2.Text = ConvertBits(val2).ToString();
        int result = 0;
        switch (opt) {
            case "and":
                result = val1 & val2;
                break;
            case "or":
                result = val1 | val2;
                break;
            case "xor":
                result = val1 ^ val2;
                break;
        }
        lab_resultNum.Text = result.ToString();
        lab_resultBit.Text = ConvertBits(result).ToString();
    }

    //点击按位或按钮
    private void bt_or_Click(object sender, EventArgs e)
    {
        Calculator("or");
    }
    //点击按位与按钮
    private void bt_and_Click(object sender, EventArgs e)
    {
        Calculator("and");
    }
    //点击按位异或按钮
    private void bt_Xor_Click(object sender, EventArgs e)
    {
        Calculator("xor");
    }
}

6、位移运算符

二进制数只由0 和1 组成, 而且数内的每一个位置都可以表示成数值0 或一个2 的次幂.
在C#中有两种运算符可以用来改变二进制数中位的位置. 它们是:向左移位运算符(<<)和向右移位运算符(>>).
这两种运算符都是对两个运算数进行处理:一个数值(写在左侧)和要移动的位数(写在右侧). 例如, 如果写成如下形式 : 1 << 1, 那么结果就是00000010. 而如果写成2 >> 1就又可以得到原来的结果. 下面再来看一个稍微复杂些的例子.
数值3 的二进制表示形式是 : 00000011, 如果写成3 << 1, 那么结果就是00000110. 而如果写成3 << 2, 那么结果则变成了00001100. 如果写 3 >> 1, 那么结果就是00000001. 后续章节还会介绍如何编写视窗应用程序来举例说明位移运算符的用法.

7、十进制转二进制程序

本节将举例说明如何使用少量的按位运算符来确定一个整数值的位模式. 用户录入一个整数后点击Displaybits按钮. 整数值就会转化成相应的二进制形式显示在标签内, 其中显示的位数是八位一组, 一共四组.
把整数转化为二进制的关键工具就是掩码(mask). 转换函数在显示数的位数时用掩码隐藏掉一些位. 当掩码和整数一起进行And操作后, 就可以得到表示其数值的二进制字符串. 首先来看几个整数值及其所表示的二进制数值:
在这里插入图片描述
在这里插入图片描述
在计算机中负整数的二进制表示并不总是像例子显示的那样简单. 如果想了解更多的内容, 请参阅有关汇编语言和计算机组成方面的书籍。
在这里插入图片描述
正如看到的那样, 上述这个数值65535 是16 位二进制所能表示的最大数值. 如果数值增加到65536, 就会得到下列结果:
在这里插入图片描述
最后再来看看当对存储在C#整数变量内的最大数进行转换的时候究竟会发生什么:
在这里插入图片描述
如果试图录入2147483648, 那么应用程序就会出错. 大家可能会认为最左侧的二进制位是有效的, 但是由于这一位是用来表示正负数的符号位, 所以它是不能用的.
现在来研究一下驱动这个应用程序的代码. 首先会列出代码的内容, 然后再解释程序的工作原理:
(下面的代码计算的是十进制数的二进制补码, 关于二进制和补码的知识, 可以参考原码,反码, 补码 详解)

//原文代码不能运行, 已修正
using System;
using System.Text;
using System.Windows.Forms;

public partial class Digital2Binary : Form {
    public Digital2Binary()
    {
        InitializeComponent();
    }

    //按钮的点击函数
    private void ConvertButton_Click(object sender, EventArgs e)
    {
        int result;            
        result = int.Parse(InputBox.Text);
        ResultLable.Text = ConvertBits(result).ToString();
    }

    private StringBuilder ConvertBits(int val)
    {
        //掩码, 10000000 00000000 00000000 00000000
        int dispMask = 1 << 31;
        //4个8位加三个空格, 长度一共是35
        StringBuilder bitBuffer = new StringBuilder(35);
        for (int i = 1; i <= 32; i++) {
            if ((val & dispMask) == 0)
                bitBuffer.Append("0");
            else
                bitBuffer.Append("1");
            //假设输入的是-1, -1<<1后得到-2,可推出, 按位操作符, 操作的也是补码
            val <<= 1;
            if ((i % 8) == 0)
                bitBuffer.Append(" ");
        }
        return bitBuffer;
    }
}

上述程序执行的大多数工作都在ConvertBits函数内. 变量dispMask保存二进制掩码, 而变量bitBuffer则用来保存转换后的二进制字符串. 为了使用类的 Append 方法而非字符串直接连接的方式来构造二进制字符串, 所以把 bitBuffer 变量声明为StringBuilder类型。
二进制字符串是在For循环中构造的. 由于要构造32 位的字符串, 所以循环要重复进行32 次. 为了构造二进制位字符串, 需要把数值与二进制位掩码进行AND(与)操作. 如果操作的结果为0, 那么就会把0 追加给字符串. 如果结果为1, 则会把1 追加给字符串. 之后为了下一次循环能够处理数字的下一位, 要将数字的值左移动一位. 最后, 为了便于阅读, 会在字符串中每隔八个二进制位就追加一个空格来分隔 四个8 位。

8、移位操作原理演示

本节会用一个视窗程序来举例说明位移运算符的工作原理. 此程序会为提供两个文本框, 分别用于输入要进行移位的数字和要移动的位数, 而且还会用两个文本标签来分别显示左侧运算数的初始二进制表示以及位移操作后结果的二进制形式. 应用程序有两个按钮分别表示向左移操作和向右移操作. 此外, 还有Clear按钮和Exit 按钮.
下面是这个程序的代码:

//原文代码不能运行, 已修正
using System;
using System.Text;
using System.Windows.Forms;

public partial class Form1 : Form {
    public Form1()
    {
        InitializeComponent();
    }

    private StringBuilder ConvertBits(int val)
    {
        //掩码, 10000000 00000000 00000000 00000000
        int dispMask = 1 << 31;
        //4个8位加三个空格, 长度一共是35
        StringBuilder bitBuffer = new StringBuilder(35);
        for (int i = 1; i <= 32; i++) {
            if ((val & dispMask) == 0)
                bitBuffer.Append("0");
            else
                bitBuffer.Append("1");
            //假设输入的是-1, -1<<1后得到-2,可推出, 按位操作符, 操作的也是补码
            val <<= 1;
            if ((i % 8) == 0)
                bitBuffer.Append(" ");
        }
        return bitBuffer;
    }
    //左移
    private void bt_moveL_Click(object sender, EventArgs e)
    {
        int val = int.Parse(tbox_num.Text);
        lab_before.Text = ConvertBits(val).ToString();
        val <<= int.Parse(tbox_bitCount.Text);
        lab_after.Text = ConvertBits(val).ToString();
    }
    //右移
    private void bt_moveR_Click(object sender, EventArgs e)
    {
        int val = int.Parse(tbox_num.Text);
        lab_before.Text = ConvertBits(val).ToString();
        val >>= int.Parse(tbox_bitCount.Text);
        lab_after.Text = ConvertBits(val).ToString();
    }
    //清除文本
    private void bt_clear_Click(object sender, EventArgs e)
    {
        tbox_bitCount.Text = "";
        tbox_num.Text = "";
        lab_after.Text = "";
        lab_before.Text = "";
        tbox_num.Focus();
    }
}

接下来是程序执行中的几个实例. 先是执行4 << 2 :
在这里插入图片描述
接着执行256 >> 8 :
在这里插入图片描述

9、BitArray类

BitArray类用来处理位的集合. 位的集合可以用来有效地表示Boolean(布尔)值的集合. BitArray和ArrayList十分类似, 可以动态地调整元素数量, 所以需要添加二进制位时不用担心数组越界的问题.

9.1、使用BitArray类

通过实例化BitArray就可以创建BitArray对象, 同时也可以通过构造函数指定二进制位的数量:

BitArray BitSet = new BitArray(32); 

以上写法会使BitArray的32 个位都设置为false. 如果想要全部初始化为true, 可以写成下面这样 :

BitArray BitSet = new BitArray(32, true); 

BitArray的构造函数有许多种不同的重载, 我们将要使用的是下面这一种, 用字节(Byte)数组来实例化BitArray对象 :

byte[] ByteSet = new byte[] { 1, 2, 3, 4, 5 }; 
BitArray BitSet = new BitArray(ByteSet); 

BitSet现在包含了字节值为1, 2, 3, 4 和5 的二进制位.
BitArray中数据所代表的二进制的有效位在数组的左侧开始, 即从索引0开始, 并且它们的值是true或false. 这样可能会使人犯糊涂. 例如, 下面这个八位的BitArray的内容就等价为数值1 :
True False False False False False False False
常见的方式是把二进制数的有效位放置在右侧, 并且显示为0或1的数字, 就像下面这样:
0 0 0 0 0 0 0 1
这样就需要自行编写代码来更改每一位的显示内容, 以及它们的显示顺序
下面这个简单的程序段循环遍历了BitArray中的字节, 并将它们直接打印出来:

using System;
using System.Collections;

namespace chapter6 {
    class Program {
        static void Main()
        {
            byte[] ByteSet = new byte[] { 1, 2, 3, 4, 5 };
            BitArray BitSet = new BitArray(ByteSet);
            for (int i = 0; i <= BitSet.Count - 1; i++) {
                if (i % 8 == 0)
                    Console.WriteLine();
                Console.Write(BitSet.Get(i) + " ");
            }
            Console.ReadLine();
        }
    }
}

运行结果如下 :
在这里插入图片描述
就像我们前面说的, 这样显示BitArray的内容, 很难读懂, 而且这也不能真实的反映出数组内存储的情况. 不过马上我们就要通过一些代码, 以一种更容易理解的方式显示BitArray的内容. 但首先需要明白如何从 BitArray中检索到一个位的值.
利用Get方法可以检索到存储在BitArray中的指定位的值. 此方法需要一个整数参数, 代表值的索引, 该方法的返回值为True或False.
如果存储在BitArray中的数据代表的是二进制数值, 那么就需要按照正确的顺序显示 1 和0, 其中正确的顺序就是指从右边开始而不是从左边开始. 由于无法改变BitArray类存储数据所用的内部代码, 所以要编写外部代码来获得希望的输出.
下面这段程序将会把之前代码中的BitArray数据, 以字节为单位户, 按照正确的二进制形式显示出来:

using System;
using System.Collections;

class chapter6 {
    static void Main()
    {
        byte[] ByteSet = new byte[] { 1, 2, 3, 4, 5 };
        BitArray BitSet = new BitArray(ByteSet);
        string[] binNumber = new string[8];
        int bit = 0;
        for (int i = 0; i < BitSet.Count; i++) {
            bit = i % 8;
            if (BitSet.Get(i) == true)
                binNumber[bit] = "1";
            else
                binNumber[bit] = "0";
            //每读取八位就把binNumber数组的内容倒着输出到一行中
            if (bit == 7) {
                for (int j = 7; j >= 0; j--) {
                    Console.Write(binNumber[j]);
                }
                Console.WriteLine();
            }
        }
        Console.ReadLine();
    }
}

代码运行结果如下 :

加粗样式
此程序中, BitSet数组就是保存有字节值. binNumber数组用来保存二进制字符串. 这个二进制字符串是根据每个字节值的二进制位组成的。
每次读取一个位的值, 程序会首先把它转化成为1或0, 存入binNumber的适当索引. 每当已经读取了八个位的值到binNumber中后, 就把其中的字符串元素倒着输出一遍. 输出的时机是靠循环数i对8取模的结果做到的, 如果结果是7, 说明已经读取了8个位的值。

10、其他BitArray类的方法

本小节会讨论BitArray类其他几个方法和属性. 这些方法和属性很可能会在使用

以上是关于C#中BitArray类的主要内容,如果未能解决你的问题,请参考以下文章

C#编程(维数组)----------位数组

C#位图BitArray 小试牛刀

C#中的Collections命名空间

BitArray虽好,但请不要滥用,又一次线上内存暴增排查

c# byte操作

使用BitArray判断素数