数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)

文章目录

  • 1. 并查集
  • 2. 哈夫曼编码树

1. 并查集

并查集是一个多棵树的集合(森林)。

并查集由多个集合构成,每一个集合就是一颗树。
并:合并多个集合。查:判断两个值是否再一个集合中。

每棵树存在数组中,使用双亲表示法。数组每个元素的父节点。如果没有父节点数组保存-1。根节点位置的数组值就算这颗树节点的个数。
eg:
数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第1张图片

根据上图分析可知:
如果要合并的节点x和y不在一个树上
ufs[root_x] += ufs[root_y];(将y这个节点对应的子树和x的节点进行合并)
ufs[root_y] = root_x;(改变y这个节点的父亲节点)

根据上文可知,并查集查找的速度与树的高度有关。

当查找节点后,如果能将这个节点到根节点路径上的节点都直接插入到根节点上,则可以显著降低树的高度,从而提高效率。

数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第2张图片

上述过程在查找节点的根节点时实现,找到根节点时先不返回,再遍历一遍更新节点的父节点即可。

把这个节点到根节点路径上的所有节点直接插入到根节点上
while (data != root)
{
    int parent = ufs[data];
    ufs[data] = root;
    data = parent;
}

C++代码:

//构建并查集

#include 
#include 
#include 

class UnionFindSet
{
private:
    //数组的下标保存的是并查集的数据,数组的值记录的是并查集这个节点的父节点下标
    std::vector<int> ufs;

public:
    UnionFindSet(size_t size)
    {
        ufs.resize(size, -1);
    }

    // x和y所在的两个集合合并
    void Union(int x, int y)
    {
        assert(x < ufs.size() && y < ufs.size());
        int root_x = FindRoot(x);
        int root_y = FindRoot(y);

        if (root_x != root_y)
        {
            //不在一棵树上
            ufs[root_x] += ufs[root_y];
            ufs[root_y] = root_x;
        }
    }

    //找data的根
    int FindRoot(int data)
    {
        int root = data;
        while (ufs[root] >= 0)
        {
            root = ufs[root];
        }

        //找到根后,这里做优化,降低并查集树的高度
        //把这个节点到根节点路径上的所有节点插入到根节点上
        while (data != root)
        {
            int parent = ufs[data];
            ufs[data] = root;
            data = parent;
        }
        return root;
    }

    //获取并查集中树的个数
    int GetTreeSize()
    {
        int ret = 0;
        for (int i = 0; i < ufs.size(); i++)
        {
            if (ufs[i] < 0)
                ret += 1;
        }
        return ret;
    }

    //打印并查集信息
    void PrintUfs()
    {
        for (int i = 0; i < ufs.size(); i++)
        {
            printf("%2d ", i);
        }
        printf("\n");
        for (int i = 0; i < ufs.size(); i++)
        {
            printf("%2d ", ufs[i]);
        }
        printf("\n");
    }
};
#include "UnionFindSet.h"
#include 
int main(int argc, char const *argv[])
{
    UnionFindSet set(9);

    for (int times = 0; times < 8; times++)
    {
    	//打印并查集树的个数
        std::cout << set.GetTreeSize() << std::endl;
        set.PrintUfs();
        set.Union(times, times + 1);
    }

    std::cout << set.GetTreeSize() << std::endl;
    set.PrintUfs();

    return 0;
}

数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第3张图片

2. 哈夫曼编码树

哈夫曼树:
在许多应用中,树中结点常常被赋予一个表示某种意义的数值,称为该结点的权。

从树的根到任意结点的路径长度(经过的边数)与该结点上权值的乘积,称为该结点的带权路径长度。

树中所有叶结点的带权路径长度之和称为该树的带权路径长度。

数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第4张图片

哈夫曼编码:
哈夫曼编码就是在哈夫曼树的基础上构建的,这种编码方式最大的优点就是用最少的字符包含最多的信息内容,进而实现信息的压缩存储。

根据发送信息的内容,通过统计文本中相同字符的个数作为每个字符的权值,建立哈夫曼树。对于树中的每一个子树,统一规定其左孩子标记为 0 ,右孩子标记为 1 。这样,用到哪个字符时,从哈夫曼树的根结点开始,依次写出经过结点的标记,最终得到的就是该结点的哈夫曼编码。

文本中字符出现的次数越多,在哈夫曼树中的体现就是越接近树根。编码的长度越短。

eg:
数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第5张图片

这是用权值分别为 7、5、2、4 的字符 a、b、c、d 构建的哈夫曼树。

显然,字符 a 用到的次数最多,所以它对应的哈弗曼编码应最短,这里用 0 表示;其次,是字符 b 用的多,因此字符 b 编码为 10 ,
以此类推,字符 c 的编码为 110 ,字符 d 的编码为 111。

权值越大,表示此字符在文件中出现的次数越多,那么,为了实现用最少的字符包含最多的内容,就应该给出现次数越多的字符,分配的哈弗曼编码越短。

使用程序求哈夫曼编码有两种方法:

  1. 从叶子结点一直找到根结点,逆向记录途中经过的标记。例如,上图中字符 c 的哈夫曼编码从结点 c 开始一直找到根结点,结果为:0 1 1 ,所以字符 c 的哈夫曼编码为:1 1 0(逆序输出)。
  2. 从根结点出发,一直到叶子结点,记录途中经过的标记。例如,上图中字符 c 的哈夫曼编码,就从根结点开始,依次为:1 1 0。

需要注意:

注意:0和1究竟是表示左子树还是右子树没有明确规定。
左、右孩子结点的顺序是任意的, 所以构造出的哈夫曼树并不唯一,但各哈夫曼树的带权路径长度WPL相同且为最优。
此外,如有若干权值相同的结点,则构造出的哈夫曼树更可能不同,但WPL必然相同且是最优的

哈夫曼树构造流程:
给定N个权值分别为W1, W2,…,Wn”的结点,构造哈夫曼树的算法描述如下:

  1. 将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
  2. 构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新结点的权值置为左、右子树上根结点的权值之和。
  3. 从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
  4. 重复步骤2和3,直至F中只剩下一棵树为止。

从上述构造过程中可以看岀哈夫曼树具有如下特点:

  1. 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大。
  2. 构造过程中共新建了n-1个结点(双分支结点),因此哈夫曼树的结点总数为2*n-1
  3. 每次构造都选择2棵树作为新结点的孩子,因此哈夫曼树中不存在度为1的结点。

C++代码:

#include 

#include 

#include 

#include 

#include 
//传入一串字符,使用哈夫曼树对其进行编码,字符串中每个字符出现的次数就是作为哈夫曼树的权值

struct TreeNode
{
    char val;   //节点保存的值
    int weight; //权值
    TreeNode *left;
    TreeNode *right;
    TreeNode(int _weight) : val(0), weight(_weight), left(nullptr), right(nullptr) {}
    TreeNode(char _val, int _weight) : val(_val), weight(_weight), left(nullptr), right(nullptr) {}
};

class HuffTree
{
private:
    void _InitTimes(const std::string &src)
    {
        for (int i = 0; i < src.size(); i++)
        {
            times[src[i]] += 1;
        }
    }

    void _PreDisplay(TreeNode *node, std::string &str)
    {
        if (node->left == nullptr && node->right == nullptr)
        {
            //叶子节点
            // std::cout << str << std::endl;
            code[node->val] = str;
            return;
        }
        if (node->left != nullptr)
        {
            str.push_back('0');
            _PreDisplay(node->left, str);
            str.pop_back(); //出递归后,刚刚递归进去给字符串插入的字符也要弹出
        }
        if (node->right != nullptr)
        {
            str.push_back('1');
            _PreDisplay(node->right, str);
            str.pop_back();
        }
    }

    void _PreDisplay(TreeNode *node)
    {
        std::string retBuff;
        _PreDisplay(node, retBuff);
    }

public:
    TreeNode *root;                             //构造的哈夫曼树
    std::unordered_map<char, std::string> code; //保存字符进行编码的结果
    std::unordered_map<char, int> times;        //保存传入的字符串中每个字符出现的次数

    HuffTree(const std::string &src)
    {
        //统计每个字符出现的次数
        _InitTimes(src);

        std::vector<TreeNode *> ArrayNode;
        //先给所有节点开辟空间
        std::unordered_map<char, int>::iterator pos = times.begin();
        while (pos != times.end())
        {
            ArrayNode.push_back(new TreeNode(pos->first, pos->second));
            pos++;
        }

        //循环创建哈夫曼树节点
        //如果只有一个节点
        if (ArrayNode.size() == 0)
        {
            root = ArrayNode[0];
        }
        else
        {
            for (int time = 0; time < ArrayNode.size() - 1; time++)
            {
                //找权值最小的和第二小的节点
                int minIndex = 0;
                int minSecIndex = 0;
                // ArrayNode[minIndex] == nullptr证明这个节点已经建立过哈夫曼树了,需要跳过
                while (ArrayNode[minIndex] == nullptr)
                {
                    minIndex++;
                }
                for (int i = 0; i < ArrayNode.size(); i++)
                {
                    if (ArrayNode[i] != nullptr && ArrayNode[i]->weight < ArrayNode[minIndex]->weight)
                    {
                        minIndex = i;
                    }
                }

                //找次小值
                while (ArrayNode[minSecIndex] == nullptr || minIndex == minSecIndex)
                {
                    minSecIndex++;
                }
                for (int i = 0; i < ArrayNode.size(); i++)
                {
                    if (i != minIndex)
                    {
                        if (ArrayNode[i] != nullptr && ArrayNode[i]->weight < ArrayNode[minSecIndex]->weight)
                        {
                            minSecIndex = i;
                        }
                    }
                }

                // printf("出现次数最小的字符是%c,出现次数%d\n", ArrayNode[minIndex]->val, ArrayNode[minIndex]->weight);
                // printf("出现次数次少的字符是%c,出现次数为%d\n", ArrayNode[minSecIndex]->val, ArrayNode[minSecIndex]->weight);
                // printf("============\n");

                //创建新节点,将这个节点插入到最小字符位置,次少节点位置处理过了置空.并将树结构构造好
                root = new TreeNode(ArrayNode[minIndex]->weight + ArrayNode[minSecIndex]->weight);

                root->left = ArrayNode[minIndex];
                root->right = ArrayNode[minSecIndex];

                ArrayNode[minIndex] = root;
                ArrayNode[minSecIndex] = nullptr;
            }
        }
    }

    //打印编码
    void PrintCode()
    {
        //遍历树,直到遍历到叶子节点,规定向左树走编码为0,向右树走编码为1
        _PreDisplay(root);

        //打印编码集合
        auto pos = code.begin();
        while (pos != code.end())
        {
            std::cout << pos->first << ":" << pos->second << std::endl;
            pos++;
        }
    }

    //解码
    char DeCode(const std::string &src)
    {
        TreeNode *node = root;
        for (int i = 0; i < src.size(); i++)
        {
            if (src[i] == '0')
                node = node->left;
            if (src[i] == '1')
                node = node->right;
        }
        assert(node->left == nullptr && node->right == nullptr); // node一定走到了叶子节点
        return node->val;
    }
};
#include "hufftree.h"

int main(int argc, char const *argv[])
{
    HuffTree tree("abandon");
    tree.PrintCode();

    std::cout << "010解码:" << tree.DeCode("010") << std::endl;
    return 0;
}

这里构建的哈夫曼树如下图:
abandon中
a出现2次,b出现1次,n出现2次,d出现1次,o出现1次。次数作为整个节点的权值

数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第6张图片

数据结构-难点突破(C++实现并查集+路径优化,详解哈夫曼编码树)_第7张图片
需要注意的是,这里对其编码使用一个字符‘1’或‘0’进行编码,压缩效果不明显。

正式做项目时,可以选择树向左移动时,代表比特位0,向右移动时代表比特1。这样才可以真正达到压缩效果。

具体细节可以移步数据结构-压缩软件核心-C++(利用哈夫曼树进行编码,对文件进行压缩与解压缩)

代码位置Github

你可能感兴趣的:(#,数据结构,#,哈夫曼压缩与解压缩,数据结构,c++,算法)