5.1 堆
当需要存储一个不同优先级的优先队列时(总是删除最大/最小值,插入任意元素),我们就要用到堆,当然,用数组,链表,或树都可以完成,但是算法复杂度不够理想。
堆是一种特殊的树,它的任意节点都比它的左右儿子大/小,所以它的根节点是最大的/最小的,删除的时候只需要删除最上面的根节点即可,插入时,为了使空间利用率最大,按照完全二叉树插入,这就是最大/小堆
还记得刚开始说二叉树的时候,我们试过用数组存储二叉树,然而因为利用率不高,只适合存完全二叉树这种排的比较满的树,对于现在堆这种结构,我们就可以用数组来存,从上到下一层一层排序,第i个节点左儿子是2i,右儿子是2i+1,父节点是i/2。
操作集:
MaxHeap Create(int MaxSize);
Boolean IsFull(MaxHeap H);
Boolean IsEmpty(MaxHeap H);
MaxHeap Insert(ElementType x, MaxHeap H);
ElementType Delete(MaxHeap H);
- 最大堆的结构
typedef struct HeapStruct *MaxStruct;
struct HeapStruct{
//指向数组
ElementType *Elements;
//现有元素个数
int Size;
//最大容量
int Capacity;
};
- 创建一个最大堆
MaxHeap Create(int MaxSize){
MaxHeap H = malloc(sizeof(struct HeapStruct));
//数组空出一个位置存放最大值作为哨兵
H->Elements = malloc(sizeof((MaxSize+1)*ElementType));
H->Size = 0;
H->Capacity = MaxSize;
//宏定义一个数据里面的最大可能值,插入时可以减少一个判断条件。
H->Elements[0] = MaxData;
return H;
}
- 插入
MaxHeap Insert(ElementType x, MaxHeap H){
int i;
if(IsFull(H)){
printf("已满!");
return NULL;
}
//改变最大堆信息的同时,告诉i应该插入的位置
i = ++H->Size;
for(;H->Elements[i/2] < x;i /= 2){
H->Elements[i] = H->Elements[i/2];
}
H->Elements[i] = x;
return H;
}
- 删除
ElementType Delete(MaxHeap H){
if( IsEmpty(H) ){
printf("堆空!");
return NULL;
}
ElementType MaxItem = H->Elements[1];
ElementType item;
int parent = 1;
int child;
item = H->Elements[H->Size--];
for(;2*parent <= H->Size;parent = child){
child = 2 * parent;
if( child != H->Size){
if( H->Elements[child] < H->Elements[child+1])
child++;
}
if(item >= H->Elements[child])break;
else
H->Elements[parent] = H->Elements[child];
}
H->Elements[parent] = item;
return MaxItem;
}
给定一个序列,创建一个堆,可以通过不断插入的方式创建。但是有效率更高的方法,先把序列存入完全二叉树中,然后从最后一个有儿子的节点开始调整,形成一个节点,他的左右儿子都是堆的递归
//这段代码未经验证,有什么问题希望能反馈给我……
MaxHeap Create(ElementType T[Size]){
MaxHeap H;
for(int i = 0; i < Size; i++)
H->Elements[++H->Size] = T[i];
int parent = H->Size/2;
int child;
ElementType t;
for(;parent>0;parent--){
for(;2*parent <= H->Size;parent = child){
child = 2 * parent;
if( child != H->Size){
if( H->Elements[child] < H->Elements[child+1])
child++;
}
if(H->Elements[parent] >= H->Elements[child])break;
else{
t = H->Elements[parent];
H->Elements[parent] = H->Elements[child];
H->Elements[child] = t;
}
}
}
}
5.2 哈夫曼树和哈夫曼编码
查找或者使用数据时,不是平均的使用每一个数据,而是有的数据频率高,有的数据频率低,不同的频率可以视为数据不同的权重,这些带权重的数据在树中的分布会影响到查找的效率。
树中的每个叶节点都有自己的权重,从根节点到叶节点经过的路径乘以叶节点的权重作为某个叶节点的带权路径长度,所有的叶节点的带权路径长度之和就是该树的带权路径,哈夫曼树就是带权路径最小的树。
构造哈夫曼树: 在数据中找到权值最小的的两个,组成一个树,这棵树的权值就是他们两个权值的和,然后这棵新树的根节点作为新的节点放入原来的数据中。
typedef struct TreeNode *HuffmanTree;
struct TreeNode{
int weight;
HuffmanTree Right,Left;
};
//事先将元素按权值构成最小堆。传入最小堆,返回哈夫曼树。
HuffmanTree Huffman(MinHeap H){
HuffmanTree T;
for(int i = 1; i < H->Size; i++){
T = malloc(sizeof(struct TreeNode));
T->Left = Delete(H);
T->Right = Delete(H);
T->weight = T->Left->weight+T->Right->weight;
Insert(T,H);
}
T = Delete(H);
return T;
}
哈夫曼树有几个特点:
- 没有只有一个儿子的节点(也就是度为1的节点)
- 如果有n个叶节点,总结点数为2n-1
- 如果有权值一样的元素,有可能会构成不同结构的树,但是他们的总路径是相同的
- 哈夫曼树左右儿子交换位置之后任然为哈夫曼树。
哈夫曼编码:对于使用频率高的字符,我们需要用更简洁的表示方法,但是用不等长的编码方法就会出现二义性问题,同样的二进制码可以表示不同的意义,为了避免这种情况,我们就要用到哈夫曼树,每个节点的分支都有0和1来表示,要编码的字符都在树的叶节点上,这样就可以避免二义性问题,再把不同频率作为权值,就可以用哈夫曼树做哈夫曼编码。(这让我想到了有的CPU指令集)。
5.3 集合及其运算
把一个个的元素看成一个个的集合,如果两个元素之间联通,那么就把这两个集合并起来。
集合我们只关心两个问题,并集和查找,查找就是找到元素所在的那个集合,为此,我们用树来存储集合,用树的根表示该集合,求并集的时候只需要把一棵树接在另一个树上即可。
用树的结构表示集合,在计算机中,我们可以用结构数组来存储这种树,Parent表示该节点的父节点在数组中的位置,根节点的Parent为-1,注意,一个数组可以存好几个不同的集合,也可以只有一个集合。
其实就是数据库中的那种递归字段
typedef struct {
ElementType Data;
int Parent;
}SetType;
- 查找,输入结构数组和要查找的元素,返回元素所在的集合(即该集合树的根节点)。
int Find(ElementType x, SetType S[]){
int i;
for(i = 0; i < MaxSize && S[i].Data != x; i++);
if(i >= MaxSize)return -1;
//此处的条件为大于等于0,其实也可以用“不等于-1”,但是因为后面的并操作会用
//根节点的Parent值存储别的信息,所以不用“不等于-1”
for(;S[i].Parent >= 0;i = S[i].Parent);
return i;
}
- 并,parent值用集合元素个数的负值表示(负值小的个数多)
void Union(SetType S[], ElementType x1, ElementType x2){
int root1,root2;
root1 = Find(x1,S);
root2 = Find(x2,S);
if(root1 != root2){
if(S[root1].Parent < S[root2].Parent){
S[root1].parent += S[root2].parent;
S[root2].parent = root1;
}
else{
S[root2].parent += S[root1].parent;
S[root1].parent = root2;
}
}
}