堆排序初步学习——用数组模拟堆

堆排序

堆分为大根堆、小根堆,其就是一棵完全二叉树。

一、二叉树

1.1 定义

一棵深度为k且有2k-1个结点的二叉树称为满二叉树。满二叉树每一层的结点个数都达到了最大值, 即满二叉树的第i层上有2i-1个结点 (i≥1) 。

如果对满二叉树的结点进行编号(从1开始), 约定编号从根结点起, 自上而下, 自左而右,则深度为k的, 有n个结点的二叉树, 当且仅当其每一个结点都与深度为k的满二叉树中编号从1至n的结点一一对应时, 称之为完全二叉树。

从满二叉树和完全二叉树的定义可以看出, 满二叉树是完全二叉树的特殊形态, 即如果一棵二叉树是满二叉树, 则它必定是完全二叉树。

完全二叉树除了最后一个分支外,每个分支都必有两个孩子,最后一个可能有一个或两个孩子。

1.2 性质

如果对一棵有n个结点的完全二叉树的结点按层序编号, 则对任一结点i (1≤i≤n) 有:(注:[ ]表示向下取整,舍弃)

  1. 如果i=1, 则结点i是二叉树的根, 无双亲;如果i>1, 则其双亲parent (i) 是结点[i/2]。
  2. 如果2i>n, 则结点i无左孩子, 否则其左孩子lchild (i) 是结点2i。
  3. 如果2i+1>n, 则结点i无右孩子, 否则其右孩子rchild (i) 是结点2i+1。

1.3 特点

完全二叉树的特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。

二、堆排序实现

用一个一维数组模拟完全二叉树,数的结点的编号对应数组的下标,因此,数组从下标1开始存储。

2.1 小根堆

小根堆的性质:每个子节点的值都要大于它的父结点的值, 但左右孩子大小之间没限制。因此,小根堆中,父结点一定小于它的孩子结点,根据递归实现,根节点一定是一棵完全二叉树中最小的值。

如何手写一个堆?

首先需要两个函数:down(x)up(x)

down函数表示将结点编号为x的结点向下调整;up函数表示将结点编号为x的结点向上调整。

在这两个函数的基础上可以实现以下功能:

  1. 插入一个数:heap[++sz]=x; up(sz); 尾部插入
  2. 求集合中的最小值:heap[1]
  3. 删除最小值:heap[1]=heap[sz]; sz– – ; down(1); 尾部代替头部
  4. 删除任一个结点为k的元素:heap[k]=heap[sz]; sz– – ; down(k); up(k);
  5. 修改任一个结点为k的元素:heap[k]=x; down(k); up(k);
删除最小值

【AcWing 838. 堆排序】

输入一个长度为n的整数数列,从小到大输出前m小的数。

#include 

using namespace std;

const int N=1e5+10;
int heap[N],sz;//用一维数组heap模拟完全二叉树,sz表示用到的最大结点编号,即树上结点个数

void down(int x)
{
    //结点为x的 左孩子 2x   右孩子 2x+1
    //小根堆:需要调整时满足条件:父结点大于子节点
    int t=x;
    if (x*2<=sz && heap[t]>heap[x*2])   t=x*2;//假设先和左孩子交换
    if (x*2+1<=sz && heap[t]>heap[x*2+1]) t=x*2+1; //判断是否还需要和右孩子交换
    if (x!=t){
        swap(heap[t],heap[x]);
        down(t);
    }
}

void up(int x)
{
    //结点为x(左孩子:偶数,右孩子:奇数)的父结点 :  x/2
    //向上调整时,不必管兄弟节点,只用判断父结点
    while (x/2 && heap[x]<heap[x/2]) {
        swap(heap[x],heap[x/2]);
        x/=2;
    }
}

int main() {
    int n,m;
    cin>>n>>m;
    
    // 初始化小根堆
    for (int i = 1; i <= n; ++i)  cin>>heap[i]; //从下标1开始存储
    sz=n;
    for (int i = n/2; i >= 1; --i)  down(i);
    //最后一个叶子结点编号为n,其父结点为n/2,所以最后一个非叶子结点编号为n/2,从这儿开始向下调整,时间复杂度O(n)

    //输出前m小的数,每输出一个数,调整一次小根堆
    for (int i = 0; i < m; ++i) { //循环m次即可
        cout<<heap[1]<<" ";
        //删除最小的数
        heap[1]=heap[sz];
        sz--;
        down(1);
    }

    return 0;
}
删除、修改结点为k的结点
1. 删除、修改下标为k的结点
void del_k(int k)
{
    heap[k]=heap[sz--];
    down(k);
    up(k);
}

void rep_k(int k,int x)
{
    heap[k]=x;
    down(k);
    up(k);
}
2. 删除、修改第k次插入的结点

【AcWing 839. 模拟堆】

维护一个集合,初始时集合为空,支持如下几种操作:

  1. “I x”,插入一个数x;
  2. “PM”,输出当前集合中的最小值;
  3. “DM”,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. “D k”,删除第k个插入的数;
  5. “C k x”,修改第k个插入的数,将其变为x;
#include 

using namespace std;

const int N=1e5+10;
int heap[N],sz;//sz为0时,代表树为null
int getp[N],getb[N],num; //num记录第几次插入

void swap_heap(int i,int j) //交换的结点的下标i j
{
    swap(heap[i],heap[j]);
    int ki=getb[i],kj=getb[j];
    swap(getp[ki],getp[kj]);
    swap(getb[i],getb[j]);
}

void down(int x)
{//向下调整,看左右孩子
    int t=x;
    if (x*2<=sz && heap[t]>heap[x*2]) t=x*2;
    if (x*2+1<=sz && heap[t]>heap[x*2+1]) t=x*2+1;
    if (x!=t) {
        swap_heap(x,t);
        down(t);
    }
}

void up(int x)
{//向上调整,只看父结点
    while (x/2 && heap[x]<heap[x/2]){
        swap_heap(x/2,x);
        x/=2;
    }
}

void insert(int x)
{
    getp[++num]=++sz;
    getb[sz]=num;
    heap[sz]=x;
    up(sz);
}

void del_i(int i)
{   //删除下标为i结点,等价于和最后一个结点出互换并调整该结点
    swap_heap(i,sz--);   //删除根节点 i=1 也算在内
    down(i);
    up(i);
}

void rep_i(int i,int x)
{  //替换下标为i结点的值为x
    heap[i]=x;
    down(i);
    up(i);
}

int main() 
{
    int n;
    cin>>n;
    string op;
    int k,x;
    while (n--){
        cin>>op;
        if (op=="I") {
            cin>>x;   insert(x);
        } else if (op=="PM") {
            cout<<heap[1]<<endl;
        } else if (op=="DM") {
            del_i(1);
        } else if (op=="D") {
            cin>>k;  k=getp[k];
            del_i(k);
        } else if (op=="C"){
            cin>>k>>x; k=getp[k];
            rep_i(k,x);
        }
    }
    return 0;
}

你可能感兴趣的:(算法设计与分析入门,二叉树,数据结构,算法,堆排序,链表)