簡體   English   中英

Python - 從數字的位表示中去除尾隨零的最快方法

[英]Python - Fastest way to strip the trailing zeros from the bit representation of a number

這是同一個 C++ 問題的 python 版本。

給定一個數字num ,從其二進制表示形式中去除尾隨零的最快方法是什么?

例如,讓num = 232 我們有bin(num)等於0b11101000並且我們想去除尾隨零,這將產生0b11101 這可以通過字符串操作來完成,但通過位操作可能會更快。 到目前為止,我已經想到了使用num & -num的東西

假設num != 0num & -num產生二進制0b1<trailing zeros> 例如,

num   0b11101000
-num  0b00011000
&         0b1000

如果我們有一個以 2 的冪為鍵,以 2 的冪為值的dict ,我們可以使用它來知道將num右移多少,以便僅去除尾隨的零:

#        0b1     0b10     0b100     0b1000
POW2s = {  1: 0,    2: 1,     4: 2,      8: 3, ... }

def stripTrailingZeros(num):
  pow2 = num & -num
  pow_ = POW2s[pow2]  # equivalent to math.log2(pow2), but hopefully faster
  return num >> pow_

使用字典POW2s以空間換取速度——另一種方法是使用math.log2(pow2)


有沒有更快的方法?


也許另一個有用的花絮是num ^ (num - 1)產生0b1!<trailing zeros>其中!<trailing zeros>表示取尾隨零並將它們翻轉為 1。 例如,

num    0b11101000
num-1  0b11100111
^          0b1111

另一種選擇是使用 while 循環

def stripTrailingZeros_iterative(num):
  while num & 0b1 == 0:  # equivalent to `num % 2 == 0`
    num >>= 1
  return num

最終,我需要在一大串數字上執行這個 function。 一旦我這樣做了,我就想要最大的。 因此,如果我開始時有[64, 38, 22, 20] ,那么在執行剝離后我將有[1, 19, 11, 5] 然后我想要其中的最大值,即19

在沒有指定輸入的預期分布的情況下,這樣的問題真的沒有答案。 例如,如果所有輸入都在range(256) ,則無法將單個索引查找打敗到 256 種可能情況的預計算列表中。

如果輸入可以是兩個字節,但您不想為 2**16 預計算結果消耗空間,則很難被擊敗(假設that_table[i]給出i中尾隨零的計數):

low = i & 0xff
result = that_table[low] if low else 8 + that_table[i >> 8]

等等。

不想依賴log2() 其准確性完全取決於編譯 CPython 的平台上的 C 庫。

在 int 可以達到數億位的情況下,我實際使用的是:

    assert d

    if d & 1 == 0:
        ntz = (d & -d).bit_length() - 1
        d >>= ntz

在這種情況下, while循環將是一場災難,它花費的時間是移位位數的二次方。 在這種情況下,即使是一次不必要的移位也將是一筆巨大的開支,這就是為什么上面的代碼首先檢查是否至少有一位需要移位。 但是,如果整數“小得多”,那么該檢查的成本可能會超過它節省的成本。 “在沒有指定輸入的預期分布的情況下沒有答案。”

在我的電腦上,一個簡單的 integer 除法是最快的:

import timeit
timeit.timeit(setup='num=232', stmt='num // (num & -num)')
0.1088077999993402
timeit.timeit(setup='d = { 1: 0, 2 : 1, 4: 2, 8 : 3, 16 : 4, 32 : 5 }; num=232', stmt='num >> d[num & -num]')
0.13014470000052825
timeit.timeit(setup='import math; num=232', stmt='num >> int(math.log2(num & -num))')
0.2980690999993385

你說你“最終,[..] 在一個大的數字列表上執行這個 function 以獲得奇數並找到所述奇數的最大值。”

那么為什么不簡單地:

from random import randint


numbers = [randint(0, 10000) for _ in range(5000)]


odd_numbers = [n for n in numbers if n & 1]
max_odd = max(odd_numbers)
print(max_odd)

最終要按照你說的去做,執行“右移直到結果為奇數”操作似乎沒有什么意義? 除非你想要對所有元素執行該操作的結果的最大值,這不是你所說的?

我同意@TimPeters 的回答,但是如果你把 Python 放在它的步伐中並實際生成一些數據集並嘗試提出的各種解決方案,他們會在使用 Python int s 時保持任意數量的 integer 大小的傳播,所以你最好的選擇是 integer最多 32 位數字的除法,之后見下表:

from pandas import DataFrame
from timeit import timeit
import math
from random import randint


def reduce0(ns):
    return [n // (n & -n)
            for n in ns]


def reduce1(ns, d):
    return [n >> d[n & -n]
            for n in ns]


def reduce2(ns):
    return [n >> int(math.log2(n & -n))
            for n in ns]


def reduce3(ns, t):
    return [n >> t.index(n & -n)
            for n in ns]


def reduce4(ns):
    return [n if n & 1 else n >> ((n & -n).bit_length() - 1)
            for n in ns]


def single5(n):
    while (n & 0xffffffff) == 0:
        n >>= 32
    if (n & 0xffff) == 0:
        n >>= 16
    if (n & 0xff) == 0:
        n >>= 8
    if (n & 0xf) == 0:
        n >>= 4
    if (n & 0x3) == 0:
        n >>= 2
    if (n & 0x1) == 0:
        n >>= 1
    return n


def reduce5(ns):
    return [single5(n)
            for n in ns]


numbers = [randint(1, 2 ** 16 - 1) for _ in range(5000)]
d = {2 ** n: n for n in range(16)}
t = tuple(2 ** n for n in range(16))
assert(reduce0(numbers) == reduce1(numbers, d) == reduce2(numbers) == reduce3(numbers, t) == reduce4(numbers) == reduce5(numbers))

df = DataFrame([{}, {}, {}, {}, {}, {}])
for p in range(1, 16):
    p = 2 ** p
    numbers = [randint(1, 2 ** p - 1) for _ in range(4096)]

    d = {2**n: n for n in range(p)}
    t = tuple(2 ** n for n in range(p))

    df[p] = [
        timeit(lambda: reduce0(numbers), number=100),
        timeit(lambda: reduce1(numbers, d), number=100),
        timeit(lambda: reduce2(numbers), number=100),
        timeit(lambda: reduce3(numbers, t), number=100),
        timeit(lambda: reduce4(numbers), number=100),
        timeit(lambda: reduce5(numbers), number=100)
    ]
    print(f'Complete for {p} bit numbers.')


print(df)
df.to_csv('test_results.csv')

結果(在 Excel 中繪制時): 本地機器結果(更新)

注意這里之前的plot是錯誤的。 雖然沒有代碼和數據,但代碼已更新為包含@MarkRansom 的解決方案。 因為事實證明它是非常大的數字(超過 4k 位數字)的最佳解決方案。

while (num & 0xffffffff) == 0:
    num >>= 32
if (num & 0xffff) == 0:
    num >>= 16
if (num & 0xff) == 0:
    num >>= 8
if (num & 0xf) == 0:
    num >>= 4
if (num & 0x3) == 0:
    num >>= 2
if (num & 0x1) == 0:
    num >>= 1

這里的想法是執行盡可能少的班次。 最初的while循環處理超過 32 位長的數字,我認為這不太可能,但為了完整性必須提供它。 之后每條語句移動一半的位數; 如果你不能移動 16,那么你最多可以移動 15,即 (8+4+2+1)。 這 5 個if語句涵蓋了所有可能的情況。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM