[算法学习] 排序算法(三)——堆排序

这里继续整理常见的排序算法.
本文介绍堆排序
type right by Thomas Alan 光风霁月023 .XDU

1. 构建二叉堆

二叉堆实现的重点是对堆这个类的设计以及元素上移和下移操作.
便于插入新元素和取出最大元素.
上移: 比较当前节点和父节点的大小决定是否移动
下移: 比较当前节点和子节点的大小决定是否移动

同时这里提供两种初始化二叉堆的方法
i) 初始化再插入元素: 时间复杂度为O(nlogn)
ii) 通过传入数组来自我构造堆: 时间复杂度为O(n)
注: 这里二叉堆的索引从位置1开始

首先是最大堆MaxHeap类的设计


MaxHeap.png

这里给出代码

template <typename Item>
class MaxHeap
{
private:
    Item*       m_data      = nullptr;
    TINT32      m_count     = 0;
    TINT32      m_capacity  = 0;

private:
    void shiftUp(TINT32 k)
    {
        // 判断索引位置的元素和父节点的大小关系, 不符则换位
        while (k > 1 && m_data[k / 2] < m_data[k])
        {
            swap(m_data[k / 2], m_data[k]);
            k /= 2;
        }
    }

    void shiftDown(TINT32 k)
    {
        // 确保这个节点有孩子(左孩子索引 <= 元素数量)
        while (2 * k <= m_count)
        {
            TINT32 idj = 2 * k;

            // 右孩子存在且右孩子比左孩子大
            if (idj + 1 <= this->m_count && m_data[idj + 1] > m_data[idj])
            {
                idj += 1;
            }

            // 它本身比左右孩子大, 不做变动
            if (m_data[k] >= m_data[idj])
            {
                break;
            }

            // 交换它和孩子的位置
            swap(m_data[k], m_data[idj]);
            k = idj;
        }
    }

public:
    MaxHeap(TINT32 capacity)
    {
        this->m_data = new Item[capacity + 1];
        this->m_capacity = capacity + 1;
    }

    MaxHeap(Item arr[], TINT32 num)
    {
        // 分配空间
        this->m_data = new Item[num + 1];
        this->m_capacity = num + 1;

        // 赋值
        for (TINT32 idx = 0; idx < num; idx++)
        {
            this->m_data[idx + 1] = arr[idx];
        }

        this->m_count = num;

        // 从最后一个非叶子节点开始向前遍历, 进行shiftDown操作
        for (TINT32 idx = this->m_count / 2; idx >= 1; idx--)
        {
            shiftDown(idx);
        }
    }

    ~MaxHeap()
    {
        delete[] this->m_data;
        this->m_data = nullptr;
    }

    TINT32 size()
    {
        return this->m_count;
    }

    bool isEmpty()
    {
        return 0 == this->m_count;
    }

    void insert(Item item)
    {
        assert(this->m_count + 1 <= this->m_capacity); // 理论上, 插入新的元素如果超过了堆的容量需要申请新的空间, 暂时不做处理直接assert

        m_data[this->m_count + 1] = item;
        this->m_count++;
        this->shiftUp(this->m_count);
    }

    Item extractMax()
    {
        assert(this->m_count > 0);
        Item ret = m_data[1];

        swap(m_data[1], m_data[this->m_count]); // 交换首尾元素
        this->m_count--;
        shiftDown(1); // 将换来的第一个元素下移

        return ret;
    }
};

这里给出程序入口

// 构造函数1
template <typename T>
void heapSort1(T arr[], TINT32 num)
{
    MaxHeap<T> max_heap = MaxHeap<T>(num);
    for (TINT32 idx = 0; idx < num; idx++)
    {
        max_heap.insert(arr[idx]);
    }

    for (TINT32 idx = num - 1; idx >= 0; idx--)
    {
        arr[idx] = max_heap.extractMax();
    }
}

// 构造函数2
template <typename T>
void heapSort2(T arr[], TINT32 num)
{
    MaxHeap<T> max_heap = MaxHeap<T>(arr, num);
    for (TINT32 idx = num - 1; idx >= 0; idx--)
    {
        arr[idx] = max_heap.extractMax();
    }
}

2. 原地堆排序

为了将一个数组升序排列, 我们可以把一个数组做成一个最大堆, 然后将数组最大的元素和末尾元素进行交换, 再将数组末尾之前的部分重新组成最大堆(对交换来的元素进行 shift down 操作), 如此循环.
此时为了方便维护, 二叉堆的索引从0开始.
索引idx节点的父节点索引为: (idx - 1) / 2,
左孩子索引为: 2 * i + 1,
右孩子索引为: 2 * i + 2.

下面是程序实现

// 1. shift down 操作
template <typename T>
void __shiftDown(T arr[], TINT32 num, TINT32 k)
{
    while (2 * k + 1 < num)
    {
        TINT32 idj = 2 * k + 1;
        if (idj + 1 < num && arr[idj + 1] > arr[idj])
        {
            idj += 1;
        }

        if (arr[k] >= arr[idj])
        {
            break;
        }

        swap(arr[k], arr[idj]);
        k = idj;
    }
}

// 2. 入口
template <typename T>
void heapSort(T arr[], TINT32 num)
{
    // heapify, 构建堆
    for (TINT32 idx = (num - 1) / 2; idx >= 0; idx--)
    {
        __shiftDown(arr, num, idx);
    }

    for (TINT32 idx = num - 1; idx > 0; idx--)
    {
        swap(arr[0], arr[idx]);
        __shiftDown(arr, idx, 0);
    }
}

3. 索引堆

如果堆元素是一个比较复杂的结构, 那么按照1.中的堆结构改变元素位置就变得比较复杂.
来回交换复杂结构的元素, 会带来很大的不必要的消耗.
因此我们可以引入索引堆的概念, 构造存储元素的索引的数组m_index_list, 将堆中的元素以及元素对应索引分开存储, 对元素进行位置改变时, 实际上只改变元素所对应的索引, 就可以容易地改变元素位置.
引入索引堆的概念后, 我们可以轻易实现两件事情:
i) 取出某一索引对应的元素 Item getItem(TINT32 idx)
ii) 将某一索引的元素替换为新的元素 void change(TINT32 idx, Item new_item). 需要在索引数组m_index_list中遍历找到需要替换的idx的元素在堆中真正的索引idj.
注: 这里提到的索引是m_data的存储索引idx, 元素在堆中实际的位置idj对用户来说是透明的

同时, 在进行操作 ii) 时, 需要遍历m_index_list数组, 因此我们可以再引入一个反向查找数组m_rev_index_list, 存储的是元素在堆中的实际位置, 这样就可以快速找到idj, idj = m_rev_index_list[idx]. 这个数组在每次元素变化时进行维护. 将其初始化为0数组.

下面是索引堆的设计, 在二叉堆的基础上, 新增的内容加粗表示


IndexMaxHeap.png

下面是代码实现

template <typename Item>
class IndexMaxHeap
{
private:
    Item* m_data;
    TINT32* m_index_list;
    TINT32* m_rev_index_list;

    TINT32 m_count = 0;
    TINT32 m_capacity = 0;

private:
    void shiftUp(TINT32 k)
    {
        while (k > 1 && m_data[this->m_index_list[k / 2]] < m_data[this->m_index_list[k]])
        {
            swap(this->m_index_list[k / 2], this->m_index_list[k]);

            // 维护反向查找数组
            this->m_rev_index_list[this->m_index_list[k / 2]] = k / 2;
            this->m_rev_index_list[this->m_index_list[k]] = k;
            k /= 2;
        }
    }

    void shiftDown(TINT32 k)
    {
        while (2 * k <= m_count)
        {
            TINT32 idj = 2 * k;
            if (idj + 1 <= this->m_count && m_data[this->m_index_list[idj + 1]] > this->m_index_list[m_data[idj]])
            {
                idj += 1;
            }

            if (m_data[this->m_index_list[k]] >= m_data[this->m_index_list[idj]])
            {
                break;
            }

            swap(this->m_index_list[k], this->m_index_list[idj]);

            // 维护反向查找数组
            this->m_rev_index_list[this->m_index_list[k]] = k;
            this->m_rev_index_list[this->m_index_list[idj]] = idj;
            k = idj;
        }
    }

public:
    IndexMaxHeap(TINT32 capacity)
    {
        this->m_data = new Item[capacity + 1];
        this->m_index_list = new TINT32[capacity + 1];
        this->m_rev_index_list = new TINT32[capacity + 1];
        for (TINT32 idx = 0; idx <= capacity; idx++)
        {
            this->m_rev_index_list[idx] = 0;
        }

        this->m_count = 0;
        this->m_capacity = capacity;
    }

    ~IndexMaxHeap()
    {
        delete[] this->m_data;

        // 新增的需要释放的数组
        delete[] this->m_index_list;
        delete[] this->m_rev_index_list;
    }

    TINT32 size()
    {
        return this->m_count;
    }

    bool isEmpty()
    {
        return 0 == this->m_count;
    }

    // 插入元素的同时需要指定元素的索引
    void insert(TINT32 idx, Item item)
    {
        assert(this->m_count + 1 <= this->m_capacity);

        // 对索引是否越界的检测, 维护
        assert(idx + 1 >= 1 && idx + 1 <= this->m_capacity);
        idx += 1;

        // 数据列idx是item
        this->m_data[idx] = item;
        // 在index_list的末尾是添加了新的索引idx
        this->m_index_list[this->m_count + 1] = idx;

        // 维护反向查找数组
        this->m_rev_index_list[idx] = this->m_count + 1;

        this->m_count++;
        this->shiftUp(this->m_count);
    }

    Item extractMax()
    {
        assert(this->m_count > 0);

        Item ret = m_data[this->m_index_list[1]];

        swap(this->m_index_list[1], this->m_index_list[this->m_count]);

        // 维护反向查找数组
        this->m_rev_index_list[this->m_index_list[1]] = 1;
        this->m_rev_index_list[this->m_index_list[this->m_count]] = 0;

        this->m_count--;
        shiftDown(1);

        return ret;
    }

    TINT32 extractMaxIndex()
    {
        assert(this->m_count > 0);

        TINT32 ret = this->m_index_list[1] - 1;

        swap(this->m_index_list[1], this->m_index_list[this->m_count]);
        this->m_rev_index_list[this->m_index_list[1]] = 1;
        this->m_rev_index_list[this->m_index_list[this->m_count]] = 0;

        this->m_count--;
        shiftDown(1);

        return ret;
    }

    Item getItem(TINT32 idx)
    {
        return this->m_data[idx + 1];
    }

    // 判断当前的索引堆是否包含idx对应索引的元素
    bool contain(TINT32 idx)
    {
        assert(idx + 1 >= 0 && idx + 1 <= this->m_capacity);
        return 0 != this->m_rev_index_list[idx];
    }

    void change(TINT32 idx, Item new_item)
    {
        // 判断当前的索引堆是否包含idx对应索引的元素
        assert(contain(idx));
        idx += 1;
        this->m_data[idx] = new_item;

        // 接下来需要对堆进行维护, 因此需要在索引数组m_index_list中找到索引idx所在位置记为idj
        // idj即数据m_data[idx]在堆中实际的位置, 之后进行shiftUp, shiftDown

        // 遍历m_index_list找到idj(不引入反向查找数组)
        // for (TINT32 idj = 1; idj <= this->m_count; idj++)
        // {
        //     if (this->m_index_list[idj] == idx)
        //     {
        //         shiftUp(idj);
        //         shiftDown(idj);
        //         return;
        //     }
        // }

        TINT32 idj = this->m_rev_index_list[idx];
        shiftUp(idj);
        shiftDown(idj);
    }
};
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278

推荐阅读更多精彩内容