【问题标题】:Mapping a list to a Huffman Tree whilst preserving relative order将列表映射到霍夫曼树,同时保持相对顺序
【发布时间】:2013-11-26 17:07:19
【问题描述】:

我在使用霍夫曼树的搜索算法时遇到问题:对于给定的概率分布,我需要霍夫曼树是相同的,而不管输入数据的排列如何。

这是正在发生的事情与我想要的图片:

基本上我想知道是否可以保留从列表到树的项目的相对顺序。如果不是,为什么会这样?

作为参考,我使用霍夫曼树根据概率划分生成子组,以便我可以运行下面的 search() 过程。请注意,merge() 子例程中的数据与权重一起被合并。代码字本身不如树重要(应该保持相对顺序)。

例如,如果我生成以下霍夫曼代码:

probabilities = [0.30, 0.25, 0.20, 0.15, 0.10]
items = ['a','b','c','d','e']
items = zip(items, probabilities)
t = encode(items)
d,l = hi.search(t)
print(d)

使用以下类:

class Node(object):
    left = None
    right = None
    weight = None
    data = None
    code = None

    def __init__(self, w,d):
        self.weight = w
        self.data = d

    def set_children(self, ln, rn):
        self.left = ln
        self.right = rn

    def __repr__(self):
        return "[%s,%s,(%s),(%s)]" %(self.data,self.code,self.left,self.right)

    def __cmp__(self, a):
        return cmp(self.weight, a.weight)

    def merge(self, other):
        total_freq = self.weight + other.weight
        new_data = self.data + other.data
        return Node(total_freq,new_data)

    def index(self, node):
        return node.weight

def encode(symbfreq):
    pdb.set_trace()
    tree = [Node(sym,wt) for wt,sym in symbfreq]
    heapify(tree)
    while len(tree)>1:
        lo, hi = heappop(tree), heappop(tree)
        n = lo.merge(hi)
        n.set_children(lo, hi)
        heappush(tree, n)
    tree = tree[0]

    def assign_code(node, code):
        if node is not None:
            node.code = code
        if isinstance(node, Node):
            assign_code(node.left, code+'0')
            assign_code(node.right, code+'1')

    assign_code(tree, '')
    return tree

我明白了:

'a'->11
'b'->01
'c'->00
'd'->101
'e'->100

但是,我在搜索算法中所做的假设是,更有可能的项目被推向左侧:也就是说,我需要“a”才能拥有“00”代码字——无论'abcde' 序列的任何排列。一个示例输出是:

codewords = {'a':'00', 'b':'01', 'c':'10', 'd':'110', 'e':111'}

(请注意,即使“c”的代码字是“d”的后缀,也可以)。

为了完整起见,这里是搜索算法:

def search(tree):
    print(tree)
    pdb.set_trace()
    current = tree.left
    other = tree.right
    loops = 0
    while current:
        loops+=1
        print(current)
        if current.data != 0 and current is not None and other is not None:
            previous = current
            current = current.left
            other = previous.right
        else:
            previous = other
            current = other.left
            other = other.right
    return previous, loops

它的工作原理是在一组 0 和 1 中搜索“最左边的”1 - 霍夫曼树必须将更多可能的项目放在左边。例如,如果我使用上面的概率和输入:

items = [1,0,1,0,0]

那么算法返回的项目的索引是 2 - 这不是应该返回的(应该是 0,因为它在最左边)。

【问题讨论】:

  • 你所画的树并不是你所得到的。例如。左侧的树显示 d 的值为 111,但 d 的值为 101。
  • @MarkAdler,对不起。我已经盯着这个看了3天,现在从下到上都说不出来。你会原谅一个小小的失误吗?
  • 您的示例代码早期有t = hi.encode(items),但未定义hi。这是什么?
  • 顺便说一句,你从我之前给你的代码中拿走了一些东西,现在这会非常有用:区分 Symbol 节点和人工(内部)霍夫曼节点,和一个将符号名称映射到其Symbol 节点的字典。拥有这两者会让你想要的现在或多或少简单明了。
  • @TimPeters 抱歉,应该是 t = encode(items)

标签: python data-structures huffman-code


【解决方案1】:

通常的做法是仅使用 Huffman 算法来生成代码长度。然后使用规范过程从长度生成代码。树被丢弃。代码是按从短代码到长代码的顺序分配的,在一个代码内,对符号进行排序。这给出了您期望的代码,a = 00b = 01 等。这称为Canonical Huffman code

这样做的主要原因是为了使霍夫曼码的传输更加紧凑。无需将每个符号的代码与压缩数据一起发送,您只需发送每个符号的代码长度。然后可以在另一端重构代码进行解压。

Huffman 树通常也不用于解码。使用规范代码、简单比较以确定下一个代码的长度,以及使用代码值的索引将直接带您到符号。或者表驱动的方法可以避免搜索长度。

至于您的树,当频率相等时会做出任意选择。特别是,在第二步中,第一个被拉出的节点是c,概率为 0.2,第二个被拉出的节点是b,概率为 0.25。然而,拉动第一步中创建的节点(e,d) 而不是b 也同样有效,其概率也是 0.25。事实上,这就是您想要的最终状态。唉,你已经放弃了对 heapq 库的任意选择的控制权。

(注意:由于您使用的是浮点值,因此 0.1 + 0.15 不一定完全等于 0.25。尽管事实证明确实如此。再举一个例子,0.1 + 0.2 等于0.3. 如果您想了解当频率总和等于其他频率或频率总和时会发生什么,最好使用整数作为频率。例如 6,5,4,3,2。)

可以通过修复一些错误来修复一些错误的顺序:将lo.merge(high) 更改为hi.merge(lo),并将位的顺序反转为:assign_code(node.left, code+'1') 后跟assign_code(node.right, code+'0')。那么至少a 被分配00 并且de 之前并且bc 之前。则排序为adebc

现在我想一想,即使您选择 (e,d) 而不是 b,例如通过将 b 的概率设置为 0.251,您仍然无法获得您所追求的完整订单。不管怎样,(e,d) (0.25) 的概率大于c (0.2) 的概率。因此,即使在这种情况下,最终的排序也将是(带有上述修复)abdec,而不是您想要的abcde。因此,不可能得到你想要的,假设一个一致的树顺序和关于符号组概率的位分配。例如,假设对于每个分支,左侧的东西比右侧的东西具有更大或相等的概率,并且总是将 0 分配给左侧,并且始终将 1 分配给右侧。你需要做一些不同的事情。

我想到的不同的事情是我在答案开始时所说的。使用 Huffman 算法来获取代码长度。然后,您可以按照您喜欢的任何顺序将代码分配给符号,并构建一棵新树。这比试图提出某种方案来强制原始树成为你想要的,并证明它在所有情况下都有效要容易得多。

【讨论】:

  • 我对代码本身并不感兴趣:这是我用来搜索的树。将原始列表中最左边的项目映射到树中最左边的项目非常重要。
  • 我的意思是你不需要树。你为什么关心这棵树?究竟是什么让它“真正重要”?
  • 我在乎,因为我使用它来编码项目组以根据它们的相对概率对其执行属性测试。该算法仅在从列表映射到树时保留项目的相对位置时才给出正确答案。我已经给出了在他的问题中搜索树的算法。
【解决方案2】:

我将用工作代码充实 Mark Adler 所说的内容。他说的都是对的 :-) 高点:

  1. 不得使用浮点权重或任何其他会丢失权重信息的方案。使用整数。简单而正确。例如,如果您有 3 位浮点概率,请通过 int(round(the_probability * 1000)) 将每个概率转换为整数,然后可能会调整它们以确保总和正好是 1000。
  2. heapq 堆不是“稳定的”:如果多个项目具有相同的最小重量,则没有定义弹出哪个项目。
  3. 所以在构建树的同时你无法得到你想要的。
  4. “规范霍夫曼代码”的一个小变体似乎是您确实想要的。为此构建一棵树是一个冗长的过程,但每一步都很简单。构建的第一棵树被丢弃:从中获取的唯一信息是分配给每个符号的代码的长度

跑步:

syms = ['a','b','c','d','e']
weights = [30, 25, 20, 15, 10]
t = encode(syms, weights)
print t

打印这个(为便于阅读而格式化):

[abcde,,
    ([ab,0,
        ([a,00,(None),(None)]),
        ([b,01,(None),(None)])]),
    ([cde,1,
        ([c,10,(None),(None)]),
        ([de,11,
            ([d,110,(None),(None)]),
            ([e,111,(None),(None)])])])]

据我所知,这正是您想要的。如果不是,请抱怨;-)

编辑:规范代码的分配存在错误,除非权重非常不同,否则不会显示;修好了。

class Node(object):
    def __init__(self, data=None, weight=None,
                       left=None, right=None,
                       code=None):
        self.data = data
        self.weight = weight
        self.left = left
        self.right = right
        self.code = code

    def is_symbol(self):
        return self.left is self.right is None

    def __repr__(self):
        return "[%s,%s,(%s),(%s)]" % (self.data,
                                      self.code,
                                      self.left,
                                      self.right)

    def __cmp__(self, a):
        return cmp(self.weight, a.weight)

def encode(syms, weights):
    from heapq import heapify, heappush, heappop

    tree = [Node(data=s, weight=w)
            for s, w in zip(syms, weights)]
    sym2node = {s.data: s for s in tree}
    heapify(tree)
    while len(tree) > 1:
        a, b = heappop(tree), heappop(tree)
        heappush(tree, Node(weight=a.weight + b.weight,
                            left=a, right=b))

    # Compute code lengths for the canonical coding.
    sym2len = {}
    def assign_codelen(node, codelen):
        if node is not None:
            if node.is_symbol():
                sym2len[node.data] = codelen
            else:
                assign_codelen(node.left, codelen + 1)
                assign_codelen(node.right, codelen + 1)
    assign_codelen(tree[0], 0)

    # Create canonical codes, but with a twist:  instead
    # of ordering symbols alphabetically, order them by
    # their position in the `syms` list.
    # Construct a list of (codelen, index, symbol) triples.
    # `index` breaks ties so that symbols with the same
    # code length retain their original ordering.
    triples = [(sym2len[name], i, name)
                for i, name in enumerate(syms)]
    code = oldcodelen = 0
    for codelen, _, name in sorted(triples):
        if codelen > oldcodelen:
            code <<= (codelen - oldcodelen)
        sym2node[name].code = format(code, "0%db" % codelen)
        code += 1
        oldcodelen = codelen

    # Create a tree corresponding to the new codes.
    tree = Node(code="")
    dir2attr = {"0": "left", "1": "right"}
    for snode in sym2node.values():
        scode = snode.code
        codesofar = ""
        parent = tree
        # Walk the tree creating any needed interior nodes.
        for d in scode:
            assert parent is not None
            codesofar += d
            attr = dir2attr[d]
            child = getattr(parent, attr)
            if codesofar == scode:
                # We're at the leaf position.
                assert child is None
                setattr(parent, attr, snode)
            elif child is not None:
                assert child.code == codesofar
            else:
                child = Node(code=codesofar)
                setattr(parent, attr, child)
            parent = child

    # Finally, paste the `data` attributes together up
    # the tree.  Why?  Don't know ;-)
    def paste(node):
        if node is None:
            return ""
        elif node.is_symbol():
            return node.data
        else:
            result = paste(node.left) + paste(node.right)
            node.data = result
            return result
    paste(tree)

    return tree

重复符号

我可以把 sym2node 字典换成有序字典来处理吗 重复'a'/'b'等?

不,有两个原因:

  1. 没有映射类型支持重复键;并且,
  2. “重复符号”的概念对霍夫曼编码毫无意义。

因此,如果您决心 ;-) 追求这一点,首先您必须确保符号是独一无二的。只需在函数开头添加这一行:

        syms = list(enumerate(syms))

例如,如果传入的syms是:

['a', 'b', 'a']

这将变为:

[(0, 'a'), (1, 'b'), (2, 'a')]

所有符号现在都是 2 元组,并且显然是唯一的,因为每个符号都以唯一的整数开头。 算法 唯一关心的是符号可以用作字典键;它不在乎它们是字符串、元组还是任何其他支持相等性测试的可散列类型。

因此,算法中的任何内容都不需要更改。但在结束之前,我们要恢复原始符号。只需在 paste() 函数之前插入:

    def restore_syms(node):
        if node is None:
            return
        elif node.is_symbol():
            node.data = node.data[1]
        else:
            restore_syms(node.left)
            restore_syms(node.right)
    restore_syms(tree)

这只是遍历树并从符号的.data 成员中去除前导整数。或者,也许更简单,只需遍历 sym2node.values(),并转换每个的 .data 成员。

【讨论】:

  • 我现在不在电脑旁,但这个周末我会进行测试。非常感谢。
  • 这正是我所需要的,非常感谢!我可以将 sym2node 字典交换为有序字典以处理重复的“a”/“b”等吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-28
  • 1970-01-01
  • 2020-03-19
  • 1970-01-01
  • 1970-01-01
  • 2014-03-18
  • 1970-01-01
相关资源
最近更新 更多