1. 程式人生 > >Python: 實現bitmap資料結構

Python: 實現bitmap資料結構

這是根據網上看的一篇文章,對程式碼進行了修改。

bitmap是很常用的資料結構,比如用於Bloom Filter中、用於無重複整數的排序等等。bitmap通常基於陣列來實現,陣列中每個元素可以看成是一系列二進位制數,所有元素組成更大的二進位制集合。對於Python來說,整數型別預設是有符號型別,所以一個整數的可用位數為31位。

bitmap實現思路

bitmap是用於對每一位進行操作。舉例來說,一個Python陣列包含4個32位有符號整型,則總共可用位為4 * 31 = 124位。如果要在第90個二進位制位上操作,則要先獲取到運算元組的第幾個元素,再獲取相應的位索引,然後執行操作。

上圖所示為一個32位整型,在Python中預設是有符號型別,最高位為符號位,bitmap不能使用它。左邊是高位,右邊是低位,最低位為第0位。

初始化bitmap

首先需要初始化bitmap。拿90這個整數來說,因為單個整型只能使用31位,所以90除以31並向上取整則可得知需要幾個陣列元素。程式碼如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size = int((max + 31 - 1) / 31) #向上取整

if __name__ == '__main__':
	bitmap = Bitmap(90)
	print '需要 %d 個元素。' % bitmap.size
$ python bitmap.py
需要 3 個元素。

計算索引

確定了陣列大小後,也就可以建立這個陣列了。如果要將一個整數儲存進這個陣列,首先需要知道儲存在這個陣列的第幾個元素之上,然後要知道是在這個元素的第幾位上。因此計算索引分為:

  1. 計算在陣列中的索引

  2. 計算在陣列元素中的位索引

計算在陣列中的索引

計算在陣列中的索引其實是跟之前計算陣列大小是一樣的。只不過之前是對最大數計算,現在換成任一需要儲存的整數。但是有一點不同,計算在陣列中的索引是向下取整,所以需要修改calcElemIndex方法的實現。程式碼改為如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

if __name__ == '__main__':
	bitmap = Bitmap(90)
	print '陣列需要 %d 個元素。' % bitmap.size
	print '47 應儲存在第 %d 個數組元素上。' % bitmap.calcElemIndex(47)
$ python bitmap.py
陣列需要 3 個元素。
47 應儲存在第 1 個數組元素上。

所以獲取最大整數很重要,否則有可能建立的陣列容納不下某些資料。

計算在陣列元素中的位索引

陣列元素中的位索引可以通過取模運算來得到。令需儲存的整數跟31取模即可得到位索引。程式碼改為如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

if __name__ == '__main__':
	bitmap = Bitmap(90)
	print '陣列需要 %d 個元素。' % bitmap.size
	print '47 應儲存在第 %d 個數組元素上。' % bitmap.calcElemIndex(47)
	print '47 應儲存在第 %d 個數組元素的第 %d 位上。' % (bitmap.calcElemIndex(47), bitmap.calcBitIndex(47),)
$ python bitmap.py
陣列需要 3 個元素。
47 應儲存在第 1 個數組元素上。
47 應儲存在第 1 個數組元素的第 16 位上。

別忘了是從第0位算起哦。

置1操作

二進位制位預設是0,將某位置1則表示在此位儲存了資料。程式碼改為如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

	def set(self, num):
		elemIndex = self.calcElemIndex(num)
		byteIndex = self.calcBitIndex(num)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem | (1 << byteIndex)

if __name__ == '__main__':
	bitmap = Bitmap(90)
	bitmap.set(0)
	print bitmap.array
$ python bitmap.py
[1, 0, 0]

因為從第0位算起,所以如需要儲存0,則需要把第0位置1。

清0操作

將某位置0,也即丟棄已儲存的資料。程式碼如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

	def set(self, num):
		elemIndex = self.calcElemIndex(num)
		byteIndex = self.calcBitIndex(num)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem | (1 << byteIndex)

	def clean(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem & (~(1 << byteIndex))

if __name__ == '__main__':
	bitmap = Bitmap(87)
	bitmap.set(0)
	bitmap.set(34)
	print bitmap.array
	bitmap.clean(0)
	print bitmap.array
	bitmap.clean(34)
	print bitmap.array
$ python bitmap.py
[1, 8, 0]
[0, 8, 0]
[0, 0, 0]

清0和置1是互反操作。

測試某位是否為1

判斷某位是否為1是為了取出之前所儲存的資料。程式碼如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

	def set(self, num):
		elemIndex = self.calcElemIndex(num)
		byteIndex = self.calcBitIndex(num)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem | (1 << byteIndex)

	def clean(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem & (~(1 << byteIndex))

	def test(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		if self.array[elemIndex] & (1 << byteIndex):
			return True
		return False

if __name__ == '__main__':
	bitmap = Bitmap(90)
	bitmap.set(0)
	print bitmap.array
	print bitmap.test(0)
	bitmap.set(1)
	print bitmap.test(1)
	print bitmap.test(2)
	bitmap.clean(1)
	print bitmap.test(1)
$ python bitmap.py
[1, 0, 0]
True
True
False
False

接下來實現一個不重複陣列的排序。已知一個無序非負整數陣列的最大元素為879,請對其自然排序。程式碼如下:

#!/usr/bin/env python
#coding: utf8

class Bitmap(object):
	def __init__(self, max):
		self.size  = self.calcElemIndex(max, True)
		self.array = [0 for i in range(self.size)]

	def calcElemIndex(self, num, up=False):
		'''up為True則為向上取整, 否則為向下取整'''
		if up:
			return int((num + 31 - 1) / 31) #向上取整
		return num / 31

	def calcBitIndex(self, num):
		return num % 31

	def set(self, num):
		elemIndex = self.calcElemIndex(num)
		byteIndex = self.calcBitIndex(num)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem | (1 << byteIndex)

	def clean(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		elem      = self.array[elemIndex]
		self.array[elemIndex] = elem & (~(1 << byteIndex))

	def test(self, i):
		elemIndex = self.calcElemIndex(i)
		byteIndex = self.calcBitIndex(i)
		if self.array[elemIndex] & (1 << byteIndex):
			return True
		return False

if __name__ == '__main__':
	MAX = 879
	suffle_array = [45, 2, 78, 35, 67, 90, 879, 0, 340, 123, 46]
	result       = []
	bitmap = Bitmap(MAX)
	for num in suffle_array:
		bitmap.set(num)
	
	for i in range(MAX + 1):
		if bitmap.test(i):
			result.append(i)

	print '原始陣列為:    %s' % suffle_array
	print '排序後的陣列為: %s' % result
$ python bitmap.py
原始陣列為:   [45, 2, 78, 35, 67, 90, 879, 0, 340, 123, 46]
排序後的陣列為:[0, 2, 35, 45, 46, 67, 78, 90, 123, 340, 879]

結束語

bitmap實現了,則利用其進行排序就非常簡單了。其它語言也同樣可以實現bitmap,但對於靜態型別語言來說,比如C/Golang這樣的語言,因為可以直接宣告無符號整型,所以可用位就變成32位,只需將上述程式碼中的31改成32即可,這點請大家注意。

我對程式碼修改改了兩部分程式碼

如下

class Bitmap(object):
    def __init__(self, max):
        self.size = self.calcElemIndex(max)
        self.array = [0 for _ in range(self.size)]

    def calcElemIndex(self, num):
        """
        加 1 的原因是,0 陣列中也可能包含 0
        所以應該向上取整
        :param num:
        :return:
        """
        # return int((num + 31 - 1) / 31)  # 向上取整
        return (num + 1) // 31 + 1

    def calcBitIndex(self, num):
        return num % 31

    def set(self, num):
        elemIndex = self.calcElemIndex(num) - 1
        byteIndex = self.calcBitIndex(num)
        print(f"self.array is {self.array}, size is {len(self.array)}")
        print(f'eleIndex is {elemIndex}')
        elem = self.array[elemIndex]
        # 為什麼要用 | 這個的知道,
        # 這個可以表示集合的或
        # 還可以表示二進位制的或
        self.array[elemIndex] = elem | (1 << byteIndex)

    def test(self, i):
        eleIndex = self.calcElemIndex(i) - 1
        byteIndex = self.calcBitIndex(i)
        elem = self.array[eleIndex]
        return True if elem & (1 << byteIndex) == (1 << byteIndex) else False


nums = [45, 2, 78, 35, 67, 90, 879, 0, 340, 123, 46]
MAX = 879
bitmap = Bitmap(MAX)
for num in nums:
    bitmap.set(num)

result = []
for i in range(MAX + 1):
    if bitmap.test(i):
        result.append(i)

print(result)