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

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

1. 构建二叉堆

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

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

首先是最大堆MaxHeap类的设计


MaxHeap.png

这里给出代码

template 
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 
void heapSort1(T arr[], TINT32 num)
{
    MaxHeap max_heap = MaxHeap(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 
void heapSort2(T arr[], TINT32 num)
{
    MaxHeap max_heap = MaxHeap(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 
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 
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 
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);
    }
};

你可能感兴趣的:([算法学习] 排序算法(三)——堆排序)