实现霍夫曼编码算法

哈夫曼编码是一种压缩技术,用于减少发送或存储信息或字符串所需的比特数。它基于这样一种理念:频繁出现的字符将具有较短的比特表示,而较少出现的字符将具有较长的比特表示。如果我们把哈夫曼编码看作一个树形结构,那么出现频率较低的字符或项目将位于树的顶端,而出现频率较高的项目将位于树的底部或树叶中。哈夫曼编码在很大程度上依赖于优先级队列。要计算哈夫曼编码,首先要创建一棵节点树。

创建节点树的过程:

  1. 我们必须为每个符号创建一个叶节点并将其添加到优先级队列中。

  2. 当队列中有多个节点时,请执行以下操作:

    • 将优先级最高(概率/频率最低)的节点删除两次,得到两个节点。

    • 创建一个新的内部节点,将这两个节点作为子节点,其概率/频率等于这两个节点的概率/频率之和。

    • 将新节点添加到队列中。

  3. 剩下的节点就是根节点,树就完整了。

然后,我们必须从树根到树叶遍历所构建的二叉树,在每个节点为一个分支分配和累积一个 "0",为另一个分支分配和累积一个 "1"。每片叶子上累积的 0 和 1 就构成了这些符号和权重的哈夫曼编码。下面是利用 SPL 优先级队列实现的哈夫曼编码算法:

Unresolved include directive in modules/ROOT/pages/ch11/ch11-05.adoc - include::example$Chapter11/4.php[]

在这里,我们为每个符号建立一个最小堆,并使用它们的权重来设置优先级。一旦堆构建完成,我们就会一个接一个地提取两个节点,然后结合它们的数据和优先级,将其添加回堆。除非只存在一个节点,也就是根节点,否则这个过程会一直持续下去。现在,让我们运行以下代码来生成哈夫曼代码:

Unresolved include directive in modules/ROOT/pages/ch11/ch11-05.adoc - include::example$Chapter11/4.php[]

在这里,我们使用 str_split 将字符串分解为数组,然后使用数组计数值将其转换为关联数组,其中字符是键,字符在字符串中出现的次数是值。前面的代码将产生以下输出:

Symbol Weight Huffman Code
i         1           00000
D         1           00001
d         1           00010
A         1           00011
t         4           001
H         1           01000
m         1           01001
P         2           0101
g         1           01100
o         1           01101
e         1           01110
n         1           01111
7         1           10000
l         1           10001
u         2           1001
          5           101
h         1           11000
c         1           11001
a         3           1101
r         3           1110
s         3           1111

贪心算法还有许多其他实际用途。我们将用贪心算法解决一个工作调度问题。让我们以一个敏捷软件开发人员团队为例,他们正在进行为期两周的迭代或冲刺。他们有一些用户故事要完成,任务有一些截止日期(按日期),故事有一些速度(故事的大小)。团队的目标是在给定的期限内获得冲刺的最大速度。让我们考虑以下带有截止日期和速度的任务:

Index

1

2

3

4

5

6

Story

S1

S2

S3

S4

S5

S6

Deadline

2

1

2

1

3

4

Velocity

95

32

47

42

28

64

从上表中可以看出,我们有 6 个用户故事,它们的截止日期从 1 到 4 有 4 个不同的时间段。 我们必须在第 1 个时间段完成用户故事 S2 或 S4,因为该任务的截止日期是 1。 故事 S1 和 S3 也是如此,它们必须在第 2 个时间段或之前完成。但是,由于我们有 S3,而且 S3 的速度大于 S2 和 S4,因此贪婪法将选择 S3 作为第 1 个时隙。让我们来编写计算速度的贪心代码:

Unresolved include directive in modules/ROOT/pages/ch11/ch11-05.adoc - include::example$Chapter11/5.php[]

在这里,我们将获得作业列表(用户故事 ID、截止日期和速度),并利用这些信息找出最大速度及其各自的用户故事 ID。首先,我们使用自定义用户排序功能 usort 对作业数组进行排序,并根据它们的速度按降序排序。然后,我们从截止日期列计算可用槽位的最大数量。然后,我们将槽数组初始化为-1,以保留已用槽的标记。下一个代码块将遍历每个用户故事,并为用户故事找到合适的时隙。如果可用时隙已满,我们就不再继续。现在,让我们使用下面的代码块运行这段代码:

Unresolved include directive in modules/ROOT/pages/ch11/ch11-05.adoc - include::example$Chapter11/5.php[]

这将在命令行中产生以下输出:

Stories to Complete: S3 S1 S5 S6
Max Velocity: 234

贪婪算法有助于解决局部优化问题,如工作调度、网络流量控制、图算法等。然而,要获得全局优化的解决方案,我们需要关注算法的另一个方面,即动态编程。