优先队列的概念
出队是依照元素的优先级大小,而不是进入队列的先后顺序。
- 向队列里插入新元素
- 拿一个队列里优先级最高的元素
采用什么去实现一个优先队列 ?
一些方式 | 插入 | 删去最大值 |
---|---|---|
数组 | 在尾部插入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=1∑nWkLk
哈夫曼树: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 位
不等长编码:频率高的字符用的编码尽量要短,频率低的用的编码可以相对长一些。(也就是频率高的尽量放在二叉树的上层)
进行不等长编码,要避免二义性,任何字符的编码都不是另一字符的编码的前缀。
当所有要进行编码的字符 都在叶节点上的时候,就可以满足(任何字符的编码都不是另一字符的编码的前缀)。