二叉堆实现及时间复杂度分析

前言

二叉堆是一种特殊的堆,其特点是堆顶的元素为整个堆的最大值或最小值。因此,堆有两种实现形式,一种是最大堆,另一种是最小堆。其定义如下:
最大堆:父结点的键值总是大于或等于任何一个子节点的键值
最小堆:父结点的键值总是小于或等于任何一个子节点的键值
堆存放的底层数据结构,一般采用数组。堆有什么用呢,一般可以用来排序(堆排序),取前K个大值(或小值)的元素,抑或只关心数据的最大值或最小值的情况(启发式搜索算法中,频繁查询代价最小值的元素,如Astar),均可构造堆结构,进行快速存取。

时间复杂度

这里直接给出几个操作的时间复杂度,后面详细分析下
堆排序:nlog(n)
堆插入元素:log(n)
堆调整:log(n)
堆重建:O(n)

堆的基本操作

堆基本操作包括,删除堆顶元素,插入元素,堆重建。这里,均以最小堆为例,说明堆基本操作及代码。最大堆只需要将判断条件修改一下即可。

堆插入

堆插入,就是现在已经有建好的堆了,新来一个元素后,需要将新元素加到堆中。因为,前面已经有的元素,其顺序关系已经满足最小堆的定义。即父结点的键值总是小于或等于任何一个子节点的键值。要满足这个定义,可以先把新元素,放到堆的最后,然后与其父结点进行比较,当父节点大于新元素时,交换两者的位置,让新元素成为父节点。然后再次比较当前新位置的父节点,是否比新元素大。这也就是堆结构中的上浮操作,把较小的元素从最末的位置,慢慢交换上来,直到满足定义或者到达堆顶

// 已经建好堆的数组,插入新元素
void insert_heaq(vector<int>& data, int val)
{
    data.push_back(val);
    int child = data.size() - 1;
    int i = (child - 1) / 2;

    // 上浮,i 为父节点
    while(child > 0)
    {
        if(data[i] <= val)
        {
            // 父节点不大于自己,停止上浮
            break;
        }
        data[child] = data[i];
        
        child = i;
        i = (child - 1) >> 1;
    }
    data[child] = val;
}

解释一下:采用数组结构,从下标0开始存放元素,假设当前结点的下标为i,其元素值为data[i],对应的左子结点下标为left = (i * 2) + 1,右子结点下标为right = (i * 2) + 2,其父结点下标为p = (i-1) / 2。也就是data[3]的父节点为data[(3-1)/2],也就是data[1]。一般,还有种存放方式,就是从下标1开始存放,其左结点(i * 2)和右结点(i * 2) + 1。
代码解释:首先将新元素放到vector的末尾,然后循环比较自己的父结点是否比自己大,比自己大则交换。

删除堆顶元素

一般地,外部获取到堆顶元素后,会采取删除堆顶元素的操作。

int front_heaq(vector<int>& data)
{
	if(data.empty()) 
		return -1; // 定义-1为错误码
	return data[0];
}

删除堆顶元素肯定会破坏原堆的性质,因此需要对堆进行调整。一种做法是,先把堆顶元素与末尾元素交换,然后判断新的堆顶元素,是否满足定义,若不满足则与左右子结点比较,将左右子结点中,较小的元素交换到当前父结点位置,然后依次比较下去。这就是,堆的下沉操作。把较大的元素与子结点比较,下沉到堆尾

// 获取堆顶元素,并删除堆顶
int pop_heaq(vector<int>& data)
{
    if(data.empty())
    {
        return -1; // 定义-1为错误码
    }
    
    int i = 0,left,right,min_index,min_val;
    int res = data[0];  // 作为返回元素
    int val = data.back(); // 堆末元素
    data.pop_back();

    int n = data.size() - 1; //最末位置下标
    int len = ((n-1) / 2)+ 1; // 最末非叶子结点位置

    // 数据从0开始存放
    left = ((i  << 1) + 1);
    right = ((i << 1) + 2);

    // 元素下沉,i为父节点
    while(i < len && left <= n)
    {
        min_index = left;
        min_val = data[left];
        if(right <= n && data[right] < min_val)
        {
            // 保留最小子节点
            min_val = data[right];
            min_index = right;
        }
        // 与最小节点比较
        if(min_val < val)
        {
            // 把小的值挪到父节点
            data[i] = min_val;
            
            // 更新父节点及子节点
            i = min_index;
            left = ((i << 1) + 1);
            right = ((i << 1) + 2);
            continue;
        }
        break;
    }
    data[i] = val;
    return res;
}

堆重建

有两种方法,一种是,直接遍历数组每一个元素,对每一个新元素调用insert_heaq函数进行插入,这种方法的时间复杂度是nlog(n)。另外一种是原地调整数组元素,使其满足定义。具体的方法是:
从最末一个非叶子结点开始,由下到上,依次比较当前结点与其左右结点的大小,如子结点值小于当前值,则交换。交换之后,要检查交换后的新位置是否满足,其值小于或等于它的子节点,迭代检查。
更加详细的展示和分析可以参考这个链接:zabery-堆排序及分析

调堆的过程应该从最后一个非叶子节点开始,假设有数组A = {1, 3, 4, 5, 7, 2, 6, 8, 0}。那么调堆的过程如下图,数组下标从0开始,A[3] = 5开始。分别与左孩子和右孩子比较大小,如果A[3]最大,则不用调整,否则和孩子中的值最大的一个交换位置,在图1中是A[7] > A[3] > A[8],所以A[3]与A[7]对换,从图1.1转到图1.2。
***调整示意图

给出代码如下:

// 调整当前index所在的堆,比较index的子节点,并递归调整
static void adjust_heaq(vector<int>& data,int index)
{
    int min_index,min_val,left,right;
    int n = data.size() - 1;  //最末位置下标
    int len = ((n-1) / 2)+ 1; // 最末非叶子结点位置

    left = (index << 1) + 1; // 左结点
    right = (index << 1) + 2; // 右结点

    // 和下沉一样的逻辑
    while(index < len && left <= n)
    {
        min_val = data[left];
        min_index = left;
        if(right <= n && data[right] < min_val)
        {
            // 保留最小子节点
            min_val = data[right];
            min_index = right;
        }
        if(min_val < data[index])
        {
            // 交换元素
            data[min_index] = data[index];
            data[index] = min_val;

            // 更新父节点,一直往下判断
            index = min_index;
            left = (index << 1) + 1;
            right = (index << 1) + 2;
            continue;
        }
        break;
    }
}
// 原地建堆,时间复杂度o(n)
void build_heaq(vector<int>& data)
{
    if(data.empty())
    {
        return;
    }
    // 从下往上交换:最小的非叶子节点开始
    int n = data.size() - 1;
    int i = (n - 1) / 2;
    for(; i >= 0; i--)
    {
        adjust_heaq(data,i);
    }
}

简单测试下,代码c++,不过c也是一样的写法。

// 主函数入口,测试函数
int main()
{
    vector<int> data = {1,3,9,4,5,8,2,0,6};
    build_heaq(data);
    return 0;
}

经过排列后的data顺序为:

0 1 2 3 5 8 9 4 6 

堆结构树

时间复杂度分析

对堆重建的时间复杂度分析如下:
假设堆的高度为h,根节点高位为0,则每一个高度的元素个数计算方式为 2 i ( i = 0 , 1 , . . . , h ) 2^i(i = 0,1,...,h) 2i(i=0,1,...,h)。因此,位于高度 i i i的某个元素需要遍历的次数为 ( h − i ) (h-i) (hi)。整个堆的全部元素遍历次数为:

S = ∑ i ( h − i ) ∗ 2 i ( i = 0 , 1 , 2 , . . . , h ) S =\sum_i(h-i)*2^i(i=0,1,2,...,h) S=i(hi)2i(i=0,1,2,...,h)

也就是堆顶元素需要遍历 ( h − 0 ) ∗ 2 0 (h-0)*2^0 (h0)20,第一个元素需要遍历 ( h − 1 ) ∗ 2 1 (h-1)*2^1 (h1)21。可以看到, S S S为等差数列与等比数列的乘积求和,因此可以采用错位相减的方法求得 S S S

2 S = ∑ i ( h − i ) ∗ 2 i + 1 ( i = 0 , 1 , 2 , . . . , h ) 2S=\sum_i(h-i)*2^{i+1}(i=0,1,2,...,h) 2S=i(hi)2i+1(i=0,1,2,...,h)
2 S − S = S = h + 2 1 + 2 2 + . . . + 2 h = 2 h + 1 − 2 − h 2S-S=S=h+2^1+2^2+...+2^h=2^{h+1}-2-h 2SS=S=h+21+22+...+2h=2h+12h

一般地, h h h可以近似为 l o g ( n ) log(n) log(n),且最末的叶子节点不会遍历,用 h − 1 h-1 h1代替 h h h,则

S = 2 l o g ( n ) − l o g ( n ) − 1 = n − l o g ( n ) − 1 S=2^{log(n)}-log(n)-1=n-log(n)-1 S=2log(n)log(n)1=nlog(n)1

因此,时间复杂度为 O ( n ) O(n) O(n)

结束语

以上就是实现及时间复杂度分析的全部内容,欢迎留言讨论。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值