我想从以下几个方面阐述霍夫曼编码
1 数据结构中实现霍夫曼编码
2 利用堆排序实现霍夫曼编码
1 数据结构中实现霍夫曼的方法比较简单
关键的是数据结构的定义,书中为了简化链表操作,将其设置为静态链表,因为拥有n个需编码的字符,它们需要2n-1个节点,其中n个叶节点,n-1个内节点,因此用静态链表不仅空间不浪费,而且操作很快。静态表的成员包括权值,父节点编号,左孩子,右孩子编号。
思想就是 分配2n-1个这种数据结构,前n个用来存放叶子节点(需编码的n个字符),后n-1为内节点即结合后所产生的这些叶子节点的父节点以及内节点结合生成的节点。
每次取出{未结合叶节点}+{结合后的节点}集合中最小的两个节点进行结合,生成的节点加入结合后的节点集合中,依次执行n-1次(n个叶子结合n-1次),然后构建所需要的霍夫曼树,由叶子节点开始访问直到根结点可得高度或深度,递归执行叶子到根结点可得编码。
这里选择集合中最小两个元素起初我使用的是快速排序时间复杂度为O(nlgn)。后面发现网上有人用一种O(n)的时间复杂度求最小两个节点,实现的思想就是遍历整个已确定权值的集合,每遍设置m1,m2为两个最大数,l,r为需要确定的两个最小权值(其中l为第一小的位置,r为第二小的位置),第一遍发现第一个小于m1的数时,将m1的值传给m2,第一小的(l)的值传给(r),(l)等于当前位置标号,m1等于当前权值。这样就能保证m1,m2始终为紧邻的两个最小值,保证了正确性。也就是说m1始终表示比m2小的值,l,r用来确定最小的两个权值在静态链表中的位置。
以下代码只是实现了霍夫曼树以及求出了每个叶子节点的经过的边数目
#include "stdio.h" #define MAX 4294967295 typedef struct Hoffman{ unsigned int weight; unsigned int parent,lchild,rchild; }HCNode; /* int Partion(int *W,int *P,int s,int e) { int x =0; int i=0,j=0; int temp; x = P[e]; i = s -1; for(j=s;j<e;j++) { if(W[j]<=x) { i= i+1; temp = W[j]; W[j] = W[i]; W[i] =temp; temp = P[j]; P[j] = P[i]; P[i] = temp; } } temp = W[e]; W[e] = W[i+1]; W[i+1] = temp; temp =P[e]; P[e] =P[i+1]; P[i+1]=temp; return i+1; } void quickSort(int *W,int *P,int s,int e) { int q ; if(s<e) { q = Partion(W,P,s,e); quickSort(W,P,s,q-1); quickSort(W,P,q+1,e); } } void selectMinTwo(HCNode *root,int *keyIndex,int n,int *s1,int *s2) { int *W =NULL; int *P =NULL; int index=1; int i_key =1; int count =1; W =(int *)malloc(sizeof(int)*(n+1)); P =(int *)malloc(sizeof(int)*(n+1)); for(index=1;index<=n;index++) { if(keyIndex[index] ==0) { W[i_key] = root[index].weight; P[i_key] = index; i_key++; } } quickSort(W,P,1,i_key-1); for(index=1;index<=i_key-1;index++) { if(count==1) { *s1 = P[index]; count++; } else if(count==2) { *s2 = P[index]; break; } } free(W); free(P); }*/ void buildHoff(HCNode *root,int *A,int n) { int i=0; int s1=0,s2=0; int m1,m2; int total =0,f=0,depth =0,k=0; int *keyIndex=(int *)malloc(sizeof(int)*(2*n)); for(i=1;i<=n;i++) { root[i].weight = A[i]; root[i].lchild = 0; root[i].rchild =0; root[i].parent =0; keyIndex[i] =0; } for(;i<=2*n-1;i++) { root[i].weight =0; root[i].lchild =0; root[i].rchild =0; root[i].parent =0; keyIndex[i] =0; } for(i=n+1;i<=2*n-1;i++) { m1 = MAX; m2 = MAX; s1 =0; s2 =0; /* selectMinTwo(root,keyIndex,i-1,&s1,&s2);*/ for(k=1;k<=i-1;k++) { if(keyIndex[k]==0) { if(root[k].weight<m1) { m2 =m1; s2=s1; m1=root[k].weight; s1 =k; } else if(root[k].weight<m2) { m2 =root[k].weight; s2 =k; } } } keyIndex[s1] =1; keyIndex[s2] =1; root[s1].parent = i; root[s2].parent = i; root[i].lchild = s1; root[i].rchild = s2; root[i].weight = root[s1].weight+root[s2].weight; } for(i=1;i<=n;i++) { depth =0; for(f=root[i].parent;f!=0;f=root[f].parent) { depth++; } total+=root[i].weight *depth; } printf("%d\n",total); free(keyIndex); } int main() { int testCase ; int numPerCase ; int *A; int index =0; int n =0,m=0; HCNode *root; scanf("%d",&testCase); while(testCase--) { scanf("%d",&numPerCase); A = (int *)malloc(sizeof(int)*(numPerCase+1)); index =1; n=numPerCase; while(numPerCase--) { scanf("%d",&A[index]); index++; } if(n==1) { printf("%d\n",A[1]); } else { /*m = 2*n-1;*/ root =(HCNode *)malloc(sizeof(HCNode)*(2*n)); buildHoff(root,A,n); free(root); } free(A); } return 0; }
2 利用最小堆结构,实现霍夫曼编码
该思想利用了堆的性质,以及霍夫曼编码的性质,首先利用最小堆,堆顶元素始终为最小的元素,同时父亲节点的值要小于等于孩子节点的值,兄弟节点的大小没有关系。那么就产生了一种得天独厚的优势,首先我根据n个需编码的字符权值,构建最小堆,取出堆顶得第一小元素,将堆尾元素移到堆顶,进行堆的重新调整,然后再取出堆顶,获得第二小元素,再进行堆的调整,此时合并两个最小元素,建立一个子树,此时将该子树的根结点插入堆的堆尾,进行重新调整,但此时只需和祖先节点进行对比不需管其他节点,但是快速排序的话,就需要对整个集合进行重新对比,可以想象利用堆的性质,效率可以显著提升。
这里实现堆排序构建霍夫曼编码个人感觉比较复杂,因为我利用数组实现,主要是对指针操作,不过明白的话,应该很快就可以实现的
具体流程图如下:
代码如下:
#include "stdio.h" int LEFT(int i) { return 2*i; } int RIGHT(int i) { return 2*i+1; } void MinHeap(int *heap,int i,int heap_size) { int l = LEFT(i); int r = RIGHT(i); int largest = -1; int temp; if(l<=heap_size&&heap[heap[l]]<heap[heap[i]]) { largest = l; } else { largest = i; } if(r<=heap_size&&heap[heap[r]]<heap[heap[largest]]) { largest = r; } if(largest != i) { temp = heap[i]; heap[i] = heap[largest]; heap[largest] =temp; MinHeap(heap,largest,heap_size); } } void BuildHoff(int *A,int heapSize) { int *heap = (int *)malloc(sizeof(int)*(2*heapSize+1)); int heap_size = heapSize; int nonzero_count=0,i; int pos[2]; int parent,curr,temp,total; memcpy(heap+heap_size+1,A,sizeof(int)*heap_size); memset((char *)heap,0,sizeof(*heap)*(heap_size+1)); for(i=1;i<=heap_size;i++) { if(heap[heap_size+i]) { heap[i] = heap_size+i; } } heap_size = heapSize; for(i=heap_size/2;i>0;i--) { MinHeap(heap,i,heap_size); } /* for(i=1;i<=2*heapSize;i++) { printf("%d\t",heap[i]); } printf("*********************\n");*/ heap_size+=1; while(heap_size>2) { for(i=0;i<2;i++) { pos[i]=heap[1]; heap[1]=heap[--heap_size]; MinHeap(heap,1,heap_size); } heap[heap_size+1]= heap[pos[0]]+heap[pos[1]]; heap[pos[0]] =heap[pos[1]] =heap_size+1; heap[heap_size] = heap_size+1; curr = heap_size; parent = curr>>1; while(parent&&heap[heap[parent]]> heap[heap[curr]]) { temp = heap[parent]; heap[parent] = heap[curr]; heap[curr] = temp; curr =parent; parent=curr>>1; } heap_size++; /* for(i=1;i<=2*heapSize;i++) { printf("%d\t",heap[i]); } printf("-----------\n");*/ } /* for(i=1;i<=2*heapSize;i++) { printf("%d\t",heap[i]); } printf("\n");*/ heap[2] = 0; for(i=3;i<=2*heapSize;i++) { heap[i] = heap[heap[i]]+1; } total =0; for(i=heapSize+1;i<=2*heapSize;i++) { total += heap[i]*A[i-heapSize-1]; } printf("%d\n",total); free(heap); } int main() { int testCase; int *A; int numPerCase; int total,index; scanf("%d",&testCase); while(testCase--) { scanf("%d",&numPerCase); index =0; total = numPerCase; A = (int *)malloc(sizeof(int)*(numPerCase)); while(numPerCase--) { scanf("%d",&A[index]); index++; } BuildHoff(A,total); free(A); } return 0; }
下图为两个算法实现的对比
可见利用第二种方法 最小堆实现 ,算法所需时间为213ms,内存所需1208KB
而利用静态链表实现,算法所需时间为1003ms.内存所需20KB。
通过以上分析,可以明显发现利用最小堆实现该算法,算法执行时间上面得到了大幅度的提升,看来合理的选择相应的数据结构,有的时候也能产生巨大的性能提升~