最大堆,哈夫曼树

优先队列的概念

出队是依照元素的优先级大小,而不是进入队列的先后顺序。

  • 向队列里插入新元素
  • 拿一个队列里优先级最高的元素

采用什么去实现一个优先队列 ?

一些方式插入删去最大值
数组在尾部插入O(1)找到最大值O(n),删除后移动元素O(n)
链表在头部插入O(1)找到最大值O(n),删除后移动元素O(1)
有序数组找到合适的位置插入O(n) \ O(log2n),移动元素O(n)直接删去最后一个元素O(1)
有序链表找到合适的位置O(n)删去最大元素O(1)
采用二叉树存储结构:最大的在树根,完全二叉树 ?先试试

最大堆

用数组表示的一个完全二叉树,每个节点的元素值不小于其子节点的元素值。
操作集:创建判断是否已满判断是否已空将元素插入返回最大元素

 typedef struct HeapStruct{
 	ElementType *Elements;		 
 	int size;// 当前元素个数 
 	int Capacity;// 最大容量 
 }*MaxHeap;
// 创建 
 MaxHeap Create(int MaxSize){
 	MaxHeap H=new struct HeapStruct;
 	H->Elements=new ElementType[MaxSize+1];	// 从下标为1的地方开始存放 
 	H->size=0;
 	H->Capacity=MaxSize;
 	H->Elements[0]=MaxData;		// 定义哨兵, 这里放一个很大的数 
 	return H;
 } 
bool IsFull( MaxHeap H ){return H->size == H->Capacity;}

最大堆的插入(上滤)

插入后要继续保持有序性:

假如数组里现有5个元素,先把新元素放在6号位置,然后和 3 号位置 (6号位置的父节点位置) 比较,如果 3号元素比6号元素小,就把3号元素拿下来,6号元素放到3号位置上,继续和3号位置的父节点(1号位置) 进行比较,如果1号元素小,就把他拿下来放到3号位置上,新的元素继续上去,由于0号元素是一个很大的值。所以,走到一号位置就会停下来了。

void Insert(MaxHeap H,ElementType item){
// H->Elements[0] 已被设置为哨兵
	if(IsFull(H)){
		cout<<"最大堆已满";
		return; 
	}
	int i=++H->size;//先将新元素放在最后的位置,i表示放置的数组索引
	for(;H->Elements[i/2]<item;i/=2){//将比item小的父节点元素向下移 
		H->Elements[i]=H->Elements[i/2];	 
	}
	H->Elements[i]=item;//最终要插入的位置
}

上滤过程中,只可能与祖先们交换
完全二叉树必平衡

时间复杂度:log2n,因为树的高度就是 在log n

最大堆的删除(下滤)

删除的位置确定,树根。
删掉之后先将数组最后的元素替补过去。
在这里插入图片描述确保有序性:
看 31 的左右儿子,挑一个大的和31进行替换
31 换到新位置 之后,继续看他的左右儿子,

ElementType DeleteMax(MaxHeap H){
	int parent,child;
	ElementType MaxItem,temp;
	if(IsEmpty(H)){
		cout<<"最大堆已空";
		return; 
	}
	MaxItem=H->Elements[1];			// 取出最大元素 
	temp=H->Elements[H->size--];	// 指向 最后一个元素 
	
	//寻找 temp 应该放置的地方: parent, 先放在 根节点, 然后 逐层的 寻找 合适的位置
	for(parent=1;parent*2<=H->size;parent=child){
		child=parent*2;
		if((child!=H->size)&&(H->Elements[child]<H->Elements[child+1]))
			child++;					//  找出 左右儿子 中 较大的一个 
		if(temp>=H->Elements[child])	//  如果 temp 比 这个位置的左右儿子 都大, 那 temp 放在这里就是合适的
			break;
		else
			H->Elements[parent]=H->Elements[child];
	}
	
	H->Elements[parent]=temp;
	return MaxItem;
}

时间复杂度:log2n

最大堆的建立

根据已存在的 n 个元素 按最大堆的要求,存放在一个一维数组里
假如 通过 插入 操作,则要做 n 轮循环,时间复杂度:在最坏情况下,每个节点都需要上滤至根,所需成本线性正比于其深度,因此总体的时间成本也应该就是每一个节点深度的总和。 在完全二叉树中,至少有一半节点是叶节点,而且在渐进意义上,他们的深度都是logn ,仅这部分节点而言,他们所花费的时间成本就是 nlog2n !! 有没有更好的 方法?

先将 n 个元素按顺序存入,先满足 完全二叉树 的结构 特性
再 调整 各节点 的位置,满足 有序特性

前面的删除操作中,当把最大元素(即根节点)拿掉后,数组末尾元素替换过去。此时,树根的左子树是一个堆,右子树也是一个堆,树根是一个新的元素。
所以实际上,在堆删除里面,最核心的操作是:已知左边是一个堆,右边是一个堆,来了一个新元素,怎么把它调成一个堆。
方法就是,跟下面的左右儿子去比较,然后调一个上来。
能不能把这种思路 用在 建堆 上面?
在这里插入图片描述
对 79 来讲,左边不是堆,右边也不是堆
对 66 来讲,左边不是堆,右边也不是堆
。。。。。。。
。。。。。
那从底下开始做,倒数第一个有 儿子的节点开始,上图中 是 87,对 87 来说,左边是一个堆,右边是一个堆。依据上面的策略,可以将其调成一个堆。然后将 30和他的左右儿子 调成一个堆,接着是 83 和他的左右儿子,然后是 43, 66,79

/* 将 H中以 H->Data[p]为根的子堆调整为最大堆 */
void PercDown( MaxHeap H, int p ){ 
    int Parent, Child;
    ElementType X;
    X = H->Data[p]; 									// 取出根结点存放的值 
    for( Parent=p; Parent*2<=H->Size; Parent=Child ) {
        Child = Parent * 2;
        if( (Child!=H->Size) && (H->Data[Child]<H->Data[Child+1]) )
            Child++;  /* Child指向左右子结点的较大者 */
        if( X >= H->Data[Child] ) 
			break; 										//  找到了合适位置 
        else  											 
            H->Data[Parent] = H->Data[Child];			//  将那个较大值调上来, 之后在下一层继续找
    }
    H->Data[Parent] = X;
}


void BuildHeap( MaxHeap H )
{ /* 调整 H->Data[]中的元素,使满足最大堆的有序性  */
    int i;
    /* 从最后一个结点的父节点开始,到根结点1 */
    for( i = H->Size/2; i>0; i-- )
        PercDown( H, i );
}

每一个要调整的根节点都会下降一定的高度,建堆时,最坏情况下需要挪动元素次数是等于树中各结点的高度和。
因此,整个效率就是每个节点所对应的高度之和—>渐进于 O(n)

时间复杂度:O(n)

为什么会出现一个是O(nlogn),一个是O(n)的差异现象呢?
原因在于,在完全二叉树中,越靠近底层,节点越多,越靠近顶层,节点越少。
因此以深度作为成本的指标,累计的总和自然更大。

总结

数据结构形式插入删除最大值构建
最大堆(数组表示的一个完全二叉树,且元素是有序的排放)O(log2n) 上滤O(log2n) 下滤先顺序存放,再调整为最大堆 O(n)

哈夫曼树

对于二叉树上的每一个节点,有时候去搜索它的 频率 / 概率,每个节点的是不一样的。
一般来说,我们应该把 查找频率 高的节点 放在靠上的层级。查找频率 比较低的 放在 靠下 的层级。

怎么根据不同的频率来构造一个效率比较好甚至是最好的搜索树?
带权路径长度(WPL):假设二叉树有 n 个叶子结点,每个叶节点 带有权值 Wk,其深度为Lk
W P L = ∑ k = 1 n W k L k WPL=\sum_{k=1}^{n}W_{k}L_{k} WPL=k=1nWkLk
哈夫曼树:WPL最小的树。

哈夫曼树的构造

依元素的权值(查找频率),进行从小到大的排序,之后把 权值最小的两个元素并在一起,形成一个新的二叉树,这个二叉树的权值就是并在一起的两个元素的权值的和。从得到的新节点和剩余节点里,继续挑两个最小的,继续并在一起,重复直至没有剩余节点。

typedef struct TreeNode * Huffman;
struct TreeNode{
	int weight;
	Huffman Left,Right;
};
// H 是一个最小堆, 数组里每一个元素 都是一个 指向 struct TreeNode 的指针
Huffman build(MinHeap H){
	int i;
	Huffman T;
	BuildHeap(H);			//  调整为最小堆 	       O(n) 复杂度 
	int count=H->Size;
	for(int i=1;i<count;i++){			// Size-1 次 合并
		T=new struct TreeNode;
		T->Left=DeleteMin(H);	//  从堆中 取出两个 最小的 
		T->Right=DeleteMin(H);
		T->weight=T->Left->weight+T->Right->weight;
		Insert(H,T); 	// 构建形成的新的节点插入到堆里 
	}
	T=DeleteMin(H);
	return T;
}

时间复杂度: nlogn

哈夫曼树的特点

  • 没有度为1的节点。
  • n个叶子节点的哈夫曼树共有 2n-1 个节点
  • 哈夫曼树的任意非叶节点的左右子树交换后仍是哈夫曼树。
  • 对于同一组权值{w1,w2,…,wn},存在不同构的两颗哈夫曼树。如:{1,2,3,3},但 WPL是一样的。

哈夫曼编码

一段字符串,不同的字符出现的频次是不一样的,如何对字符进行编码? 使得该字符串的编码存储空间最少。
例如:一段文本,有58个字符,由7个字符构成 (a,e,i,s,t,空格,换行) 每个字符出现的频次不同

每个字符采用ASCII编码,58 * 8=464位
采用等长三位二进制,58 * 3=174 位
不等长编码:频率高的字符用的编码尽量要短,频率低的用的编码可以相对长一些。(也就是频率高的尽量放在二叉树的上层)

进行不等长编码,要避免二义性,任何字符的编码都不是另一字符的编码的前缀。
当所有要进行编码的字符 都在叶节点上的时候,就可以满足(任何字符的编码都不是另一字符的编码的前缀)。

关于如何写数学公式

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
## 数组 - [1-1 动态扩容的数组](1.1_dynamicArray.py) - [1-2 大小固定的有序数组](1.2_orderArray.py) ## 链表 - [2-1 无序链表](2.1_unOrderList.py) - [2-2 有序链表](2.2_orderList.py) - [2-3 双向链表](2.3_biList.py) - 2-4 循环链表 - 2-5 链表反转 - 2-6 合并有序链表 - 2-7 链表中间节点 ## 栈 - 3-1 顺序栈 - 3-2 链式栈 - 3-3 最大栈 ## 队列 - 4-1 顺序队列 - 4-2 链式队列 - 4-3 循环队列 ## 树 - [5-1 树的基本操作](utils.py) - [5-2 二叉树](5.2_binaryTree.py) - [5-3 二叉堆](5.3_binaryHeap.py) - [5-4 二叉搜索树](5.4_binarySearchTree.py) - [5-5 平衡二叉树](5.5_avlTree.py) - [5-6 红黑树](5.6_rbTree.py) - [5-7 trie](5.7_trie.py) - [5-8 双数组字典树](5.8_datree.py) - [5-9 哈夫曼树](5.9_huffmanTree.py) ## 图 - [6-1 图基础操作](6.1_networkx.py) - [6-2 广度优先搜索](6.2_bfs.py) - [6-3 深度优先搜索](6.3_dfs.py) - [6-4 prim](6.4_prim.py) - [6-5 kruskal](6.5_kruskal.py) - [6-6 Dijkstra](6.6_dijkstra.py) - [6-7 Floyd](6.7_floyd.py) - [6-8 拓扑排序](6.8_topologic.py) ## 搜索 - 7-1 顺序查找 - 7-2 二分查找 - 7-3 插值查找 - 7-4 斐波那契查找 - 7-5 稠密索引 - 7-6 分块索引 - 7-7 倒排索引 - 7-8 哈希 -------- 该资源内项目源码是个人的毕设,代码都测试ok,都是运行成功后才上传资源,答辩评审平均分达到96分,放心下载使用! <项目介绍> 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用! 2、本项目适合计算机相关专业(如计科、人工智能、通信工程、自动化、电子信息等)的在校学生、老师或者企业员工下载学习,也适合小白学习进阶,当然也可作为毕设项目、课程设计、作业、项目初期立项演示等。 3、如果基础还行,也可在此代码基础上进行修改,以实现其他功能,也可用于毕设、课设、作业等。 下载后请首先打开README.md文件(如有),仅供学习参考, 切勿用于商业用途。 --------

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值