1.大根堆

1.1 定义

  大根树:树中的每一个节点的值都大于或等于其子节点的值

  大根堆:既是大根树又是完全二叉树(增加了完全二叉树的限制条件)所以下图中只有(a)和(c)是大根堆

1.2 大根堆的插入(数组实现)

  假设在下面大根堆中插入一个元素9,插入步骤如下,时间复杂度为O(height)=O(logn)

  • 尝试插入在6号位置,如果新的元素小于3号位置,则插入;否则把3号位置的元素向下移动到6号位置
  • 尝试插入在3号位置,如果新的元素小于1号元素,则插入;否则把1号位置的元素向下移动到3号位置,循环终止
1
2
3
4
5
6
7
8
9
// 为theElement寻找插入位置, currentNode从新叶子节点开始向上移动
int currentNode = ++heapSize;
while (currentNode != 1 && heap[currentNode / 2] < theElement)
{
heap[currentNode] = heap[currentNode / 2]; // 元素向下移动
currentNode /= 2; // currentNode移动到他的双亲
}

heap[currentNode] = theElement;

1.3 大根堆的出队

  出队元素是最大的元素,也就是根节点,我们首先将1号元素删除,然后拿到最后一个元素,从上向下做插入的操作,时间复杂度为O(height)=O(logn)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 从根节点开始, 为最后一个元素查找插入位置
int currentNode = 1; // 当前双亲节点
int child = 2; // 当前孩子节点
while (child <= heapSize)
{
// heap[child] 应该是 currentNode 更大的孩子
if (child < heapSize && heap[child] < heap[child + 1])
child++;

// 对比 lastElement 是否可以插入到 currentNode
if (lastElement >= heap[child])
break; // 可以插入则直接结束

// 不可以插入则继续循环
heap[currentNode] = heap[child]; // 子节点向上移动
currentNode = child; // 当前指向节点移动到子节点
child *= 2;
}
heap[currentNode] = lastElement;

1.4 大根堆的初始化

  自下而上:从最后一个元素开始对比,到第一个元素结束,具体步骤如下

  • ①找到孩子节点 2 和 7 中更大的元素,接下来与其双亲节点对比,7 更大所以交换 5 和 7 的位置
  • ①找到孩子节点 7 和 6 中更大的元素,接下来与其双亲节点对比,7 更大所以交换 1 和 7 的位置;②找到孩子节点 2 和 5 中更大的元素,接下来与其双亲节点对比,5 更大所以交换 1 和 5 的位置

  这里的循环包括两个步骤,第一个步骤是从下到上交换一次,第二个步骤是从上到下再检查一遍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 堆化
for (int root = heapSize / 2; root >= 1; root--)
{
T rootElement = heap[root];

// 给元素 rootElement 寻找位置
int child = 2 * root; // 孩子 child 的双亲是 rootElement 的位置
// 确定 rootElement 的位置
while (child <= heapSize)
{
// heap[child] 找到孩子里更大的那一个
if (child < heapSize && heap[child] < heap[child + 1])
child++;

// 是否可以把 rootElement 插入到该位置
if (rootElement >= heap[child])
break; // 可以

// 不可以
heap[child / 2] = heap[child]; // 孩子向上移动
child *= 2; // 下移一层查看
}
heap[child / 2] = rootElement;
}

1.5 完整代码

1.5.1 优先级队列抽象父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
using namespace std;
template<class T>
class maxPriorityQueue
{
public:
virtual ~maxPriorityQueue() {}
// 队列是否为空
virtual bool empty() const = 0;
// 队列大小
virtual int size() const = 0;
// 返回优先级最高的元素
virtual const T& top() = 0;
// 弹出优先级最高的元素
virtual void pop() = 0;
// 插入元素
virtual void push(const T& theElement) = 0;
};

1.5.2 大根堆的数组实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#pragma once
#include "maxPriorityQueue.h"
#include <iostream>
#include <algorithm>

using namespace std;

template<class T>
class maxHeap : public maxPriorityQueue<T>
{
private:
int heapSize; // 队列中的元素数量
int arrayLength; // 队列的容量
T* heap; // 元素数组

public:
maxHeap(int initialCapacity = 10);
~maxHeap() { delete[] heap; }
bool empty() const { return heapSize == 0; }
int size() const { return heapSize; }
const T& top()
{// 返回优先级最大的元素
if (heapSize == 0)
{
cout << "队列为空" << endl;
return NULL;
}
return heap[1];
}
void pop();
void push(const T&);
void initialize(T*, int);
void deactivateArray()
{
heap = NULL; arrayLength = heapSize = 0;
}
void output() const;
};


template<class T>
void changeLength(T*& a, int oldLength, int newLength)
{// 改变数组长度
if (newLength < 1)return;
T* temp = new T[newLength]; // 新的数组
int number = min(oldLength, newLength); // 需要复制的元素个数
copy(a, a + number, temp);
delete[] a; // 清理内存
a = temp;
}

template<class T>
maxHeap<T>::maxHeap(int initialCapacity)
{// 构造函数
if (initialCapacity < 1)
{
cout << "构造大根堆的数量必须大于1" << endl;
return;
}
arrayLength = initialCapacity + 1;
heap = new T[arrayLength];
heapSize = 0;
}

template<class T>
void maxHeap<T>::push(const T& theElement)
{// 将元素插入到大根堆

// 检查数组容量
if (heapSize == arrayLength - 1)
{
changeLength(heap, arrayLength, 2 * arrayLength);
arrayLength *= 2;
}

// 为theElement寻找插入位置, currentNode从新叶子节点开始向上移动
int currentNode = ++heapSize;
while (currentNode != 1 && heap[currentNode / 2] < theElement)
{
heap[currentNode] = heap[currentNode / 2]; // 元素向下移动
currentNode /= 2; // currentNode移动到他的双亲
}

heap[currentNode] = theElement;
}

template<class T>
void maxHeap<T>::pop()
{// 从大根堆删除最大的元素

if (heapSize == 0) // 大根堆是否为空
return;

// 删除最大的元素
heap[1].~T();

// 拿到最后一个元素, 并将大根堆元素数量减1
T lastElement = heap[heapSize--];

// 从根节点开始, 为最后一个元素查找插入位置
int currentNode = 1; // 当前双亲节点
int child = 2; // 当前孩子节点
while (child <= heapSize)
{
// heap[child] 应该是 currentNode 更大的孩子
if (child < heapSize && heap[child] < heap[child + 1])
child++;

// 对比 lastElement 是否可以插入到 currentNode
if (lastElement >= heap[child])
break; // 可以插入则直接结束

// 不可以插入则继续循环
heap[currentNode] = heap[child]; // 子节点向上移动
currentNode = child; // 当前指向节点移动到子节点
child *= 2;
}
heap[currentNode] = lastElement;
}

template<class T>
void maxHeap<T>::initialize(T* theHeap, int theSize)
{// 在数组 theHeap[1:theSize] 中初始化大根堆
delete[] heap;
heap = theHeap;
heapSize = theSize;

// 堆化
for (int root = heapSize / 2; root >= 1; root--)
{
T rootElement = heap[root];

// 给元素 rootElement 寻找位置
int child = 2 * root; // 孩子 child 的双亲是 rootElement 的位置
// 确定 rootElement 的位置
while (child <= heapSize)
{
// heap[child] 找到孩子里更大的那一个
if (child < heapSize && heap[child] < heap[child + 1])
child++;

// 是否可以把 rootElement 插入到该位置
if (rootElement >= heap[child])
break; // 可以

// 不可以
heap[child / 2] = heap[child]; // 孩子向上移动
child *= 2; // 下移一层查看
}
heap[child / 2] = rootElement;
}
}

template<class T>
void maxHeap<T>::output() const
{
for (int i = 1; i < heapSize; i++)
{
cout << this->heap[i] << " ";
}
cout << endl;
}

2.左高树

2.1 高度优先左高树和重量优先左高树

  左高树合并操作的时间复杂度更低,适合需要合并操作较多的数据

  定义一类特殊的节点为外部节点来代替空子树,其余节点为内部节点。增加了外部节点的二叉树称为扩充二叉树

  定义距离s:s 是该节点到外部节点的最短路径

  定义重量w:w 是该节点重量的和,节点本身重量为1,节点的重量为自身重量与孩子重量的和。

  高度优先左高树(height-biased leftist tree,HBLT):这个二叉树任何一个内部节点左孩子的 s 值都大于等于右孩子的 s 值。其中上图(a)就不是一棵左高树,因为它左侧子树的左孩子值为0,而右孩子值为1,下图是2个HBLT。

  重量优先左高树(weight-biased leftist tree,WBLT):这个二叉树任何一个内部节点左孩子的 w 值都大于或等于右孩子的 w 值。

2.2 HBLT的性质

  1. 堆的性质:任意节点值的大小 ≤ 其孩子节点的大小(小根堆,也可以是 ≥ )
  2. 左偏性质:左孩子的距离 s ≥ 右孩子的距离 s
  3. 任意节点的距离 s 都等于其右孩子的距离 + 1
  4. 一棵有 n 个节点的二叉树,根的距离dis ≤ long(n+1) - 1(距离最远就是满二叉树的情况,否则一定有更短路径出去)

2.3 HBLT的插入和删除

  插入相当于将已有的树和一棵仅有一个元素的树进行合并操作

  删除相当于将左右两棵HBLT进行合并操作

2.4 HBLT的合并(链式实现)

  用一张图解释合并的过程,我们需要把两棵树合并到 x 上

  • 对比 x 指向节点数值和 y 指向节点数值的大小
  • 如果节点数值 x < y,则将 x 指向的地址和 y 指向的地址交换
  • 将 x 指针移动到 x 的右孩子位置

  这就是图左完整的递归过程,递归的过程实际上就是在比较两个左高树右侧孩子链的大小,并来回交换来保证堆的性质,所以整个递归应该是O(logm)+O(logn)的时间复杂度

  • 首先 x 移动到下一个位置指向元素 7,对比元素 7 和元素 8,元素 8 更大,所以指针指向的地址互换,元素 8 移动到右孩子的位置
  • 然后 x 移动到下一个位置指向元素 6,对比元素 6 和元素 7,元素 7 更大,所以指针指向的地址呼唤,元素 7 移动到右孩子的位置
  • 然后 x 移动到下一个位置指向 NULL,因为 x 指向 NULL,所以直接将 x 指向 y 所指向的地址,递归结束

  最后一步是回溯,已经完成元素的插入后,需要维护左高树的性质,回溯的过程就是根据距离不断交换孩子节点的位置,保证左孩子的距离 ≥ 右孩子的距离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
template<class T>
void maxHblt<T>::merged(binaryTreeNode<pair<int, T> >*& x, binaryTreeNode<pair<int, T> >*& y)
{// 合并两棵左高树, x是自己的根节点地址, y是传入的根节点地址
if (y == NULL) // 如果传入的树为空, 不需要操作
return;
if (x == NULL) // 如果x节点为空, 则直接让该节点的值等于传入节点的值
{
x = y; return;
}

// 维护堆的性质, 保证 x 是更大的那一个
if (x->element.second < y->element.second)
swap(x, y);

// 接下来进行递归, x 向其右孩子移动一位
merged(x->rightChild, y);

// 最后是回溯的过程, 需要维护左高树的性质, 保证左孩子的路径长度大于右孩子
if (x->leftChild == NULL)
{// 左树为空直接交换即可
x->leftChild = x->rightChild;
x->rightChild = NULL;
x->element.first = 1;
}
else
{// 左树不为空则对比他们的路径长度
if (x->leftChild->element.first < x->rightChild->element.first)
swap(x->leftChild, x->rightChild);
// 最后更新路径长度
x->element.first = x->rightChild->element.first + 1;
}
}

2.5 HBLT的初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
template<class T>
void maxHblt<T>::initialize(T* theElements, int theSize)
{// 根据给定的数组初始化左高树
arrayQueue<binaryTreeNode<pair<int, T> >*> q(theSize);

// 初始化树的队列
for (int i = 1; i <= theSize; i++)
// 建立一个只有一个节点的树插入队列
q.push(new binaryTreeNode<pair<int, T> >
(pair<int, T>(1, theElements[i])));

// 从队列中重复取出树进行合并操作
for (int i = 1; i <= theSize - 1; i++)
{// 从队列中取出2个树
binaryTreeNode<pair<int, T> >* b = q.front();
q.pop();
binaryTreeNode<pair<int, T> >* c = q.front();
q.pop();
merged(b, c);
// 把合并后的树插入队列
q.push(b);
}

if (theSize > 0)
this->root = q.front();
this->treeSize = theSize;
}