算法分析与设计:二元归并树贪心算法(Huffman树)

K元归并问题

两个分别包含 n n n个和 m m m个记录的已分类文件可以在 O ( n + m ) O(n+m) O(n+m)时间内归并在一起而得到一个分类文件。当要把两个以上的已分类文件归并在一起时,可以通过成对地重复归并已分类的文件来完成。例如:假定 X 1 , X 2 , X 3 , X 4 X_1,X_2,X_3,X_4 X1,X2,X3,X4 是要归并的文件,则可以首先把 X 1 X_1 X1 X 2 X_2 X2归并成文件 Y 1 Y_1 Y1,然后将 Y 1 Y_1 Y1 X 3 X_3 X3归并成 Y 2 Y_2 Y2,最后将 Y 2 Y_2 Y2 X 4 X_4 X4归并,从而得到想要的分类文件;也可以先把 X 1 X_1 X1 X 2 X_2 X2归并成 Y 1 Y_1 Y1,然后将 X 3 X_3 X3 X 4 X_4 X4归并成 Y 2 Y_2 Y2,最后归并 Y 1 Y_1 Y1 Y 2 Y_2 Y2而得到想要的分类文件。给出 n n n个文件,则有许多种把这些文件成对地归并成一个单一分类文件的方法。不同的配对法要求不同的计算时间。

问题是确定一个把 n n n个已分类文件归并在一起的最优方法(即需要最少比较的方法)

像刚才所描述的归并模式称为二路归并模式(每一个归并步包含两个文件的归并)。二路归并模式可以用二元归并树来表示。叶结点被画成方块形,表示已知的文件。这些叶结点称为外部结点。剩下的结点被画成圆圈,称为内部结点。每个内部结点恰好有两个儿子,表示把它的两个儿子所表示的文件归并而得到的文件。每个结点中的数字都是那个结点所表示的文件的长度(记录数)。一个 i i i级结点在距离根为 ( i − 1 ) (i-1) (i1)的地方,在 i i i级结点上的文件的记录都要移动 ( i − 1 ) (i-1) (i1)次。如果 d i d_i di是由根到代表文件 F i F_i Fi的外部结点的距离, q i q_i qi F i F_i Fi的长度,则这颗二元归并树的记录移动总量是 ∑ i = 1 n d i q i \sum_{i=1}^{n}d_i q_i i=1ndiqi 。这个和数叫做这颗树的带权外部路径长度。一个最优二路归并模式与一颗具有最小权外部路径的二元树相对应。


贪心算法

有一类问题是要从所有的允许解中求出最优解,其策略之一是“贪心法”,即逐次实施“贪心选择”:在每个选择步骤上做出的选择都是当前状态下最优的。贪心选择依赖于在此之前所做出的选择,但不依赖于后续步骤所需要的选择,即不依赖于后续待求解子问题。显然,这种选择方法是局部最优的,但不是从问题求解的整体考虑进行选择,因此不能保证最后所得一定是最优解。贪心法是求解问题的一种有效方法,所得到的结果如果不是最优的,通常也是近似最优的。


伪代码

伪代码


C++算法实现源代码

#include <iostream>
#include <iomanip>
#include <vector>
#include <algorithm>
#define INT_MAX 0x7fffffff
using namespace std;


/*****************************************************************
* 函数描述: 打印 vector
* 函数参数: arr——向量
* 函数返回: void
*****************************************************************/
void printVector(vector<int>& arr)
{
    for (vector<int>::iterator it = arr.begin(); it != arr.end(); it++)
    {
        cout << setw(5) << *it << '\t';
    }
    cout << "\n" << endl;
}



class MergeTree
{
private:
    struct MergeNode
    {
        int weight; // 节点权重,记录数
        int parent;
        int lchild, rchild;
    };

public:
    // 构造函数
    MergeTree(vector<int> arr)
    {
        int n = arr.size(); // 求得当前 arr 的节点个数
        count = 2 * n - 1;  // 二叉树的总节点数;
        MoveTotal = 0;      // 移动总量初始化为0
        Node = new MergeNode[count];
        // 使用传入的向量构造叶子节点
        for (int i = 0; i < size(arr); ++i)
        {
            LeafNodes.push_back(arr[i]);
        }
    }

    MergeNode* Node;
    int count;             // 总节点数
    int MoveTotal;         // 移动总量
    vector<int> LeafNodes; // 节点数据
    // 核心算法:构造最优二元归并树
    void Tree();
    // 从 LeafNodes 选取两个最小数
    void selectMin(int& s1, int& s2);
    // 打印树
    void printTree();
    // 标记已经归并的节点,把 LeafNodes 的 i 位置置位为正无穷
    void labelNode(vector<int>& arr, int i) { arr[i] = INT_MAX; }
};


/*****************************************************************
* 函数描述: 选择 vector 中两个最小的数,并将索引号储存在 s1 和 s2 中
* 函数参数: s1, s2——储存当前的两个最小的数的内存地址
* 函数返回: void
*****************************************************************/
void MergeTree::selectMin(int& s1, int& s2)
{
    int Min1 = INT_MAX, Min2 = INT_MAX;
    for (int i = 0; i < LeafNodes.size(); ++i)
    {
        if (LeafNodes[i] < Min1)
        {
            if (Min1 < Min2)
            {
                Min2 = Min1;
                s2 = s1;
            }
            Min1 = LeafNodes[i];
            s1 = i;
        }
        else if (LeafNodes[i] < Min2)
        {
            Min2 = LeafNodes[i];
            s2 = i;
        }
        else
            continue;
    }
}

/*****************************************************************
* 函数描述: 构造二元归并的最优解法树
* 函数参数: null
* 函数返回: void
*****************************************************************/
void MergeTree::Tree()
{
    sort(LeafNodes.begin(), LeafNodes.end()); // 将 arr 升序排列
    int n = LeafNodes.size();                 // 求得当前 arr 的节点个数
    //初始化,所有节点没有父节点和左右子节点
    for (int i = 0; i < count; ++i)
    {
        Node[i].parent = -1;
        Node[i].lchild = -1;
        Node[i].rchild = -1;
    }
    for (int i = n; i < count; ++i)
    {
        int i1, i2;
        selectMin(i1, i2);
        Node[i1].weight = LeafNodes[i1];
        Node[i2].weight = LeafNodes[i2];
        Node[i1].parent = i;
        Node[i2].parent = i;
        int mid = Node[i1].weight + Node[i2].weight;
        Node[i].lchild = i1;
        Node[i].rchild = i2;
        Node[i].weight = mid;
        // 把 i1 和 i2 位置置位为正无穷
        labelNode(LeafNodes, i1);
        labelNode(LeafNodes, i2);
        // 插入新的节点
        LeafNodes.insert(LeafNodes.begin() + i, mid);
        // 累加移动总量
        MoveTotal += mid;
    }
}

/*****************************************************************
* 函数描述: 打印最优解法树
* 函数参数: null
* 函数返回: void
*****************************************************************/
void MergeTree::printTree()
{
    cout << "移动总量为:" << MoveTotal << endl;
    for (int i = 0; i < count; ++i)
    {
        cout << setw(7) << "节点:" << Node[i].weight << "\t";
        cout << setw(7) << "父节点:" << Node[Node[i].parent].weight << "\t";
        cout << setw(7) << "左:" << Node[Node[i].lchild].weight << "\t";
        cout << setw(7) << "右:" << Node[Node[i].rchild].weight << endl;
    }
}


int main()
{
    vector<int> arr;
    int test[] = { 15, 30, 10, 8, 40, 13 };
    for (int i = 0; i < size(test); ++i)
    {
        arr.push_back(test[i]);
    }

    cout << "需要进行二元归并的数据:" << endl;
    printVector(arr);

    // 使用 vector 数据实例化一个归并树
    MergeTree mt(arr);
    // 进行二元最优归并
    mt.Tree();
    // 打印结果
    mt.printTree();

    return 0;
}

运行结果

下图展示了需要归并的文件长度为{ 15, 30, 10, 8, 40, 13 }的6个文件所产生的二元归并树结果。由图可知,最小的移动总量次数为278次。
二元归并树结果
下图展示了将上述运行结果还原成二叉树示意图。其中使用方块表示已知节点,所有的已知节点都在叶子节点的位置。使用圆圈表示生产的新节点,即为内部节点。所有的内部节点之和为移动总量278。

归并树示意图


数据结构

选用vector二叉树的结构对问题进行表示。

C++语言中的STL里面的vector是一种连续的空间存储,可以使用“[]”操作符快速的访问随机的元素,快速的在末尾插入元素,但是在序列中间的插入,删除元素要慢,其vector的性质类似于一个数组。这里选用vector而不是链表的原因是,在算法中需要多次查找最小的两个数,需要频繁进行随机访问元素。

二叉树的数据结构是自定义的,为了简单方便,此处并未用到链表的指针形式来构造二叉树。之前《数据结构》上讲的是使用链表来构建二叉树,但是其代码量很大,还需要专门构造迭代器(Iterator),众所周知,C++一牵扯到指针就很容易出错(说到底还是博主太菜了)。而且该问题只是记录二叉树的信息,并不需要一个很完整的二叉树链表去实现其增删改查的功能,所以只是简单的用一个struct记录了其父节点和子节点在vector中的索引信息,这里类似于一个哈希散列映射。

储存内容、父节点、子节点的struct结构


总结

二元归并树算法,其贪心重点体现在每次选取的归并文件都是最小的文件。直观地看,先选择权值小的,所以权值小的结点被放置在树的较深层(叶子节点),而权值较大的离根较近,这样一来,在计算树的带权路径长度时,自然会具有最小的带权路径长度。

其实这种最优二叉归并树有一个更加学术的名字,叫做哈夫曼树(Huffman),它的定义是由n个带权叶子结点构成的所有二叉树中带权路径长度最小的二叉树。其定义便是二元归并树的最优解。这种数据结构在现实生活中十分常用,例如哈夫曼编码、数据的解压缩等等。


延伸——K元归并树

关于k元归并树我也查了一些资料,其思路和二元的类似,都是将数据排序后寻找最小的k个元素进行归并。但是,有一个细节要注意的就是,在最后一次选取的时候可能元素个数不满足k个。为了解决这个问题,在算法的开始,我们就先取掉几个数,使得剩下的数正好能每次取k个取完。

具体算法见:https://blog.csdn.net/angon823/article/details/52598438

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值