贪心算法

贪心算法-霍夫曼编码

霍夫曼编码是一种无损数据压缩算法。在计算机数据处理中,霍夫曼编码使用变长编码表对源符号(如文件中的一个字母)进行编码,其中变长编码表是通过一种评估来源符号出现机率的方法得到的,出现机率高的字母使用较短的编码,反之出现机率低的则使用较长的编码,这便使编码之后的字符串的平均长度、期望值降低,从而达到无损压缩数据的目的。例如,在英文中,e的出现机率最高,而z的出现概率则最低。当利用霍夫曼编码对一篇英文进行压缩时,e极有可能用一个比特来表示,而z则可能花去25个比特(不是26)。用普通的表示方法时,每个英文字母均占用一个字节(byte),即8个比特。二者相比,e使用了一般编码的1/8的长度,z则使用了3倍多。倘若我们能实现对于英文中各个字母出现概率的较准确的估算,就可以大幅度提高无损压缩的比例。

构建霍夫曼编码主要包括两个部分:

1)根据输入的字符串构建霍夫曼树。

2)便利霍夫曼数并给每个字符分配编码。

哈夫曼树(Huffman Tree),又叫最优二叉树,指的是对于一组具有确定权值的叶子结点的具有最小带权路径长度的二叉树。

(1)路劲(Path):从树中的一个结点到另一个结点之间的分支构成两个结点间的路径。
(2)路径长度(Path Length):路径上的分支树。
(3)树的路径长度(Path Length of Tree):从树的根结点到每个结点的路径长度之和。在结点数目相同的二叉树中,完全二叉树的路径长度最短。
(4)结点的权(Weight of  Node):在一些应用中,赋予树中结点的一个有实际意义的树。
(5)结点的带权路径长度(Weight Path Length of Node):从该结点到树的根结点的路径长度与该结点的权的乘积。
(6)树的带权路径长度(WPL):树中所有叶子结点的带权路径长度之和
 

构建霍夫曼树的步骤:

算法:输入是没有相同元素的字符数组(长度n)以及字符出现的频率,输出是哈夫曼树。

即假设有n个字符,则构造出得哈夫曼树有n个叶子结点。n个字符的权值(频率)分别设为w1,w2,…,wn,则哈夫曼树的构造规则为:

(1)将w1,w2,…,wn看成是有n棵树的森林(每棵树仅有一个结点);
(2)在森林中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从森林中删除选取的两棵树,并将新树加入森林;
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树。

用一个例子来了解该算法:

复制代码
character   Frequency
    a            5
    b           9
    c           12 d 13 e 16 f 45
复制代码

第1步:将每个元素构造成一个节点,即只有一个元素的树。并构建一个最小堆,包含所有的节点,该算法用了最小堆来作为优先队列。

第2步:选取两个权值最小的节点,并添加一个权值为5+9=14的节点,作为他们的父节点。并更新最小堆,现在最小堆包含5个节点,其中4个树还是原来的节点,权值为5和9的节点合并为一个。

character           Frequency
       c               12
       d               13
     内部 节点           14 e 16 f 45

重复上面的步骤,直到最小堆只有一个节点。

character      Frequency
 内部节点         100

C语言实现如下:

复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_TREE_HT 100

// 一个霍夫曼树节点
struct MinHeapNode { char data; // 输入的字符数组中的一个字符 unsigned freq; // 字符出现的次数 struct MinHeapNode *left, *right; }; // 最小堆: 作为优先队列使用 struct MinHeap { unsigned size; // 最小堆元素的个数 unsigned capacity; //最大容量 struct MinHeapNode **array; }; //初始化一个最小堆节点 struct MinHeapNode* newNode(char data, unsigned freq) { struct MinHeapNode* temp = (struct MinHeapNode*) malloc(sizeof(struct MinHeapNode)); temp->left = temp->right = NULL; temp->data = data; temp->freq = freq; return temp; } // 创建一个容量为capacity 的最小堆 struct MinHeap* createMinHeap(unsigned capacity) { struct MinHeap* minHeap = (struct MinHeap*) malloc(sizeof(struct MinHeap)); minHeap->size = 0; // current size is 0 minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*)); return minHeap; } // swap 两个堆节点 void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode* t = *a; *a = *b; *b = t; } // 更新最小堆. void minHeapify(struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]); minHeapify(minHeap, smallest); } } //检测堆的大小是否为1 int isSizeOne(struct MinHeap* minHeap) { return (minHeap->size == 1); } //取得堆中最小的节点 struct MinHeapNode* extractMin(struct MinHeap* minHeap) { struct MinHeapNode* temp = minHeap->array[0]; minHeap->array[0] = minHeap->array[minHeap->size - 1]; --minHeap->size; minHeapify(minHeap, 0); return temp; } // 想最小堆中插入一个节点 void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1; while (i && minHeapNode->freq < minHeap->array[(i - 1)/2]->freq) { minHeap->array[i] = minHeap->array[(i - 1)/2]; i = (i - 1)/2; } minHeap->array[i] = minHeapNode; } //构建一个最小堆 void buildMinHeap(struct MinHeap* minHeap) { int n = minHeap->size - 1; int i; for (i = (n - 1) / 2; i >= 0; --i) minHeapify(minHeap, i); } void printArr(int arr[], int n) { int i; for (i = 0; i < n; ++i) printf("%d", arr[i]); printf("\n"); } // 检测是否是叶子节点 int isLeaf(struct MinHeapNode* root) { return !(root->left) && !(root->right) ; } // 创建一个容量为 size的最小堆,并插入 data[] 中的元素到最小堆 struct MinHeap* createAndBuildMinHeap(char data[], int freq[], int size) { struct MinHeap* minHeap = createMinHeap(size); for (int i = 0; i < size; ++i) minHeap->array[i] = newNode(data[i], freq[i]); minHeap->size = size; buildMinHeap(minHeap); return minHeap; } // 构建霍夫曼树 struct MinHeapNode* buildHuffmanTree(char data[], int freq[], int size) { struct MinHeapNode *left, *right, *top; // 第 1步 : 创建最小堆. struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size); //知道最小堆只有一个元素 while (!isSizeOne(minHeap)) { // 第二步: 取到最小的两个元素 left = extractMin(minHeap); right = extractMin(minHeap); // Step 3: 根据两个最小的节点,来创建一个新的内部节点 // '$' 只是对内部节点的一个特殊标记,没有使用 top = newNode('$', left->freq + right->freq); top->left = left; top->right = right; insertMinHeap(minHeap, top); } // 第4步: 最后剩下的一个节点即为跟节点 return extractMin(minHeap); } // 打印霍夫曼编码 void printCodes(struct MinHeapNode* root, int arr[], int top) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1); } // 如果是叶子节点就打印 if (isLeaf(root)) { printf("%c: ", root->data); printArr(arr, top); } } // 构建霍夫曼树,并遍历打印该霍夫曼树 void HuffmanCodes(char data[], int freq[], int size) { // 构建霍夫曼树 struct MinHeapNode* root = buildHuffmanTree(data, freq, size); // 打印构建好的霍夫曼树 int arr[MAX_TREE_HT], top = 0; printCodes(root, arr, top); } // 测试 int main() { char arr[] = {'a', 'b', 'c', 'd', 'e', 'f'}; int freq[] = {5, 9, 12, 13, 16, 45}; int size = sizeof(arr)/sizeof(arr[0]); HuffmanCodes(arr, freq, size); return 0; }
复制代码

运行结果如下:

f: 0
c: 100
d: 101 a: 1100 b: 1101 e: 111

时间复杂度

O(nlogn), 其中n是字符的数量。extractMin() 调用了 2*(n-1)次,extractMin()为log(n)的复杂度。

 

 

贪心算法-Dijkstra最短路径算法

Dijkstra算法的标记和结构与prim算法的用法十分相似。它们两者都会从余下顶点的优先队列中选择下一个顶点来构造一颗扩展树。但千万不要把它们混淆了。它们解决的是不同的问题,因此,所操作的优先级也是以不同的方式计算的:Dijkstra算法比较路径的长度,因此必须把边的权重相加,而prim算法则直接比较给定的权重。

源最短路径问题
给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数。另外,还给定 V 中的一个顶点,称为源。现在我们要计算从源到所有其他各顶点的最短路径长度。这里的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。

前面Bellman-Ford最短路径算法讲了单源最短路径的Bellman-Ford算法(动态规划算法)。这里介绍另外一个更常见的算法Dijkstra算法。

Dijkstra算法和 最小生成树Prim算法最小生成树算法非常类似,大家可以先熟悉下个算法。两个算法都是基于贪心算法。虽然Dijkstra算法相对来说比Bellman-Ford 算法更快,但是不适用于有负权值边的图,贪心算法决定了它的目光短浅。而Bellman-Ford 算法从全局考虑,可以检测到有负权值的回路。

这里模仿MST(Minimum Spanning Tree)的Prim算法,我们创建一个SPT(最短路径树),最初只包含源点。我们维护两个集合,一组已经包含在SPT(最短路径树)中的顶点S集合,另一组是未包含在SPT内的顶点T集合。每次从T集合中选择到S集合路径最短的那个点,并加入到集合S中,并把这个点从集合T删除。直到T集合为空为止。

举例说明,如下图所示的图:

S集合最初为空,然后选取源点0,S集合为 {0},源点到其它所有点的距离为 {0, 4, INF, INF, INF, INF, 8, INF} 。图中蓝色表示 SPT,迭代的过程如下

             

最终得到 SPT(最短路径树) 如下:

算法C++实现:

我们使用Boolean 数组sptSet[] (也有习惯用visit[]命名,表示是否访问过),来表示顶点是否为有SPT中。sptSet[v]=true,说明顶点v在SPT中。 dist[] 用来存储源点到其它所有点的最短路径。

复制代码
#include<iostream>
#include<stdio.h>
#include<limits.h>
using namespace std;

const int V=9; //从未包含在SPT的集合T中,选取一个到S集合的最短距离的顶点 int getMinIndex(int dist[V],bool sptSet[V]) { int min=INT_MAX,min_index; for(int v=0;v<V;v++) { if(!sptSet[v]&&dist[v]<min) min=dist[v],min_index=v; } return min_index; } //打印结果 void printSolution(int dist[],int n) { printf("Vertex Distance from Source\n"); for(int i=0;i<V;i++) printf("%d\t\t %d\n",i,dist[i]); } //source 代表顶点 void dijkstra(int graph[V][V],int source) { int dist[V];// 存储结果,从源点到 i的距离 bool sptSet[V]; // sptSet[i]=true 如果顶点i包含在SPT中 // 初始化. 0代表不可达 for(int i=0;i<V;i++) { dist[i]=(graph[source][i]==0)?INT_MAX:graph[source][i]; sptSet[i]=false; } // 源点,距离总是为0. 并加入SPT dist[source]=0; sptSet[source]=true; // 迭代V-1次,因此不用计算源点了,还剩下V-1个需要计算的顶点。 for(int count=0;count<V-1;count++) { // u,是T集合中,到S集合距离最小的点,u开始在T集合中,然后加入到S集合 int u=getMinIndex(dist,sptSet); // 加入SPT中 sptSet[u]=true; for(int v=0;v<V;v++) { //满足以下4个条件 //1. 点v还没有加入到spt中 //2. 点u和v之间可达 //3. 点u是可达的,距离不是无穷 //4. 经过点u之后的距离小于直接到达点v的距离 if(!sptSet[v]&&graph[u][v]&&dist[u]!=INT_MAX&&dist[u]+graph[u][v]<dist[v]) dist[v]=dist[u]+graph[u][v]; } } printSolution(dist,V); } int main() { int graph[V][V]={ { 0, 4, 0, 0, 0, 0, 0, 8, 0 }, { 4, 0, 8, 0, 0, 0, 0, 11, 0 }, {0, 8, 0, 7, 0, 4, 0, 0, 2 }, { 0, 0, 7, 0, 9, 14, 0, 0, 0 }, { 0, 0, 0, 9, 0, 10, 0, 0, 0 }, { 0, 0, 4, 0, 10, 0, 2, 0, 0 }, { 0, 0, 0, 14, 0, 2, 0, 1, 6 }, { 8, 11, 0, 0, 0, 0, 1, 0, 7 }, { 0, 0, 2, 0, 0, 0, 6, 7, 0 } }; dijkstra(graph, 0); return 0; }
复制代码

运行结果如下:输出源点0到其它各个顶点的最短距离:

 

 

 

贪心算法-Kruskal最小生成树

什么是最小生成树?

生成树是相对图来说的,一个图的生成树是一个树并把图的所有顶点连接在一起。一个图可以有许多不同的生成树。一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边。最小生成树其实是最小权重生成树的简称。生成树的权重是考虑到了生成树的每条边的权重的总和。

最小生成树有几条边?

最小生成树有(V – 1)条边,其中V是给定的图的顶点数量。

Kruskal算法

下面是步骤寻找MST使用Kruskal算法

11,按照所有边的权重排序(从小到大)
2 
32,选择最小的边。检查它是否形成与当前生成树形成环。如果没有形成环,讲这条边加入生成树。否则,丢弃它。 
4 
53,重复第2步,直到有生成树(V-1)条边

步骤2使用并查集算法来检测环。如果不熟悉并查集建议阅读下并查集

该算法是一种贪心算法。贪心的选择是选择最小的权重的边,并不会和当前的生成树形成环。让我们了解一个例子,考虑下面输入图

spanning-tree-mst

spanning-tree-mst

该图包含9个顶点和14个边。因此,形成最小生成树将有(9 – 1)= 8条边。

01排序后:
02Weight   Src    Dest
031         7      6
042         8      2
052         6      5
064         0      1
074         2      5
086         8      6
097         2      3
107         7      8
118         0      7
128         1      2
139         3      4
1410        5      4
1511        1      7
1614        3      5

现在从已经排序的边中逐个选择
1. edge 7-6:没有环,加入

2. edge 8-2: 没有环,加入

3. edge 6-5: 没有环,加入

4. edge 0-1: 没有环,加入

5. edge 2-5: 没有环,加入

6. edge 8-6: 加入后会形成环,舍弃

7. edge 2-3: 没有环,加入

8. edge 7-8: 加入后会形成环,舍弃

9. edge 0-7: 没有环,加入

10. edge 1-2: 加入后会形成环,舍弃

11. edge 3-4: 没有环,加入

目前为止一家有了 V-1 条边,可以肯定V个顶点都一包含在内,到此结束。

代码实现:

复制代码
// Kruskal 最小生成树算法
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 带有权重的边 struct Edge { int src, dest, weight; }; // 无向图 struct Graph { // V-> 顶点个数, E->边的个数 int V, E; // 由于是无向图,从 src 到 dest的边,同时也是 dest到src的边,按一条边计算 struct Edge* edge; }; //构建一个V个顶点 E条边的图 struct Graph* createGraph(int V, int E) { struct Graph* graph = (struct Graph*) malloc( sizeof(struct Graph) ); graph->V = V; graph->E = E; graph->edge = (struct Edge*) malloc( graph->E * sizeof( struct Edge ) ); return graph; } //并查集的结构体 struct subset { int parent; int rank; }; // 使用路径压缩查找元素i int find(struct subset subsets[], int i) { if (subsets[i].parent != i) subsets[i].parent = find(subsets, subsets[i].parent); return subsets[i].parent; } // 按秩合并 x,y void Union(struct subset subsets[], int x, int y) { int xroot = find(subsets, x); int yroot = find(subsets, y); if (subsets[xroot].rank < subsets[yroot].rank) subsets[xroot].parent = yroot; else if (subsets[xroot].rank > subsets[yroot].rank) subsets[yroot].parent = xroot; else { subsets[yroot].parent = xroot; subsets[xroot].rank++; } } // 很据权重比较两条边 int myComp(const void* a, const void* b) { struct Edge* a1 = (struct Edge*)a; struct Edge* b1 = (struct Edge*)b; return a1->weight > b1->weight; } // Kruskal 算法 void KruskalMST(struct Graph* graph) { int V = graph->V; struct Edge result[V]; //存储结果 int e = 0; //result[] 的index int i = 0; // 已排序的边的 index //第一步排序 qsort(graph->edge, graph->E, sizeof(graph->edge[0]), myComp); // 为并查集分配内存 struct subset *subsets = (struct subset*) malloc( V * sizeof(struct subset) ); // 初始化并查集 for (int v = 0; v < V; ++v) { subsets[v].parent = v; subsets[v].rank = 0; } // 边的数量到V-1结束 while (e < V - 1) { // Step 2: 先选最小权重的边 struct Edge next_edge = graph->edge[i++]; int x = find(subsets, next_edge.src); int y = find(subsets, next_edge.dest); // 如果此边不会引起环 if (x != y) { result[e++] = next_edge; Union(subsets, x, y); } // 否则丢弃,继续  } // 打印result[] printf("Following are the edges in the constructed MST\n"); for (i = 0; i < e; ++i) printf("%d -- %d == %d\n", result[i].src, result[i].dest, result[i].weight); return; } // 测试 int main() { /* 创建下面的图: 10 0--------1 | \ | 6| 5\ |15 | \ | 2--------3 4 */ int V = 4; // 顶点个数 int E = 5; //边的个数 struct Graph* graph = createGraph(V, E); // 添加边 0-1 graph->edge[0].src = 0; graph->edge[0].dest = 1; graph->edge[0].weight = 10; graph->edge[1].src = 0; graph->edge[1].dest = 2; graph->edge[1].weight = 6; graph->edge[2].src = 0; graph->edge[2].dest = 3; graph->edge[2].weight = 5; graph->edge[3].src = 1; graph->edge[3].dest = 3; graph->edge[3].weight = 15; graph->edge[4].src = 2; graph->edge[4].dest = 3; graph->edge[4].weight = 4; KruskalMST(graph); return 0; }
复制代码

运行结果如下:

Following are the edges in the constructed MST
2 -- 3 == 4
0 -- 3 == 5 0 -- 1 == 10

 时间复杂度:

O(ElogE) 或 O(ElogV)。 排序使用 O(ELogE) 的时间,之后我们遍历中使用并查集O(LogV) ,所以总共复杂度是 O(ELogE + ELogV)。E的值最多为V^2,所以

O(LogV) 和 O(LogE) 可以看做是一样的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值