• 计算机算法分析与设计(12)---贪心算法(最优装载问题和哈夫曼编码问题)



    一、最优装载问题

    1.1 问题表述

     1. 有一批集装箱要装上一艘载重量为 c c c 的轮船,已知集装箱 i ( 1 ≤ i ≤ n ) i(1≤i≤n) i(1in) 的重量为 w i w_i wi。最优载问题要求在装载体积不受限制的情况下,将尽可能多的集装箱装上轮船。

     2. 贪心选择策略:重量最轻者优先装载

     3. 算法思路:将装船过程划分为多步选择,每步装 1 1 1 个货箱,每次从剩下的货箱中选择重量最轻的货箱。如此下去直到所有货箱均装上船或船上不能再容纳其他任何一个货箱。

    1.2 代码编写

    算法时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

    #include
    #include 
    using namespace std;
    
    int main(){
        int n; //定义货箱个数
        int c; //定义船的最大承载量
        cout<<"输入货箱个数以及船的最大承载量"<<endl;
        cin>>n>>c;
        
        cout<<"输入每件货物的重量"<<endl;
        int w[n]; //用数组填装货箱的重量
        for(int i=0;i<n;i++)
    	{
            cin>>w[i];
        }
    
        sort(w,w+n); //快排将货箱的重量由小到大进行排序
        
        int temp=0; //中间值
        int count=0; //计数器
        
        for(int i=0;i<n;i++)
    	{
            temp = temp + w[i];
            if(temp<=c)
    		{
                count++;
    
            }
            else
    		{
                break;
            }
        }
    
        cout<<"能装入货箱的最大数量为"<<count<<endl;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    二、哈夫曼编码

    2.1 哈夫曼编码概述

     1. 哈夫曼编码是在电讯通信中的应用之一,广泛地用于数据文件压缩的十分有效的编码方法,其压缩率通常在20%~90%之间。在电讯通信业务中,通常用二进制编码来表示字母或其他字符,并用这样的编码来表示字符序列。

     2. 例如:如果需传送的电文为 ‘ A B A C C D A ’ ‘ABACCDA’ ABACCDA,它只用到四种字符,用两位二进制编码便可分辨。假设 A , B , C , D A, B, C, D A,B,C,D 的编码分别为 00 , 01 , 10 , 11 00, 01,10, 11 00,01,10,11,则上述电文便为 ‘ 00010010101100 ’ ‘00010010101100’ ‘00010010101100’(共 14 位),译码员按两位进行分组译码,便可恢复原来的电文。

    2.2 前缀码

     1. 前缀码定义:对每一个字符规定一个 0 , 1 0,1 0,1 串作为其代码,并要求任一字符的代码都不是其它字符代码的前缀,这种编码称为前缀码。

    abcdef
    编码方式1010110011111011100
    编码方式20100011011

     很容易发现,当我们在用编码方式 2 2 2 时,解码会出现问题,例如 00110 00110 00110 既可以解码成 a a b b a aabba aabba,也可以解码成 c f a cfa cfa,解码结果不唯一,编码方式不可行。
     而用前缀码(即编码的任意前缀不是其他编码),则解码结果唯一,编码方式可行。

    2.3 问题描述

     1. 如果要求得到最优的编码方式,那不同的前缀码如何比较它们的优劣呢?我们可以通过比较编码后二进制串的总长,总长越短,说明这种编码方式越优。而编码后二进制的总长,依赖于待编码字符的频数。假设我们给出的带编码字符及其频数和两种不同的前缀码,如下表所示。

    abcdef
    频数(千次)4513121695
    前缀码1010110011111011100
    前缀码2110011011111001010

     通过计算可知,使用前缀码 1 1 1编码后二进制串的总长是 224000 224000 224000,使用前缀码 2 2 2编码后二进制串的总长是 348000 348000 348000,显然,前缀码 1 1 1要优于前缀码 2 2 2

     2. 贪心策略出现频率较高的字符的编码较短出现频率较低的字符的编码较长。使用二叉树对其进行编码和解码。
    在这里插入图片描述

    2.4 代码思路

     1. 给定编码字符集 C C C C C C 中的任一字符 c c c 的出现频率 f ( c ) f(c) f(c) C C C 的一个前缀码编码方案对应一棵二叉树 T T T。字符 c c c 在树 T T T 中的深度记为 d T ( c ) d_T(c) dT(c)。定义该编码方式的平均码长为:

    在这里插入图片描述

    在这里插入图片描述
     2. 哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树 T T T。算法以 n n n 个叶结点开始,执行 n − 1 n-1 n1 次合并,运算后产生最终所要求的树 T T T。在哈夫曼树中,编码字符集中每个字符 c c c的频率是 f ( c ) f(c) f(c)。以 f f f 为键值的优先队列 Q Q Q 在用做贪心选择时有效地确定当前要合并的两棵具有最小频率的树。一旦两棵具有最小频率的树合并后,产生一棵新的树,其频率和为合并的两棵树的频率之和,并将新树加入 Q Q Q

    2.5 代码编写

    算法时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)

    #include
    using namespace std;
    
    #define MaxNode 200
    #define MaxBit 100
    
    //节点,一般遵循左0右1 
    typedef struct
    {
    	double weight; //权重
    	int parent; //父节点
    	int lchild; //左孩子节点
    	int rchild; //右孩子节点
    	char value; //节点代表的字符
    }hufnode;
    
    typedef struct
    {
    	int start; //开始指针,记录每个字母编码在数组里开始的位置
    	int bit[10]; //存放赫夫曼编码
    }hufbit;
    
    hufnode hujd[MaxNode];
    hufbit hb[MaxBit];
    
    //初始化节点,parent,lchild,rchild=-1,weight=0
    void initNode()
    {
    	for (int i = 0; i < MaxNode; i++)
    	{
    		hujd[i].lchild = -1;
    		hujd[i].parent = -1;
    		hujd[i].rchild = -1;
    		hujd[i].weight = 0;
    	}
    }
    
    //建立哈弗曼树
    void creathuf(int n)
    {
    	int i;
    	cout << "请输入每个节点的字符和权值" << endl;
    	for (i = 0; i < n; i++)
    	{
    		cin >> hujd[i].value >> hujd[i].weight;
    	}
     	
        //定义两个变量m1和m2用来存储最小的两个未处理的权值,以及两个变量x1和x2用来存储对应的最小权值的节点索引。
    	double m1, m2;
    	int x1, x2;
    	for (int i = 0; i < n - 1; i++)
    	{
    		m1 = m2 = 1000;
    		x1 = x2 = 0;
    		
    		for (int j = 0; j < n + i; j++)
    		{
    			//找最小权值结点 
    			if (hujd[j].weight < m1 && hujd[j].parent == -1)
    			{
    				m2 = m1;
    				x2 = x1;
    				m1 = hujd[j].weight;
    				x1 = j;
    			}
    			//找第二最小权值结点 
    			else if (hujd[j].weight < m2 && hujd[j].parent == -1)
    			{
    				m2 = hujd[j].weight;
    				x2 = j;
    			}
    		}
    		
            //创建一个新的父节点,其左孩子为x1,右孩子为x2		
    		hujd[n + i].weight = m1 + m2;
    		hujd[n + i].lchild = x1;
    		hujd[n + i].rchild = x2;
    		hujd[x1].parent = n + i;
    		hujd[x2].parent = n + i;
    	}
    }
    
    //编码
    void tshuff(int n)
    {
    	//p用于保存当前节点的父节点的索引,c用于保存当前节点的索引。
    	int p, c;
    	
    	for (int i = 0; i < n; i++)
    	{
    		p = hujd[i].parent;  //获取当前节点的父节点索引并赋值给p
    		c = i;               //获取当前节点的索引并赋值给c
    		hb[i].start = n - 1; //将当前节点的起始位设置为n-1,这可能意味着我们是从最右边开始编码的
    		while (p != -1)      //当前节点不是根节点时进行循环
    		{
                //如果当前节点是父节点的左孩子,我们将起始位处的二进制位设为0,并将起始位右移一位(向树的左边移动)
    			if (hujd[p].lchild == c)
    			{
    				hb[i].bit[hb[i].start] = 0;
    				hb[i].start--;
    			}
    			//如果当前节点是父节点的右孩子,我们将起始位处的二进制位设为1,并将起始位右移一位(向树的左边移动
    			if (hujd[p].rchild == c)
    			{
    				hb[i].bit[hb[i].start] = 1;
    				hb[i].start--;
    			}
    			//将当前节点的索引赋值给p,将父节点的索引赋值给c,然后开始下一轮循环。
    			//这个过程一直持续到p等于-1为止,也就是说,当我们到达树的根节点时,循环就会结束。
    			c = p;
    			p = hujd[p].parent;
    		}
    	}
    }
    
    void output(int n)
    {
    	for (int i = 0; i < n; i++)
    	{
    		cout << hujd[i].value << ":";
    		for (int j = hb[i].start+1; j < n; j++)
    		{
    			cout << hb[i].bit[j];
    		}
    		cout << endl;
    	}
    }
    int main()
    {
    	int n;
    	cout << "请输入有几个字符:" << endl;
    	cin >> n;
    	initNode();
    	creathuf(n);
    	tshuff(n);
    	cout << "编码方式为:" << endl; 
    	output(n);
    	return 0;	
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139

    2.6 平均码长

    在这里插入图片描述

    2.7 例题

    下面两道题中算平均码长时,我将深度按照从 1 1 1 开始了。
    如果按照从 0 0 0 开始,平均码长第一题答案是: 1065 1065 1065;第二题答案是: 119 119 119
    有的教材深度定义从 0 0 0 开始,有的从 1 1 1 开始,具体情况具体看待吧。

    2.7.1 题1

    在这里插入图片描述
    在这里插入图片描述

    2.7.2 题2

    在这里插入图片描述

  • 相关阅读:
    黑马程序员2023新版JavaWeb企业开发全流程学习笔记(涵盖Spring+MyBatis+SpringMVC+SpringBoot等)
    pytorch深度学习实战lesson27
    可执行文件中的段
    校园网课刷题小程序源码系统 带完整搭建教程
    linux 卸载php的最终方案
    OSS文件上传
    【译】.NET 7 中的性能改进(二)
    MySQL 中如何归档数据
    CST初级教程 七
    读书笔记:《Kubernetes:快速入门》
  • 原文地址:https://blog.csdn.net/m0_62881487/article/details/133890138