heap.h

上一篇写了写链表,这篇写下堆,这个结构接触的不多,所以正好学习一下libhv中的堆,这个堆的实现比较灵活,即可以是大顶堆也可以是小顶堆,通过比较函数是比大还是比小来区别,当然,如果没有比较函数,就成了无序的,感觉没啥意义,就不讨论无序的堆了。

堆的定义

堆是具有下列性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。

完全二叉树是啥就不解释了。。。。。 下面开始看代码

结构体定义

struct heap_node {
    struct heap_node* parent; //父节点
    struct heap_node* left;    //左孩子
    struct heap_node* right;    //右孩子
};

//用以作为比较的函数类型
typedef int (*heap_compare_fn)(const struct heap_node* lhs, const struct heap_node* rhs);
struct heap {
    struct heap_node* root;      //根节点
    int nelts;                    //节点个数
    // if compare is less_than, root is min of heap
    // if compare is larger_than, root is max of heap
    heap_compare_fn compare;     
};

初始化

static inline void heap_init(struct heap* heap, heap_compare_fn fn) {
    heap->root = NULL;  
    heap->nelts = 0;
    heap->compare = fn;  //设置比较函数,确定到底是大顶堆还是小顶堆
}

初始化后,该堆就是一个没有根的节点个数为0的空堆。

插入操作

static inline void heap_insert(struct heap* heap, struct heap_node* node) {
    // get last => insert node => sift up
    // 0: left, 1: right
    int path = 0;
    int n,d;
    ++heap->nelts;  //结点个数加1
    // traverse from bottom to up, get path of last node
    //这里是为了获取下一个加入位置在整个堆中的路径
    for (d = 0, n = heap->nelts; n >= 2; ++d, n>>=1) {
        path = (path << 1) | (n & 1);
    }

    // get last->parent by path
    struct heap_node* parent = heap->root;
    while(d > 1) {
        parent = (path & 1) ? parent->right : parent->left;
        --d;
        path >>= 1;
    }

    // insert node
    node->parent = parent;    //设置node的父节点
    if (parent == NULL) heap->root = node;  //如果父节点为NULL,说明新加入的节点是根结点
    else if (path & 1) parent->right = node; //新加入的节点是其父节点的右结点
    else parent->left = node;   //新加入的节点是其父节点的左节点

    // sift up
    //新加入结点后,因为要按照大顶堆或者小顶堆排序,所以要继续调整位置
    //假设是小顶堆,判断新加入的结点是否比其父节点小,如果比父结点小,需要交换新结点和父结点的位置;
    //并且在交换完成后,如果该结点比上一层节点还小,那么需要继续交换,直到它的父节点不存在或者
    //它的父节点比自己小
    if (heap->compare) {
        while (node->parent && heap->compare(node, node->parent)) {
            heap_swap(heap, node->parent, node);
        }
    }
}

最开始获取path是比较有趣的,这里的path功能就是下一个结点的路径,表示方法是1代表右结点,0代表左节点。举个例子:

假设我想新加一个结点G,那么下一个位置就是C的右孩子,在这个堆中G的路径就是  A  --> A的右孩子C --> C的右孩子G;而path就等于 11B(二进制),其中低位的1表示A-->C,高位的1代表C-->G。那么在看这个for循环:

    for (d = 0, n = heap->nelts; n >= 2; ++d, n>>=1) {
        path = (path << 1) | (n & 1);
    }

d代表层级,n表示判断第几个节点,第一次for循环判断最后一个节点G,然后向上查询,也就是说,第一个先查G是其父节点C的左孩子还是右孩子,方法是使用(n & 1),如果为1,表示右孩子,如果为0表示左孩子。其实就是看n是奇数还是偶数,是奇数就是右孩子,是偶数就是左孩子。因为第一次判断n==7是奇数,所以path=1B;第二次for循环判断C是其父节点的左孩子还是右孩子,因为C是第三个节点,是奇数,所以也是右孩子,path=11B;这样就可以做到如何从根节点找到G。

    // get last->parent by path
    struct heap_node* parent = heap->root;
    while(d > 1) {
        parent = (path & 1) ? parent->right : parent->left;
        --d;
        path >>= 1;
    }

知道了path的意思,上面这部分代码就很简单了,查找G的父节点,从heap->root根结点A开始遍历,查找d-1层,也就是查找到C为止。

最后比较麻烦的就是在将G加入堆后,要按照大顶堆或者小顶堆排序,假设该堆是小顶堆,如果G的值小于C,那么需要交换C和G的位置,交互完后,G的父结点就是A,需要再比较G和A的大小,如果G小于A,要继续交换G和A的位置,最后的结果为:

当然了,如果G比C大,就不需要交换位置。

最后看一下交换位置的函数

//交换parent和child的位置
static inline void heap_swap(struct heap* heap, struct heap_node* parent, struct heap_node* child) {
    assert(child->parent == parent && (parent->left == child || parent->right == child));
    //获取parent的父结点
    struct heap_node* pparent = parent->parent;
    //child结点的左孩子
    struct heap_node* lchild = child->left;
    //child结点的右孩子
    struct heap_node* rchild = child->right;
    struct heap_node* sibling = NULL;
    //如果parent的父结点为NULL,说明parent是根节点,所以child就成了新的根
    if (pparent == NULL) heap->root = child;
    //判断parent是其父节点的左孩子还是右孩子,child替代parent成了parent父节点的孩子
    else if (pparent->left == parent) pparent->left = child; 
    else if (pparent->right == parent) pparent->right = child;

    //parent以后成了child的孩子节点的父结点
    if (lchild) lchild->parent = parent;
    if (rchild) rchild->parent = parent;

    child->parent  = pparent;
    if (parent->left == child) {
        sibling = parent->right;
        child->left = parent;
        child->right = sibling;
    } else {
        sibling = parent->left;
        child->left = sibling;
        child->right = parent;
    }
    if (sibling) sibling->parent = child;

    parent->parent = child;
    parent->left   = lchild;
    parent->right  = rchild;
}

看似一堆的指针操作,很麻烦,实际上非常简单。以第一张图为例,假设我要交换C和G的位置,那么需要改变这么几点:修改A的孩子为G;修改C的父结点为G,并且C成为了G的孩子结点的新父结点;修改G的父节点为A,修改G的孩子节点为C和过去的兄弟节点,假如存在的话。

删除操作

static inline void heap_remove(struct heap* heap, struct heap_node* node) {
    if (heap->nelts == 0)   return;
    // get last => replace node with last => sift down and sift up
    // 0: left, 1: right
    int path = 0;
    int n,d;
    // traverse from bottom to up, get path of last node
    //获取最后一个结点的路径
    for (d = 0, n = heap->nelts; n >= 2; ++d, n>>=1) {
        path = (path << 1) | (n & 1);
    }
    --heap->nelts;

    // get last->parent by path
    //获取最后一个结点的父结点
    struct heap_node* parent = heap->root;
    while(d > 1) {
        parent = (path & 1) ? parent->right : parent->left;
        --d;
        path >>= 1;
    }

    // replace node with last
    //先获取最后一个结点
    struct heap_node* last = NULL;
    //父结点为NULL,也就是根节点为NULL,说明为空堆
    if (parent == NULL) {
        return;
    }
    //右结点和左结点,因为后面要把最后一个节点和node交换,而将父节点
    //的孩子赋值为NULL,相当于直接把交换后的node删除了
    else if (path & 1) {
        last = parent->right;
        parent->right = NULL;
    }
    else {
        last = parent->left;
        parent->left = NULL;
    }
    //last为NULL,说明只有一个根结点
    if (last == NULL) {
        if (heap->root == node) {
            heap->root = NULL;
        }
        return;
    }
    //把last替换到node原来的位置,node被删除
    heap_replace(heap, node, last);
    node->parent = node->left = node->right = NULL;

    //因为last替换到node原来的位置后,堆的顺序可能被打乱,
    //所以要重新排序
    if (!heap->compare) return;
    struct heap_node* v = last;
    struct heap_node* est = NULL;
    // sift down
    //last有可能比过去node的孩子结点大,那么需要将last向下移动
    while (1) {
        est = v;
        if (v->left) est = heap->compare(est, v->left) ? est : v->left;
        if (v->right) est = heap->compare(est, v->right) ? est : v->right;
        if (est == v) break;
        heap_swap(heap, v, est);
    }
    // sift up
    //last有可能比过去node的父结点小,那么需要将last向上移动
    while (v->parent && heap->compare(v, v->parent)) {
        heap_swap(heap, v->parent, v);
    }
}

删除操作的方法为先把要删除的结点与最后一个结点互换,然后删除该结点,再调整新堆的顺序。举个例子,假设我要把第一张图的B结点删除,交换完后是这样的:

因为B成了最后一个结点,直接把C的右孩子赋值为NULL,就相当于删除了B,对其他的结点不会产生影响。但是G的位置需要调整,因为G的值可能比D或者E大,那么就需要由D和E中较小的与G交换。当然了,在这个图中,A不可能比G大,所以不用交换A和G的位置。但是这是有可能发生的,如果删除的不是B而是D,G和D交换了之后,G有可能小于B,那么就需要交换G和B的位置。

heap_remove函数调用了一个新的函数heap_replace,实现了替换功能,实际上该函数比heap_swap更简单,因为替换就是交换的一半功能而已,源码不再注释了:

// replace s with r
static inline void heap_replace(struct heap* heap, struct heap_node* s, struct heap_node* r) {
    // s->parent->child, s->left->parent, s->right->parent
    if (s->parent == NULL) heap->root = r;
    else if (s->parent->left == s) s->parent->left = r;
    else if (s->parent->right == s) s->parent->right = r;

    if (s->left) s->left->parent = r;
    if (s->right) s->right->parent = r;
    if (r) {
        //*r = *s;
        r->parent = s->parent;
        r->left = s->left;
        r->right = s->right;
    }
}

最后一个函数是删除根

static inline void heap_dequeue(struct heap* heap) {
    heap_remove(heap, heap->root);
}

以上大体就是libhv中heap.h的全部功能,理解起来也并不复杂。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值