mooc_03_排序 - 堆和堆排序.md

代码地址:https://github.com/AlbinZhang/Mooc_DataAlgorithm/tree/master/04_heapSort

1. 二叉堆

要说堆排序,首先要说下数据结构中的二叉堆,有最大堆和最小堆。

二叉堆的定义:
    二叉堆是完全二叉树或者是近似完全二叉树。

二叉堆满足二个特性:
    1.父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
    2.每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。

MaxHeap_01

二叉堆一般用数组来表示, 例如上图的例子,红字标识数组下标,空出下标为0的, 
根节点的下标是1,l两个子节点就是2,3
第n个位置的子节点分别是2n,2n+1,   父节点是n/2取整

上图的数组形式:
    -, 100. 34, 22, 25, 31, 19, 17, 20, 11

实现一个二叉堆(最大堆)

要求:
    最大的元素在顶端(最大的元素在顶端)
    每个元素都比它的父节点小,或者和父节点相等
主要逻辑:
    插入:
        先把带插入的点放到数组的最后,然后和他的父节点比较,谁大谁在上面
    弹出:
        弹出顶端元素,把最后一个元素放到顶端,然后与两个子节点中大的比较,谁大谁在上面
#include <iostream>
#include <stdio.h>
#include <time.h>
#include <assert.h>

using namespace std;

template <typename T>
class MaxHeap
{
public:
    MaxHeap(int capacity){
        m_capacity = capacity;
        m_data = new T[capacity + 1];
        m_next = 1;
    }

    ~MaxHeap(){ delete m_data; }

    void insert(T node)
    {
        assert(m_next <= m_capacity);

        int index = m_next;
        m_data[index] = node;

        shiftUp(index);
        m_next++;
    }

    T pop(){
        assert(m_next > 1);
        int index = 1;
        T ret = m_data[index];
        m_data[index] = m_data[--m_next];
        shiftDown(index);
        return ret;
    }

    bool empty(){
        return m_next == 1;
    }
private:

    void shiftUp(int index){
        while (index > 1 && m_data[index] > m_data[index / 2]){
            swap(m_data[index], m_data[index / 2]);
            index = index / 2;
        }
    }

    void shiftDown(int index)
    {
        while (index * 2 < m_next)
        {
            int maxIndex = index * 2;
            int rightNode = index * 2 + 1;
            if (rightNode < m_next && m_data[maxIndex] < m_data[rightNode]){
                maxIndex = rightNode;
            }
            if (m_data[index] >= m_data[maxIndex])
                break;
            swap(m_data[index], m_data[maxIndex]);

            index = maxIndex;
        }

    }
private:
    int m_capacity;
    int m_next;     //只想下一个可以存储的数组下标

    T   *m_data;
};

int main(int argc, char *argv[])
{
    int arr[100];

    srand(time(NULL));
    for (int i = 0; i < 10; i++){
        //maxheap.push(rand() % 100);
        arr[i] = rand() % 100;
    }
    MaxHeap<int> maxheap(arr, 10);

    while (!maxheap.empty()){
        cout << maxheap.pop() << endl;
    }

    return 0;
}
shiftUp:
    是检查当前index元素是否比他的父节点大,如果大,就交换,然后继续和父节点比
shiftDown:
    用来检查当前节点是否比他的两个子节点大,如果比子节点小,就和自己节点中最大的交换,然后继续跟子节点比较

2. 堆排序

因为二叉堆(最大堆)根节点是最大的,所以每次弹出根节点,都是当前堆中最大的,
那我们就使用二叉堆进行堆排序
template <typename T>
void heapSort1(T arr[], int n)
{
    MaxHeap<T> maxheap = MaxHeap<T>(n);
    int i = 0;
    for (i = 0; i < n; i++){
        maxheap.insert(i);
    }

    for (i = n - 1; i >= 0; i--)
    {
        arr[i] = maxheap.pop();
    }
}

//
number = 1000000
MergeSort       :0.279398s
QuitSort3Way    :0.269952s
HeapSort1       :0.455481s

Heapify

如果直接使用一个数组生成二叉堆,可以使用另一个方法,看下图,
所有的叶节点(红色的)都可以看做是要给一个完整的二叉堆,
那节点4,就可以看作是这个新的二叉堆的根节点,使用shiftdown,重新使得这个二叉堆变成一个最大堆
然后再是节点3,2,1 之后 整个堆就是一个最大堆了

那么怎么找到最后一个非叶节点的呢? 
    在根节点是数组下标1的情况下,这个数组的长度为n,那么最后后一个非叶子节点是n/2
通过这个方法我们重新创建一个MaxHeap的构造函数

MaxHeap_2

MaxHeap(T arr[], int n)
{
    m_capacity = n;
    m_data = new T[n + 1];
    for (int i = 0; i < n; i++)
        m_data[i + 1] = arr[i];
    m_next = n+1;
    for (int i = (m_next - 1) / 2; i >= 1; i--)
        shiftDown(i);
}

//新的heapSort
void heapSort2(T arr[], int n)
{
    MaxHeap<T> maxheap = MaxHeap<T>(arr, n);
    int i = 0;
    for (i = n - 1; i >= 0; i--)
    {
        arr[i] = maxheap.pop();
    }
}

////////  效果还是很明显的
number = 1000000
MergeSort       :0.264709s
QuitSort3Way    :0.257416s
HeapSort1       :0.447973s
HeapSort2       :0.364310s

3. 优化堆排序

在上面介绍的堆排序中,都是先把元素拷贝到MaxHeap中,然后进行排序,最后再pop出来,重新添加到数组中
这要就需要额外的开辟缓存,元素的拷贝,但是通过之前最MaxHeap的理解,完全可以不用这样

我们现在已有一个数组,我们可以直接把这个数组通过MaxHeap(T arr[], int n)这里的方法使其成为一个最大堆

然后堆顶的元素就是当前数组中最大的元素了, 把第一个和最后一个交换,这样最大值就已经放到末尾了

然后再把前n-1个元素重新变成一个最大堆,再放到倒数第二的位置   以此类推

HeapSort_1

之前我们都是以下标1开始的,当根结点是0的时候,各个点之间的关系如下

heaoSort_2


template <typename T>
void __shiftDown(T arr[], int n, int index)
{
    while (index * 2+1 < n)
    { //如果还有子节点
        int leftIndex = 2 * index + 1;
        int rightIndex = 2 * index + 2;
        if (rightIndex < n && arr[leftIndex] < arr[rightIndex]) {
            leftIndex = rightIndex;
        }
        if (arr[index] >= arr[leftIndex]) {
            break;
        }
        swap(arr[index], arr[leftIndex]);
        index = leftIndex;
    }
}

template <typename T>
void heapSort3(T arr[], int n)
{
    for(int i = (n-1)/2; i >=0; i--){
        __shiftDown(arr, n, i);
    }
    for(int i = n-1; i > 0; i--){
        swap(arr[0], arr[i]);
        __shiftDown(arr, i, 0);
    }
}

//--------------------
number = 1000000
MergeSort   :0.350852s
QuitSort3Way  :0.249016s
HeapSort1   :0.428990s
HeapSort2   :0.323061s
HeapSort3   :0.295751s

根据排序的结果可以看到,在完全随机的情况下,原地堆排序是略优于其他堆排序的

4. 索引堆

在上面的最大堆实现中,我们把数组中的元素的位置都进行了交换,但是在实际工作中,原数组的顺序可能也是一项重要的信息,值得保存下来
所以为了保留元素组的顺序信息,我们不直接对原数组进行最大堆的计算,我们可以通过额外维护一个数组列表,
用来保存原数组经过最大堆计算后的下标的顺序,用来实现最大堆,这个就是索引堆

为了符合用户的使用习惯,索引数组是从下标0开始使用的
//从下标0开始的最大堆
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H

#include <cassert>
#include <string.h>
#include <stdio.h>

using namespace std;

template <typename T>
class IndexMaxHeap
{
  public:
    IndexMaxHeap(int capacity)
    {
        m_capacity = capacity;
        m_data = new T[capacity];
        m_next = 0;
    }

    ~IndexMaxHeap()
    {
        delete m_data;
    }

    void insert(T node)
    {
        assert(m_next < m_capacity);

        m_data[m_next] = node;

        shiftUp(m_next);
        m_next++;
    }

    T extractMaxItem()
    {
        assert(m_next > 0);
        int index = 0;
        T ret = m_data[index];
        m_data[index] = m_data[m_next];
        m_next--;
        shiftDown(index);
        return ret;
    }

    bool empty()
    {
        return m_next == 0;
    }

  private:
  /*
    跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
    n 的父节点 。(n-1)/2
  */
    void shiftUp(int index)
    {
        while(index > 0 && m_data[index] > m_data[(index-1)/2]) {
            swap(m_data[index], m_data[(index-1)/2]);
            index = (index-1)/2;
        }
    }

    /*
        跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
        n   left_node = 2*n+1
            right_node = 2*n+2
    */
    void shiftDown(int index)
    {
        while ((index*2 +1) < m_next) { //确保有子节点
            int lhs = index * 2 + 1;
            if((lhs+1) < m_next && m_data[lhs] < m_data[lhs+1]) {
                lhs = lhs+1;
            }
            if(m_data[index] >= m_data[lhs]) {
                break;
            }
            swap(m_data[index], m_data[lhs]);
            index = lhs;
        }
    }

  private:
    int m_capacity;
    int m_next; //指向下一个可以存储的数组下标

    T *m_data;
};

#endif //IndexMAXHEAP_H
然后我们再次基础上,增加一个 m_indexes数组,用于存储原数组排序后的索引,
同时插入的时候,用户可以指定插入的位置,所以原来的insert要增加一个参数int i来指定插入的位置

因为现在我们通过m_indexes存储实际数组的下标,所以shiftUp和shiftDown进行比较数值大小时,也需要通过m_indexes获取下标
但是在我们进行shiftUp,shiftDown交换操作时,我们不能直接交换m_data中的值,而是m_indexes中的索引,不然我们要他何用

并且我们增加了3个函数
    extractMaxIndex    获取最大值的索引
    getItem           通过索引获取原数组的元素
    change            指定更改一个数组中的元素,并且进行最大堆的维护,使m_indexes还是一个最大堆索引
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H

#include <cassert>
#include <string.h>
#include <stdio.h>

using namespace std;

template <typename T>
class IndexMaxHeap
{
  public:
    IndexMaxHeap(int capacity)
    : m_capacity(capacity), m_next(0)
    {
        m_data = new T[capacity];
        m_indexes = new int[capacity];
    }

    ~IndexMaxHeap()
    {
        delete m_data;
        delete m_indexes;
    }

    void insert(int i, T node)
    {
        assert(m_next < m_capacity);
        assert(i < m_capacity);

        m_data[i] = node;
        m_indexes[m_next] = i;

        shiftUp(m_next);
        m_next++;   //因为shiftUp是向上比较,所以m_next++,在前或在后都可以
    }

    T extractMaxItem()
    {
        assert(m_next > 0);

        int index = 0;
        T ret = m_data[m_indexes[index]];
        m_indexes[index] = m_indexes[m_next];
        m_next--;
        shiftDown(index);   //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
        return ret;
    }

    int extractMaxIndex()
    {
        assert(m_next > 0);

        int index = 0;
        int ret = m_indexes[index];
        m_indexes[index] = m_indexes[m_next];
        m_next--;
        shiftDown(index);   //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
        return ret;
    }

    T getItem(int i)
    {
        return m_data[i];
    }

    /*
        修改一个已经存在的数组元素
    */
    void change(int i, T newItem){
        assert(i < m_next);
        m_data[i] = newItem;

        //此时我们要找到i,这个元素在m_indexes中的位置
        //之后在shiftUp shifiDown,看当前元素能否上下移动
        for(int j = 0; j < m_next; j++){
            if(indexes[j] == i){
                shiftUp(j);
                shiftDown(j);
                return ;
            }
        }
    }


    bool empty()
    {
        return m_next == 0;
    }

  private:
  /*
    跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
    n 的父节点 。(n-1)/2
  */
    void shiftUp(int index)
    {
        while(index > 0 && m_data[m_indexes[index] > m_data[m_indexes[(index-1)/2]]) {
            swap(m_indexes[index], m_indexes[(index-1)/2]);
            index = (index-1)/2;
        }
    }

    /*
        跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
        n   left_node = 2*n+1
            right_node = 2*n+2
    */
    void shiftDown(int index)
    {
        while ((index*2 +1) < m_next) { //确保有子节点
            int lhs = index * 2 + 1;
            if((lhs+1) < m_next && m_data[m_indexes[lhs]] < m_data[m_indexes[lhs+1]]) {
                lhs = lhs+1;
            }
            if(m_data[m_indexes[index]] >= m_data[m_indexes[lhs]]) {
                break;
            }
            swap(m_indexes[index], m_indexes[lhs]);
            index = lhs;
        }
    }

  private:
    int m_capacity;
    int m_next; //指向下一个可以存储的数组下标

    T *m_data;
    int *m_indexes;
};

#endif //IndexMAXHEAP_H

优化change方法

change说了,是指定更改一个数组中的元素,并且进行最大堆的维护,使m_indexes还是一个最大堆索引

为了维护最大堆,我们需要找到元素索引在m_indexes中的位置,之前我们使用的是循环遍历的方法,
因为循环是n,shiftUp,shiftDown的复杂度是log(n),这样时间复杂度就变成了n+log(n),也就是O(n)

但是我们的堆这种结构,之前的插入和删除复杂度都是log(n)级别的,现在change是O(n),整个把时间复杂度拉低了,所以我们需要进行优化

这里我们的优化方式是通过反向查找的方式,来提高change的效率

简单来说就是在创建一个数组,使用这个数组m_reverse来维护,m_indexes中,m_data的元素在什么位置
至此我们一共有了三个数组结构,分别是m_data,m_indexes,m_reverse
他们分别的作用是:
    m_data: 按照用户指定下标的方式,存储用户的数据
    m_indexes: 为了不破环用户指定的下标,额外建立一个数组用于存储,m_data最大堆运算之后的 各个元素的位置,使用下标的方式存储
    m_reverse:为了方便通过m_data下标,直接找到m_indexes中当前下标的位置,所以使用此数组来存储当前数组下标在m_indexes中的位置
                例:
                m_data[j] 经过在最大堆中的位置是k,那么
                    m_indexes[k] = j;
                    m_reverse[m_indexes[j]] = k;
#ifndef IndexMAXHEAP_H
#define IndexMAXHEAP_H

#include <cassert>
#include <string.h>
#include <stdio.h>

using namespace std;

template <typename T>
class IndexMaxHeap
{
  public:
    IndexMaxHeap(int capacity)
    : m_capacity(capacity), m_next(0) {
        m_data = new T[capacity];
        m_indexes = new int[capacity];
        m_reverse = new int[capacity];
        memset(m_reverse, 0, sizeof(int)*capacity);
    }

    ~IndexMaxHeap() {
        delete m_data;
        delete m_indexes;
        delete m_reverse;
    }

    void insert(int i, T node) {
        assert(m_next < m_capacity);
        assert(i < m_capacity);

        m_data[i] = node;
        m_indexes[m_next] = i;
        m_reverse[i] = m_next;

        shiftUp(m_next);
        m_next++;   //因为shiftUp是向上比较,所以m_next++,在前或在后都可以
    }

    T extractMaxItem() {
        assert(m_next > 0);

        int index = 0;
        T ret = m_data[m_indexes[index]];
        m_indexes[index] = m_indexes[m_next];
        m_reverse[m_indexes[index]] = index;
        m_reverse[m_indexes[m_next]] = 0;
        m_next--;
        shiftDown(index);   //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
        return ret;
    }

    int extractMaxIndex() {
        assert(m_next > 0);

        int index = 0;
        int ret = m_indexes[index];
        m_indexes[index] = m_indexes[m_next];
        m_reverse[m_indexes[index]] = index;
        m_reverse[m_indexes[m_next]] = 0;
        m_next--;
        shiftDown(index);   //shiftDown是向下比较,所以边界就需要正确,所以m_next--要在之前,
        return ret;
    }

    bool contain(int i) {
        assert (i >= 0 && i< m_capacity);
        return m_reverse[i] == 0;
    }

    T getItem(int i) {
        assert(contain(i));
        return m_data[i];
    }

    /*
        修改一个已经存在的数组元素
    */
    void change(int i, T newItem) {
        assert(contain(i));

        m_data[i] = newItem;

        //此时我们要找到i,这个元素在m_indexes中的位置
        //之后在shiftUp shifiDown,看当前元素能否上下移动
        /*
        for(int j = 0; j < m_next; j++){
            if(indexes[j] == i){
                shiftUp(j);
                shiftDown(j);
                return ;
            }
        }
        */

        int j = m_reverse[i];
        shiftUp(j);
        shiftDown(j);
    }

    bool empty() {
        return m_next == 0;
    }

  private:
  /*
    跟父节点比较,如果大于父节点,交换,然后继续跟父节点比较
    n 的父节点 。(n-1)/2
  */
    void shiftUp(int index) {
        while(index > 0 && m_data[m_indexes[index] > m_data[m_indexes[(index-1)/2]]) {
            swap(m_indexes[index], m_indexes[(index-1)/2]);
            m_reverse[m_indexes[index]] = index;
            m_reverse[m_indexes[(index-1)/2]] = (index-1)/2;
            index = (index-1)/2;
        }
    }

    /*
        跟子节点比较,如果小于 子节点中的最大值,就交换,然后继续跟子节点比较
        n   left_node = 2*n+1
            right_node = 2*n+2
    */
    void shiftDown(int index) {
        while ((index*2 +1) < m_next) { //确保有子节点
            int lhs = index * 2 + 1;
            if((lhs+1) < m_next && m_data[m_indexes[lhs]] < m_data[m_indexes[lhs+1]]) {
                lhs = lhs+1;
            }
            if(m_data[m_indexes[index]] >= m_data[m_indexes[lhs]]) {
                break;
            }
            swap(m_indexes[index], m_indexes[lhs]);
            m_reverse[m_indexes[index]] = index;
            m_reverse[m_indexes[lhs]] = lhs;
            index = lhs;
        }
    }

  private:
    int m_capacity;
    int m_next; //指向下一个可以存储的数组下标

    T *m_data;
    int *m_indexes;
    int *m_reverse;
};

#endif //IndexMAXHEAP_H
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值