一. 堆的基本存储
关于堆这种数据结构的详细阐述,可以参看数据结构系列博文中的‘堆和优先队列’这篇博文。这里会阐述地更加简洁,会提供c++版本的堆实现代码。
二. Shift Up
向堆中添加元素
对于最大堆, 新加入的元素, 如果大于它的父元素, 就和它的父元素互换位置。这步操作称为shift up。
只要新元素的父节点比它大, 就会一直做shift up
代码实现
main.cpp(主要查看shiftup函数的逻辑)
#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cmath>
#include <cassert>
using namespace std;
template<typename Item>
class MaxHeap{
private:
Item *data;
int count;
int capacity;
void shiftUp(int k){
while( k > 1 && data[k/2] < data[k] ){
swap( data[k/2], data[k] );
k /= 2;
}
}
public:
// 构造函数, 构造一个空堆, 可容纳capacity个元素
MaxHeap(int capacity){
data = new Item[capacity+1];
count = 0;
this->capacity = capacity;
}
~MaxHeap(){
delete[] data;
}
// 返回堆中的元素个数
int size(){
return count;
}
// 返回一个布尔值, 表示堆中是否为空
bool isEmpty(){
return count == 0;
}
// 像最大堆中插入一个新的元素 item
void insert(Item item){
assert( count + 1 <= capacity );
data[count+1] = item;
count ++;
shiftUp(count);
}
public:
// 以树状打印整个堆结构
void testPrint(){
// 我们的testPrint只能打印100个元素以内的堆的树状信息
if( size() >= 100 ){
cout<<"This print function can only work for less than 100 int";
return;
}
// 我们的testPrint只能处理整数信息
if( typeid(Item) != typeid(int) ){
cout <<"This print function can only work for int item";
return;
}
cout<<"The max heap size is: "<<size()<<endl;
cout<<"Data in the max heap: ";
for( int i = 1 ; i <= size() ; i ++ ){
// 我们的testPrint要求堆中的所有整数在[0, 100)的范围内
assert( data[i] >= 0 && data[i] < 100 );
cout<<data[i]<<" ";
}
cout<<endl;
cout<<endl;
int n = size();
int max_level = 0;
int number_per_level = 1;
while( n > 0 ) {
max_level += 1;
n -= number_per_level;
number_per_level *= 2;
}
int max_level_number = int(pow(2, max_level-1));
int cur_tree_max_level_number = max_level_number;
int index = 1;
for( int level = 0 ; level < max_level ; level ++ ){
string line1 = string(max_level_number*3-1, ' ');
int cur_level_number = min(count-int(pow(2,level))+1,int(pow(2,level)));
bool isLeft = true;
for( int index_cur_level = 0 ; index_cur_level < cur_level_number ; index ++ , index_cur_level ++ ){
putNumberInLine( data[index] , line1 , index_cur_level , cur_tree_max_level_number*3-1 , isLeft );
isLeft = !isLeft;
}
cout<<line1<<endl;
if( level == max_level - 1 )
break;
string line2 = string(max_level_number*3-1, ' ');
for( int index_cur_level = 0 ; index_cur_level < cur_level_number ; index_cur_level ++ )
putBranchInLine( line2 , index_cur_level , cur_tree_max_level_number*3-1 );
cout<<line2<<endl;
cur_tree_max_level_number /= 2;
}
}
private:
void putNumberInLine( int num, string &line, int index_cur_level, int cur_tree_width, bool isLeft){
int sub_tree_width = (cur_tree_width - 1) / 2;
int offset = index_cur_level * (cur_tree_width+1) + sub_tree_width;
assert(offset + 1 < line.size());
if( num >= 10 ) {
line[offset + 0] = '0' + num / 10;
line[offset + 1] = '0' + num % 10;
}
else{
if( isLeft)
line[offset + 0] = '0' + num;
else
line[offset + 1] = '0' + num;
}
}
void putBranchInLine( string &line, int index_cur_level, int cur_tree_width){
int sub_tree_width = (cur_tree_width - 1) / 2;
int sub_sub_tree_width = (sub_tree_width - 1) / 2;
int offset_left = index_cur_level * (cur_tree_width+1) + sub_sub_tree_width;
assert( offset_left + 1 < line.size() );
int offset_right = index_cur_level * (cur_tree_width+1) + sub_tree_width + 1 + sub_sub_tree_width;
assert( offset_right < line.size() );
line[offset_left + 1] = '/';
line[offset_right + 0] = '\\';
}
};
// 测试 MaxHeap
int main() {
MaxHeap<int> maxheap = MaxHeap<int>(100);
srand(time(NULL));
for( int i = 0 ; i < 50 ; i ++ )
maxheap.insert( rand()%100 );
maxheap.testPrint();
return 0;
}
测试结果
The max heap size is: 50
Data in the max heap: 93 91 88 76 91 75 81 72 71 87 88 64 59 75 52 41 59 55 45 72 79 66 70 47 47 25 37 5 4 18 24 10 17 15 13 25 54 36 43 23 57 49 24 23 37 34 69 17 29 26
93
/ \
91 88
/ \ / \
76 91 75 81
/ \ / \ / \ / \
72 71 87 88 64 59 75 52
/ \ / \ / \ / \ / \ / \ / \ / \
41 59 55 45 72 79 66 70 47 47 25 37 5 4 18 24
/ \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \ / \
10 17 15 13 25 54 36 43 23 57 49 24 23 37 34 69 17 29 26
三. Shift Down
从堆中取出元素
1.从堆中取出元素,只能从堆顶取元素, 对于最大堆,就是取出最大元素。
2.在取出最大元素后, 堆结构会被破坏。
3.把最后一个元素B放在根结点上(此时明显不满足最大堆的性质)
4.比较B的左右两孩子节点, 让B和它们中大的那个元素互换位置(操作称为shift down)
5.对B持续做shift down 直到B没有比它大的孩子节点存在
代码编写
main.cpp
...
private:
Item *data;
int count;
int capacity;
void shiftUp(int k){
while( k > 1 && data[k/2] < data[k] ){
swap( data[k/2], data[k] );
k /= 2;
}
}
void shiftDown(int k){
while( 2*k <= count ){
int j = 2*k; // 在此轮循环中,data[k]和data[j]交换位置
if( j+1 <= count && data[j+1] > data[j] ) //右孩子大的话j++
j ++;
// data[j] 是 data[2*k]和data[2*k+1]中的最大值
if( data[k] >= data[j] ) break;
swap( data[k] , data[j] );
k = j;
}
}
public:
...
// 测试最大堆
int main() {
MaxHeap<int> maxheap = MaxHeap<int>(100);
srand(time(NULL));
int n = 100; // 随机生成n个元素放入最大堆中
for( int i = 0 ; i < n ; i ++ ){
maxheap.insert( rand()%100 );
}
int* arr = new int[n];
// 将maxheap中的数据逐渐使用extractMax取出来
// 取出来的顺序应该是按照从大到小的顺序取出来的
for( int i = 0 ; i < n ; i ++ ){
arr[i] = maxheap.extractMax();
cout<<arr[i]<<" ";
}
cout<<endl;
// 确保arr数组是从大到小排列的
for( int i = 1 ; i < n ; i ++ )
assert( arr[i-1] >= arr[i] );
delete[] arr;
return 0;
}
测试结果确实是按顺序排列
99 99 97 97 95 95 95 .........7 6 5 4 3 1
四. 基础堆排序和Heapify
将之前实现的最大堆放入Heap.h文件中, 在本节中我们将实现堆排序
heapify1
将要排序的数组一次放入堆中, 放完之后再一次取出重新放回数组中, 这样数组就变成有序的了
main.cpp
// heapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
// 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
// 整个堆排序的整体时间复杂度为O(nlogn)
template<typename T>
void heapSort1(T arr[], int n){
MaxHeap<T> maxheap = MaxHeap<T>(n);
for( int i = 0 ; i < n ; i ++ )
maxheap.insert(arr[i]);
for( int i = n-1 ; i >= 0 ; i-- )//得到是从小到大排序
arr[i] = maxheap.extractMax();
}
heapify2
算法思路
1. 找到第一个非叶子节点的节点A(节点总数/2),如下图所示即为22
2. 对A做shift down
3. 依次对A前面序号的节点都做shift down, 即下图中索引为 4 3 2 1的节点
代码编写
在Heap.h中新增一种直接传入数组的构造函数
...
public:
// 构造函数, 构造一个空堆, 可容纳capacity个元素
MaxHeap(int capacity){
data = new Item[capacity+1];
count = 0;
this->capacity = capacity;
}
// 构造函数, 通过一个给定数组创建一个最大堆
// 该构造堆的过程, 时间复杂度为O(n)
MaxHeap(Item arr[], int n){
data = new Item[n+1];
capacity = n;
for( int i = 0 ; i < n ; i ++ )
data[i+1] = arr[i];
count = n;
for( int i = count/2 ; i >= 1 ; i -- ) //count/2 正好是第一个不是叶子节点的节点
shiftDown(i);
}
~MaxHeap(){
delete[] data;
}
...
在main.cpp中实现heapify2并测试对比
#include <iostream>
#include <algorithm>
#include "Heap.h"
#include "MergeSort.h"
#include "QuickSort.h"
#include "QuickSort2Ways.h"
#include "QuickSort3Ways.h"
#include "SortTestHelper.h"
using namespace std;
// heapSort1, 将所有的元素依次添加到堆中, 在将所有元素从堆中依次取出来, 即完成了排序
// 无论是创建堆的过程, 还是从堆中依次取出元素的过程, 时间复杂度均为O(nlogn)
// 整个堆排序的整体时间复杂度为O(nlogn)
template<typename T>
void heapSort1(T arr[], int n){
MaxHeap<T> maxheap = MaxHeap<T>(n);
for( int i = 0 ; i < n ; i ++ )
maxheap.insert(arr[i]);
for( int i = n-1 ; i >= 0 ; i-- )//得到是从小到大排序
arr[i] = maxheap.extractMax();
}
// heapSort2, 借助我们的heapify过程创建堆
// 此时, 创建堆的过程时间复杂度为O(n), 将所有元素依次从堆中取出来, 实践复杂度为O(nlogn)
// 堆排序的总体时间复杂度依然是O(nlogn), 但是比上述heapSort1性能更优, 因为创建堆的性能更优
template<typename T>
void heapSort2(T arr[], int n){
MaxHeap<T> maxheap = MaxHeap<T>(arr,n);
for( int i = n-1 ; i >= 0 ; i-- )
arr[i] = maxheap.extractMax();
}
// 比较 Merge Sort, 三种 Quick Sort 和本节介绍的两种 Heap Sort 的性能效率
// 注意, 这几种排序算法都是 O(nlogn) 级别的排序算法
int main() {
int n = 1000000;
// 测试1 一般性测试
cout<<"Test for random array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
int* arr2 = SortTestHelper::copyIntArray(arr1, n);
int* arr3 = SortTestHelper::copyIntArray(arr1, n);
int* arr4 = SortTestHelper::copyIntArray(arr1, n);
int* arr5 = SortTestHelper::copyIntArray(arr1, n);
int* arr6 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
cout<<endl;
// 测试2 测试近乎有序的数组
int swapTimes = 100;
cout<<"Test for nearly ordered array, size = "<<n<<", swap time = "<<swapTimes<<endl;
arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1, n);
arr5 = SortTestHelper::copyIntArray(arr1, n);
arr6 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
cout<<endl;
// 测试3 测试存在包含大量相同元素的数组
cout<<"Test for random array, size = "<<n<<", random range [0,10]"<<endl;
arr1 = SortTestHelper::generateRandomArray(n,0,10);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1, n);
arr5 = SortTestHelper::copyIntArray(arr1, n);
arr6 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
// 这种情况下, 普通的QuickSort退化为O(n^2)的算法, 不做测试
//SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
return 0;
}
测试结果
Test for random array, size = 1000000, random range [0, 1000000]
Merge Sort : 0.210484 s
Quick Sort : 0.180363 s
Quick Sort 2 Ways : 0.167387 s
Quick Sort 3 Ways : 0.208507 s
Heap Sort 1 : 0.371742 s
Heap Sort 2 : 0.333799 s
Test for nearly ordered array, size = 1000000, swap time = 100
Merge Sort : 0.053639 s
Quick Sort : 0.098584 s
Quick Sort 2 Ways : 0.060396 s
Quick Sort 3 Ways : 0.15452 s
Heap Sort 1 : 0.356034 s
Heap Sort 2 : 0.19532 s
Test for random array, size = 1000000, random range [0,10]
Merge Sort : 0.13083 s
Quick Sort 2 Ways : 0.07854 s
Quick Sort 3 Ways : 0.029636 s
Heap Sort 1 : 0.210654 s
Heap Sort 2 : 0.187121 s
可以看到堆排序效率不如快速排序和归并排序, 堆这种数据结构更多的是在动态数据维护的使用上。
heapify的算法复杂度
1.将n个元素逐个插入一个空堆中(heapify1),算法复杂度是O(nlogn)
2.直接将一整个数组构建成一个堆(heapify2),算法复杂度是O(n)
五. 优化的堆排序
之前实现的两个heapify都是将数据放入最大堆中,在从堆中取出放回数组。
现在我们要实现的heapify是在堆中原地进行heapify,不需要另外放回数组中,直接得到排好序的数组。
步骤
1. 与heapify2一样,先将数组中数据放入堆中,并做heapify2(这回堆从index0开始,之前都是从index1开始。主要是公式稍有区别)
2. 将堆顶元素与最后一个元素互换
3. 将存放堆的数组的最后一个元素除外的 元素,看成堆结构。对index0(即交换后的堆顶元素)做shift down
4. 对新的堆重复做2和3两个步骤,直到排好序
代码实现
将之前的heapify1和heapify2 放入Heapsort.h文件中存放, 本节代码写入main.cpp中,并测试
#include <iostream>
#include <algorithm>
#include "MergeSort.h"
#include "QuickSort.h"
#include "QuickSort2Ways.h"
#include "QuickSort3Ways.h"
#include "HeapSort.h"
#include "SortTestHelper.h"
using namespace std;
// 原始的shiftDown过程
template<typename T>
void __shiftDown(T arr[], int n, int k){
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1] > arr[j] )
j += 1;
if( arr[k] >= arr[j] )break;
swap( arr[k] , arr[j] );
k = j;
}
}
// 优化的shiftDown过程, 使用赋值的方式取代不断的swap,
// 该优化思想和我们之前对插入排序进行优化的思路是一致的
template<typename T>
void __shiftDown2(T arr[], int n, int k){
T e = arr[k];
while( 2*k+1 < n ){
int j = 2*k+1;
if( j+1 < n && arr[j+1] > arr[j] )
j += 1;
if( e >= arr[j] ) break;
arr[k] = arr[j];
k = j;
}
arr[k] = e;
}
// 不使用一个额外的最大堆, 直接在原数组上进行原地的堆排序
template<typename T>
void heapSort(T arr[], int n){
// 注意,此时我们的堆是从0开始索引的
// 从(最后一个元素的索引-1)/2开始
// 最后一个元素的索引 = n-1
for( int i = (n-1-1)/2 ; i >= 0 ; i -- )
__shiftDown2(arr, n, i);
for( int i = n-1; i > 0 ; i-- ){
swap( arr[0] , arr[i] );
__shiftDown2(arr, i, 0);
}
}
// 比较 Merge Sort, 三种 Quick Sort 和本节介绍的三种 Heap Sort 的性能效率
// 注意, 这几种排序算法都是 O(nlogn) 级别的排序算法
int main() {
int n = 1000000;
// 测试1 一般性测试
cout<<"Test for random array, size = "<<n<<", random range [0, "<<n<<"]"<<endl;
int* arr1 = SortTestHelper::generateRandomArray(n,0,n);
int* arr2 = SortTestHelper::copyIntArray(arr1, n);
int* arr3 = SortTestHelper::copyIntArray(arr1, n);
int* arr4 = SortTestHelper::copyIntArray(arr1, n);
int* arr5 = SortTestHelper::copyIntArray(arr1, n);
int* arr6 = SortTestHelper::copyIntArray(arr1, n);
int* arr7 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
SortTestHelper::testSort("Heap Sort 3", heapSort, arr7, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
delete[] arr7;
cout<<endl;
// 测试2 测试近乎有序的数组
int swapTimes = 100;
cout<<"Test for nearly ordered array, size = "<<n<<", swap time = "<<swapTimes<<endl;
arr1 = SortTestHelper::generateNearlyOrderedArray(n,swapTimes);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1, n);
arr5 = SortTestHelper::copyIntArray(arr1, n);
arr6 = SortTestHelper::copyIntArray(arr1, n);
arr7 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
SortTestHelper::testSort("Heap Sort 3", heapSort, arr7, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
delete[] arr7;
cout<<endl;
// 测试3 测试存在包含大量相同元素的数组
cout<<"Test for random array, size = "<<n<<", random range [0,10]"<<endl;
arr1 = SortTestHelper::generateRandomArray(n,0,10);
arr2 = SortTestHelper::copyIntArray(arr1, n);
arr3 = SortTestHelper::copyIntArray(arr1, n);
arr4 = SortTestHelper::copyIntArray(arr1, n);
arr5 = SortTestHelper::copyIntArray(arr1, n);
arr6 = SortTestHelper::copyIntArray(arr1, n);
arr7 = SortTestHelper::copyIntArray(arr1, n);
SortTestHelper::testSort("Merge Sort", mergeSort, arr1, n);
// 这种情况下, 普通的QuickSort退化为O(n^2)的算法, 不做测试
//SortTestHelper::testSort("Quick Sort", quickSort, arr2, n);
SortTestHelper::testSort("Quick Sort 2 Ways", quickSort2Ways, arr3, n);
SortTestHelper::testSort("Quick Sort 3 Ways", quickSort3Ways, arr4, n);
SortTestHelper::testSort("Heap Sort 1", heapSort1, arr5, n);
SortTestHelper::testSort("Heap Sort 2", heapSort2, arr6, n);
SortTestHelper::testSort("Heap Sort 3", heapSort, arr7, n);
delete[] arr1;
delete[] arr2;
delete[] arr3;
delete[] arr4;
delete[] arr5;
delete[] arr6;
delete[] arr7;
return 0;
}
运行结果
Test for random array, size = 1000000, random range [0, 1000000]
Merge Sort : 0.195096 s
Quick Sort : 0.170002 s
Quick Sort 2 Ways : 0.160996 s
Quick Sort 3 Ways : 0.199719 s
Heap Sort 1 : 0.411558 s
Heap Sort 2 : 0.428785 s
Heap Sort 3 : 0.295679 s
Test for nearly ordered array, size = 1000000, swap time = 100
Merge Sort : 0.057489 s
Quick Sort : 0.103088 s
Quick Sort 2 Ways : 0.060194 s
Quick Sort 3 Ways : 0.136998 s
Heap Sort 1 : 0.35377 s
Heap Sort 2 : 0.202259 s
Heap Sort 3 : 0.138564 s
Test for random array, size = 1000000, random range [0,10]
Merge Sort : 0.136388 s
Quick Sort 2 Ways : 0.080629 s
Quick Sort 3 Ways : 0.031261 s
Heap Sort 1 : 0.214483 s
Heap Sort 2 : 0.191844 s
Heap Sort 3 : 0.136415 s
六. 排序算法总结
七. 索引堆
1.当遇到这样的问题:
数组的索引表示进程号, 数组的内容是进程优先级。
如果直接对优先级进行排序,会导致索引(即进程号)无法与优先级一一对应。
2.解决办法:
原本排好序以后arr[i] 就是按从小到大排序的,现在要求索引与值一一对应,可以中间加index层。
保持原来数组不变, 如果想得到排好序的数组,可以通过arr[index[i]]的方式获取, 而原来的arr[i]不做改变
index[0]则可以获取优先级最高的进程号
3.本节将这样的方法引入堆排序中, 即索引堆
代码实现
main.cpp
#include <iostream>
#include <cassert>
#include "SortTestHelper.h"
using namespace std;
// 最大索引堆
template<typename Item>
class IndexMaxHeap{
private:
Item *data; // 最大索引堆中的数据
int *indexes; // 最大索引堆中的索引
int count;
int capacity;
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
void shiftUp( int k ){
while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
swap( indexes[k/2] , indexes[k] );
k /= 2;
}
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
void shiftDown( int k ){
while( 2*k <= count ){
int j = 2*k;
if( j + 1 <= count && data[indexes[j+1]] > data[indexes[j]] )
j += 1;
if( data[indexes[k]] >= data[indexes[j]] )
break;
swap( indexes[k] , indexes[j] );
k = j;
}
}
public:
// 构造函数, 构造一个空的索引堆, 可容纳capacity个元素
IndexMaxHeap(int capacity){
data = new Item[capacity+1];
indexes = new int[capacity+1];
count = 0;
this->capacity = capacity;
}
~IndexMaxHeap(){
delete[] data;
delete[] indexes;
}
// 返回索引堆中的元素个数
int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
bool isEmpty(){
return count == 0;
}
// 向最大索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
// 传入的i对用户而言,是从0索引的
void insert(int i, Item item){
assert( count + 1 <= capacity );
assert( i + 1 >= 1 && i + 1 <= capacity );
i += 1;
data[i] = item;
indexes[count+1] = i;
count++;
shiftUp(count);
}
// 从最大索引堆中取出堆顶元素, 即索引堆中所存储的最大数据
Item extractMax(){
assert( count > 0 );
Item ret = data[indexes[1]];
swap( indexes[1] , indexes[count] );
count--;
shiftDown(1);
return ret;
}
// 从最大索引堆中取出堆顶元素的索引
int extractMaxIndex(){
assert( count > 0 );
int ret = indexes[1] - 1;
swap( indexes[1] , indexes[count] );
count--;
shiftDown(1);
return ret;
}
// 获取最大索引堆中的堆顶元素
Item getMax(){
assert( count > 0 );
return data[indexes[1]];
}
// 获取最大索引堆中的堆顶元素的索引
int getMaxIndex(){
assert( count > 0 );
return indexes[1]-1;
}
// 获取最大索引堆中索引为i的元素
Item getItem( int i ){
assert( i + 1 >= 1 && i + 1 <= capacity );
return data[i+1];
}
// 将最大索引堆中索引为i的元素修改为newItem
void change( int i , Item newItem ){
i += 1;
data[i] = newItem;
// 找到indexes[j] = i, j表示data[i]在堆中的位置
// 之后shiftUp(j), 再shiftDown(j)
for( int j = 1 ; j <= count ; j ++ )
if( indexes[j] == i ){
shiftUp(j);
shiftDown(j);
return;
}
}
// 测试索引堆中的索引数组index
// 注意:这个测试在向堆中插入元素以后, 不进行extract操作有效
bool testIndexes(){
int *copyIndexes = new int[count+1];
for( int i = 0 ; i <= count ; i ++ )
copyIndexes[i] = indexes[i];
copyIndexes[0] = 0;
std::sort(copyIndexes, copyIndexes + count + 1);
// 在对索引堆中的索引进行排序后, 应该正好是1...count这count个索引
bool res = true;
for( int i = 1 ; i <= count ; i ++ )
if( copyIndexes[i-1] + 1 != copyIndexes[i] ){
res = false;
break;
}
delete[] copyIndexes;
if( !res ){
cout<<"Error!"<<endl;
return false;
}
return true;
}
};
// 使用最大索引堆进行堆排序, 来验证我们的最大索引堆的正确性
// 最大索引堆的主要作用不是用于排序, 我们在这里使用排序只是为了验证我们的最大索引堆实现的正确性
// 在后续的图论中, 无论是最小生成树算法, 还是最短路径算法, 我们都需要使用索引堆进行优化:)
template<typename T>
void heapSortUsingIndexMaxHeap(T arr[], int n){
IndexMaxHeap<T> indexMaxHeap = IndexMaxHeap<T>(n);
for( int i = 0 ; i < n ; i ++ )
indexMaxHeap.insert( i , arr[i] );
assert( indexMaxHeap.testIndexes() );
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = indexMaxHeap.extractMax();
}
int main() {
int n = 1000000;
int* arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Heap Sort Using Index-Max-Heap", heapSortUsingIndexMaxHeap, arr, n);
delete[] arr;
return 0;
}
得到索引堆排序运行时间
Heap Sort Using Index-Max-Heap : 0.761144 s
八. 索引堆的优化
之前索引堆的效率缺陷:
当进程id为6的进程修改了优先级, 索引堆需要进行维护。
这时候第一步就是在index中找到值为6的位置,目前情况只能遍历index, 使得index[i] == 6。
对应上一节的代码, 即change函数效率过低。
解决方法:
新增reverse数组, 存放每一个id在index中的位置,这样change函数中不需要进行遍历寻找了
代码编写(主要是增加维护reverse并且change函数不用在做遍历操作)
main.cpp
#include <iostream>
#include <cassert>
#include "SortTestHelper.h"
using namespace std;
// 最大索引堆
template<typename Item>
class IndexMaxHeap{
private:
Item *data; // 最大索引堆中的数据
int *indexes; // 最大索引堆中的索引, indexes[x] = i 表示索引i在x的位置
int *reverse; // 最大索引堆中的反向索引, reverse[i] = x 表示索引i在x的位置
int count;
int capacity;
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
void shiftUp( int k ){
while( k > 1 && data[indexes[k/2]] < data[indexes[k]] ){
swap( indexes[k/2] , indexes[k] );
reverse[indexes[k/2]] = k/2;
reverse[indexes[k]] = k;
k /= 2;
}
}
// 索引堆中, 数据之间的比较根据data的大小进行比较, 但实际操作的是索引
void shiftDown( int k ){
while( 2*k <= count ){
int j = 2*k;
if( j + 1 <= count && data[indexes[j+1]] > data[indexes[j]] )
j += 1;
if( data[indexes[k]] >= data[indexes[j]] )
break;
swap( indexes[k] , indexes[j] );
reverse[indexes[k]] = k;
reverse[indexes[j]] = j;
k = j;
}
}
public:
// 构造函数, 构造一个空的索引堆, 可容纳capacity个元素
IndexMaxHeap(int capacity){
data = new Item[capacity+1];
indexes = new int[capacity+1];
reverse = new int[capacity+1];
for( int i = 0 ; i <= capacity ; i ++ )
reverse[i] = 0;
count = 0;
this->capacity = capacity;
}
~IndexMaxHeap(){
delete[] data;
delete[] indexes;
delete[] reverse;
}
// 返回索引堆中的元素个数
int size(){
return count;
}
// 返回一个布尔值, 表示索引堆中是否为空
bool isEmpty(){
return count == 0;
}
// 向最大索引堆中插入一个新的元素, 新元素的索引为i, 元素为item
// 传入的i对用户而言,是从0索引的
void insert(int i, Item item){
assert( count + 1 <= capacity );
assert( i + 1 >= 1 && i + 1 <= capacity );
// 再插入一个新元素前,还需要保证索引i所在的位置是没有元素的。
assert( !contain(i) );
i += 1;
data[i] = item;
indexes[count+1] = i;
reverse[i] = count+1;
count++;
shiftUp(count);
}
// 从最大索引堆中取出堆顶元素, 即索引堆中所存储的最大数据
Item extractMax(){
assert( count > 0 );
Item ret = data[indexes[1]];
swap( indexes[1] , indexes[count] );
reverse[indexes[count]] = 0;
count--;
if(count){
reverse[indexes[1]] = 1;
shiftDown(1);
}
return ret;
}
// 从最大索引堆中取出堆顶元素的索引
int extractMaxIndex(){
assert( count > 0 );
int ret = indexes[1] - 1;
swap( indexes[1] , indexes[count] );
reverse[indexes[count]] = 0;
count--;
if(count) {
reverse[indexes[1]] = 1;
shiftDown(1);
}
return ret;
}
// 获取最大索引堆中的堆顶元素
Item getMax(){
assert( count > 0 );
return data[indexes[1]];
}
// 获取最大索引堆中的堆顶元素的索引
int getMaxIndex(){
assert( count > 0 );
return indexes[1]-1;
}
// 看索引i所在的位置是否存在元素
bool contain( int i ){
assert( i + 1 >= 1 && i + 1 <= capacity );
return reverse[i+1] != 0;
}
// 获取最大索引堆中索引为i的元素
Item getItem( int i ){
assert( contain(i) );
return data[i+1];
}
// 将最大索引堆中索引为i的元素修改为newItem
void change( int i , Item newItem ){
assert( contain(i) );
i += 1;
data[i] = newItem;
// 找到indexes[j] = i, j表示data[i]在堆中的位置
// 之后shiftUp(j), 再shiftDown(j)
// for( int j = 1 ; j <= count ; j ++ )
// if( indexes[j] == i ){
// shiftUp(j);
// shiftDown(j);
// return;
// }
// 有了 reverse 之后,
// 我们可以非常简单的通过reverse直接定位索引i在indexes中的位置
shiftUp( reverse[i] );
shiftDown( reverse[i] );
}
// 测试索引堆中的索引数组index和反向数组reverse
// 注意:这个测试在向堆中插入元素以后, 不进行extract操作有效
bool testIndexesAndReverseIndexes(){
int *copyIndexes = new int[count+1];
int *copyReverseIndexes = new int[count+1];
for( int i = 0 ; i <= count ; i ++ ){
copyIndexes[i] = indexes[i];
copyReverseIndexes[i] = reverse[i];
}
copyIndexes[0] = copyReverseIndexes[0] = 0;
std::sort(copyIndexes, copyIndexes + count + 1);
std::sort(copyReverseIndexes, copyReverseIndexes + count + 1);
// 在对索引堆中的索引和反向索引进行排序后,
// 两个数组都应该正好是1...count这count个索引
bool res = true;
for( int i = 1 ; i <= count ; i ++ )
if( copyIndexes[i-1] + 1 != copyIndexes[i] ||
copyReverseIndexes[i-1] + 1 != copyReverseIndexes[i] ){
res = false;
break;
}
delete[] copyIndexes;
delete[] copyReverseIndexes;
if( !res ){
cout<<"Error!"<<endl;
return false;
}
for( int i = 1 ; i <= count ; i ++ )
if( reverse[ indexes[i] ] != i ){
cout<<"Error 2"<<endl;
return false;
}
return true;
}
};
// 使用最大索引堆进行堆排序, 来验证我们的最大索引堆的正确性
// 最大索引堆的主要作用不是用于排序, 我们在这里使用排序只是为了验证我们的最大索引堆实现的正确性
// 在后续的图论中, 无论是最小生成树算法, 还是最短路径算法, 我们都需要使用索引堆进行优化:)
template<typename T>
void heapSortUsingIndexMaxHeap(T arr[], int n){
IndexMaxHeap<T> indexMaxHeap = IndexMaxHeap<T>(n);
for( int i = 0 ; i < n ; i ++ )
indexMaxHeap.insert( i , arr[i] );
assert( indexMaxHeap.testIndexesAndReverseIndexes() );
for( int i = n-1 ; i >= 0 ; i -- )
arr[i] = indexMaxHeap.extractMax();
}
int main() {
int n = 1000000;
int* arr = SortTestHelper::generateRandomArray(n, 0, n);
SortTestHelper::testSort("Heap Sort Using Index-Max-Heap", heapSortUsingIndexMaxHeap, arr, n);
delete[] arr;
return 0;
}
运行速度得到了提升,结果如下
Heap Sort Using Index-Max-Heap : 1.06712 s