C 數據結構堆

引言 - 數據結構堆node

  堆結構都多數人很耳熟, 從堆排序優先級隊列, 咱們總會看見它的身影. 相關的資料太多了,git

- https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8Dgithub

無數的漂亮圖片連續不斷, 但目前沒搜到一個工程中能夠舒服用的代碼庫. 本文由此痛點而來. 寫算法

一篇奇妙數據結構堆的工程代碼. 追求愣頭青的手熱 ->---設計模式

對於 heap 接口思考, 我是這樣設計數組

#ifndef _H_HEAP
#define _H_HEAP

//
// cmp_f - 比較行爲 > 0 or = 0  or < 0
// : int add_cmp(const void * now, const void * node)
//
typedef int (* cmp_f)();

//
// node_f - 銷燬行爲
// : void list_die(void * node)
//
typedef void (* node_f)(void * node);

//
// head_t 堆的類型結構
//
typedef struct heap * heap_t;

//
// heap_create - 建立符合規則的堆
// fcmp     : 比較行爲, 規則 fcmp() <= 0
// return   : 返回建立好的堆對象
//
extern heap_t heap_create(cmp_f fcmp);

//
// heap_delete - 銷燬堆
// h        : 堆對象
// fdie     : 銷燬行爲, 默認 NULL
// return   : void
//
extern void heap_delete(heap_t h, node_f fdie);

//
// heap_insert - 堆插入數據
// h        : 堆對象
// node     : 操做對象
// return   : void
//
extern void heap_insert(heap_t h, void * node);

//
// heap_remove - 堆刪除數據
// h        : 堆對象
// arg      : 操做參數
// fcmp     : 比較行爲, 規則 fcmp() == 0
// return   : 找到的堆節點
//
extern void * heap_remove(heap_t h, void * arg, cmp_f fcmp);

//
// heap_top - 查看堆頂結點數據
// h        : 堆對象
// return   : 堆頂節點
//
extern void * heap_top(heap_t h);

//
// heap_top - 摘掉堆頂結點數據
// h        : 堆對象
// return   : 返回堆頂節點
//
extern void * heap_pop(heap_t h);

#endif//_H_HEAP

heap_t 是不徹底類型實體指針, 其中 struct heap 是這樣設計緩存

#include "heap.h"
#include <stdlib.h>
#include <assert.h>

#define UINT_HEAP       (1<<5u)

struct heap {
    cmp_f   fcmp;       // 比較行爲
    unsigned len;       // heap 長度
    unsigned cap;       // heap 容量
    void ** data;       // 數據節點數組
};

// heap_expand - 添加節點擴容
inline void heap_expand(struct heap * h) {
    if (h->len >= h->cap) {
        h->data = realloc(h->data, h->cap<<=1);
        assert(h->data);
    }
}

從中能夠看出當前堆結構是能夠保存 void * 數據. 其中經過 heap::fcmp 比較行爲來調整關係.安全

有了堆的數據結構定義, 那麼堆的建立和銷燬業務代碼就被無腦的肯定了 ~數據結構

//
// heap_create - 建立符合規則的堆
// fcmp     : 比較行爲, 規則 fcmp() <= 0
// return   : 返回建立好的堆對象
//
inline heap_t 
heap_create(cmp_f fcmp) {
    struct heap * h = malloc(sizeof(struct heap));
    assert(h && fcmp);
    h->fcmp = fcmp;
    h->len = 0;
    h->cap = UINT_HEAP;
    h->data = malloc(sizeof(void *) * UINT_HEAP);
    assert(h->data && UINT_HEAP > 0);
    return h;
}

//
// heap_delete - 銷燬堆
// h        : 堆對象
// fdie     : 銷燬行爲, 默認 NULL
// return   : void
//
void 
heap_delete(heap_t h, node_f fdie) {
    if (NULL == h || h->data == NULL) return;
    if (fdie && h->len > 0)
        for (unsigned i = 0; i < h->len; ++i)
            fdie(h->data[i]);
    free(h->data);
    h->data = NULL;
    h->len = 0;
    free(h);
}

隨後將迎接這個終結者堆的全貌. 此刻讀者能夠先喝口水 : )app

 

前言 - 寫一段終結代碼

  堆結構中最核心兩處就是向下調整向上調整過程代碼

// down - 堆節點下沉, 從上到下沉一遍
static void down(cmp_f fcmp, void * data[], unsigned len, unsigned x) {
    void * m = data[x];
    for (unsigned i = x * 2 + 1; i < len; i = x * 2 + 1) {
        if (i + 1 < len && fcmp(data[i+1], data[i]) < 0)
            ++i;
        if (fcmp(m, data[i]) <= 0)
            break;
        data[x] = data[i];
        x = i;
    }
    data[x] = m;
}

// up - 堆節點上浮, 從下到上浮一遍
static void up(cmp_f fcmp, void * node, void * data[], unsigned x) {
    while (x > 0) {
        void * m = data[(x-1)>>1];
         if (fcmp(m, node) <= 0)
            break;
        data[x] = m;
        x = (x-1)>>1;
    }
    data[x] = node;
}

如何理解其中奧妙呢. 能夠這麼看, 索引 i 節點的左子樹索引爲 2i+1, 右子樹樹索引爲 2i+2 = (2i+1)+1.

相反的索引爲 i 節點的父親節點就是 (i-1)/2 = (i-1)>>1. 這就是堆節點調整的無上奧妙.  隨後的代碼就

很輕鬆出手了

//
// heap_insert - 堆插入數據
// h        : 堆對象
// node     : 操做對象
// return   : void
//
inline void 
heap_insert(heap_t h, void * node) {
    heap_expand(h);
    up(h->fcmp, node, h->data, h->len++);
}

//
// heap_top - 查看堆頂結點數據
// h        : 堆對象
// return   : 堆頂節點
//
inline void * 
heap_top(heap_t h) {
    return h->len <= 0 ? NULL : *h->data;
}

//
// heap_top - 摘掉堆頂結點數據
// h        : 堆對象
// return   : 返回堆頂節點
//
inline void * 
heap_pop(heap_t h) {
    void * node = heap_top(h);
    if (node && --h->len > 0) {
        // 尾巴節點必定比小堆頂節點大, 那麼要下沉
        h->data[0] = h->data[h->len];
        down(h->fcmp, h->data, h->len, 0);
    }
    return node;
}

看完上面代碼能夠再回看下 down 和 up 代碼佈局. 是否是堆節點調整所有技巧已經瞭然於胸 ~

隨後咱們介紹堆刪除任意節點大體算法思路

  1' 循環遍歷, 找到要刪除節點

  2' 若是刪除後堆空, 或者刪除的是最後節點, 那直接搞定

  3' 最後節點複製到待刪除節點位置處

  4' 最後節點和待刪除節點權值相等, 不調整節點關係

  5' 最後節點比待刪除節點權值大, 向下調整節點關係(基於小頂堆設計)

  6' 最後節點比待刪除節點權值小, 向上調整節點關係

從上能夠看出堆刪除節點算法複雜度是 O(n) + O(logn) = O(n). 請欣賞具體代碼

//
// heap_remove - 堆刪除數據
// h        : 堆對象
// arg      : 操做參數
// fcmp     : 比較行爲, 規則 fcmp() == 0
// return   : 找到的堆節點
//
void * 
heap_remove(heap_t h, void * arg, cmp_f fcmp) {
    if (h == NULL || h->len <= 0)
        return NULL;

    // 開始查找這個節點
    unsigned i = 0;
    fcmp = fcmp ? fcmp : h->fcmp;
    do {
        void * node = h->data[i];
        if (fcmp(arg, node) == 0) {
            if (--h->len > 0 && h->len != i) {
                // 尾巴節點和待刪除節點比較
                int ret = h->fcmp(h->data[h->len], node);

                // 小頂堆, 新的值比老的值小, 那麼上浮
                if (ret < 0)
                    up(h->fcmp, h->data[h->len], h->data, i);
                else if (ret > 0) {
                    // 小頂堆, 新的值比老的值大, 那麼下沉
                    h->data[i] = h->data[h->len];
                    down(h->fcmp, h->data, h->len, i);
                }
            }

            return node;
        }
    } while (++i < h->len);

    return NULL;
}

到這堆數據結構基本代碼都已經搞定. 開始寫寫測試用例跑跑

#include "heap.h"
#include <stdio.h>

struct node {
    int value;
};

static inline int node_cmp(const struct node * l, const struct node * r) {
    return l->value - r->value;
}

static void heap_print(heap_t h) {
    struct heap {
        cmp_f   fcmp;       // 比較行爲
        unsigned len;       // heap 長度
        unsigned cap;       // heap 容量
        void ** data;       // 數據節點數組
    } * x = (struct heap *)h;

    // 數據打印for (unsigned i = 0; i < x->len; ++i) {
        struct node * node = x->data[i];
        printf("%d ", node->value);
    }
    putchar('\n');
}

int main() {
    heap_t h = heap_create(node_cmp);
    struct node a[] = { { 53 }, { 17 }, { 78 }, { 9 }, { 45 }, { 65 }, { 87 }, { 23} };
    for (int i = 0; i < sizeof a / sizeof *a; ++i)
        heap_insert(h, a + i);

    heap_print(h);

    // 數據打印
    struct node * node;
    while ((node = heap_pop(h))) {
        printf("%d ", node->value);
    }
    putchar('\n');

    // 從新插入數據
    for (int i = 0; i < sizeof a / sizeof *a; ++i)
        heap_insert(h, a + i);

    // 刪除操做 - 下沉
    heap_remove(h, &(struct node){ 17 }, NULL);
    heap_print(h);

    // 插入操做
    heap_insert(h, &(struct node){ 17 });
    heap_print(h);

    // 刪除操做 - 上浮
    heap_remove(h, &(struct node){ 78 }, NULL);
    heap_print(h);

    heap_delete(h, NULL);
    return 0;
}

最終運行結果以下

 

後續堆相關代碼變化, 能夠參照  heap - https://github.com/wangzhione/structc/blob/master/structc/struct/heap.c

說到引用 github 想起一個 git 好用配置安利給你們 ~ 今後 git ll 就活了.

git config --global color.diff auto
git config --global color.status auto git config --global color.branch auto git config --global color.interactive auto git config --global alias.ll "log --graph --all --pretty=format:'%Cred%h %Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"

奇妙數據結構堆, 終結在這裏, 後面內容能夠忽略. 期待下次再扯了 ~

 

正文 - 順帶贈送個點心

  其實到這本不應再說什麼. 單純看上面就足夠了. 但不知道有沒有朋友以爲你老是說 C 數據結構. 效

果好嗎? 對技術提高效果明顯嗎? 這裏不妨利用咱們對 C 理解, 來分析一個業務代碼. 感覺下一通百通.

我試着用 Go 中數據結構源碼舉例子. 重點看下 Go 源碼包中 "container/list" 鏈表用法(比較簡單)

package main

import (
	"container/list"
	"fmt"
)

func main() {
	// 構造鏈表對象
	pers := list.New()

	// Persion 普通人對象
	type Persion struct {
		Name string
		Age  int
	}

	// 鏈表對象數據填充
	pers.PushBack(&Persion{"wang", 27})
	pers.PushFront(&Persion{"zhi", 27})

	// 開始遍歷處理
	for e := pers.Front(); e != nil; e = e.Next() {
		per, ok := e.Value.(*Persion)
		if !ok {
			panic(fmt.Sprint("Persion List faild", e.Value))
		}
		fmt.Println(per)
	}

	for e := pers.Front(); e != nil; {
		next := e.Next()
		pers.Remove(e)
		e = next
	}
	fmt.Println(pers.Len())
}

運行結果是

$ go run list-demo.go
&{zhi 27}
&{wang 27}
0

經過上面演示 Demo, 大體知道了 list 包用法. 隨後開始着手解析 "container/list" 源碼

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package list implements a doubly linked list.
//
// To iterate over a list (where l is a *List):
//	for e := l.Front(); e != nil; e = e.Next() {
//		// do something with e.Value
//	}
//
package list

// Element is an element of a linked list.
type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value interface{}
}

// Next returns the next list element or nil.
func (e *Element) Next() *Element {
	if p := e.next; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
	if p := e.prev; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
	root Element // sentinel list element, only &root, root.prev, and root.next are used
	len  int     // current list length excluding (this) sentinel element
}

// Init initializes or clears list l.
func (l *List) Init() *List {
	l.root.next = &l.root
	l.root.prev = &l.root
	l.len = 0
	return l
}

// New returns an initialized list.
func New() *List { return new(List).Init() }

// Len returns the number of elements of list l.
// The complexity is O(1).
func (l *List) Len() int { return l.len }

// Front returns the first element of list l or nil if the list is empty.
func (l *List) Front() *Element {
	if l.len == 0 {
		return nil
	}
	return l.root.next
}

// Back returns the last element of list l or nil if the list is empty.
func (l *List) Back() *Element {
	if l.len == 0 {
		return nil
	}
	return l.root.prev
}

// lazyInit lazily initializes a zero List value.
func (l *List) lazyInit() {
	if l.root.next == nil {
		l.Init()
	}
}

// insert inserts e after at, increments l.len, and returns e.
func (l *List) insert(e, at *Element) *Element {
	n := at.next
	at.next = e
	e.prev = at
	e.next = n
	n.prev = e
	e.list = l
	l.len++
	return e
}

// insertValue is a convenience wrapper for insert(&Element{Value: v}, at).
func (l *List) insertValue(v interface{}, at *Element) *Element {
	return l.insert(&Element{Value: v}, at)
}

// remove removes e from its list, decrements l.len, and returns e.
func (l *List) remove(e *Element) *Element {
	e.prev.next = e.next
	e.next.prev = e.prev
	e.next = nil // avoid memory leaks
	e.prev = nil // avoid memory leaks
	e.list = nil
	l.len--
	return e
}

// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List) Remove(e *Element) interface{} {
	if e.list == l {
		// if e.list == l, l must have been initialized when e was inserted
		// in l or l == nil (e is a zero Element) and l.remove will crash
		l.remove(e)
	}
	return e.Value
}

// PushFront inserts a new element e with value v at the front of list l and returns e.
func (l *List) PushFront(v interface{}) *Element {
	l.lazyInit()
	return l.insertValue(v, &l.root)
}

// PushBack inserts a new element e with value v at the back of list l and returns e.
func (l *List) PushBack(v interface{}) *Element {
	l.lazyInit()
	return l.insertValue(v, l.root.prev)
}

// InsertBefore inserts a new element e with value v immediately before mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertBefore(v interface{}, mark *Element) *Element {
	if mark.list != l {
		return nil
	}
	// see comment in List.Remove about initialization of l
	return l.insertValue(v, mark.prev)
}

// InsertAfter inserts a new element e with value v immediately after mark and returns e.
// If mark is not an element of l, the list is not modified.
// The mark must not be nil.
func (l *List) InsertAfter(v interface{}, mark *Element) *Element {
	if mark.list != l {
		return nil
	}
	// see comment in List.Remove about initialization of l
	return l.insertValue(v, mark)
}

// MoveToFront moves element e to the front of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToFront(e *Element) {
	if e.list != l || l.root.next == e {
		return
	}
	// see comment in List.Remove about initialization of l
	l.insert(l.remove(e), &l.root)
}

// MoveToBack moves element e to the back of list l.
// If e is not an element of l, the list is not modified.
// The element must not be nil.
func (l *List) MoveToBack(e *Element) {
	if e.list != l || l.root.prev == e {
		return
	}
	// see comment in List.Remove about initialization of l
	l.insert(l.remove(e), l.root.prev)
}

// MoveBefore moves element e to its new position before mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveBefore(e, mark *Element) {
	if e.list != l || e == mark || mark.list != l {
		return
	}
	l.insert(l.remove(e), mark.prev)
}

// MoveAfter moves element e to its new position after mark.
// If e or mark is not an element of l, or e == mark, the list is not modified.
// The element and mark must not be nil.
func (l *List) MoveAfter(e, mark *Element) {
	if e.list != l || e == mark || mark.list != l {
		return
	}
	l.insert(l.remove(e), mark)
}

// PushBackList inserts a copy of an other list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushBackList(other *List) {
	l.lazyInit()
	for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
		l.insertValue(e.Value, l.root.prev)
	}
}

// PushFrontList inserts a copy of an other list at the front of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushFrontList(other *List) {
	l.lazyInit()
	for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() {
		l.insertValue(e.Value, &l.root)
	}
}

list 包中最核心的數據結構無外乎 Element 和 List 互相引用的結構

// Element is an element of a linked list.
type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value interface{}
}

// Next returns the next list element or nil.
func (e *Element) Next() *Element {
	if p := e.next; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

// Prev returns the previous list element or nil.
func (e *Element) Prev() *Element {
	if p := e.prev; e.list != nil && p != &e.list.root {
		return p
	}
	return nil
}

// List represents a doubly linked list.
// The zero value for List is an empty list ready to use.
type List struct {
	root Element // sentinel list element, only &root, root.prev, and root.next are used
	len  int     // current list length excluding (this) sentinel element
}

它是一個特殊循環雙向鏈表. 特殊在 Element::list 指向頭節點. 

隨着咱們對 list 內存佈局理解後, 後面的業務代碼實現起來就很通常了. 例如這裏

// PushBackList inserts a copy of an other list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushBackList(other *List) {
	l.lazyInit()
	for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() {
		l.insertValue(e.Value, l.root.prev)
	}
}

其實能夠實現的更貼合 list 庫整體的風格, 性能還更好

// PushBackList inserts a copy of an other list at the back of list l.
// The lists l and other may be the same. They must not be nil.
func (l *List) PushBackList(other *List) {
	l.lazyInit()
	for e := other.Front(); e != nil; e = e.Next() {
		l.insertValue(e.Value, l.root.prev)
	}
}

是否是發現上層代碼理解起來心智負擔不大. 不過 go 中 slice list map 都不是線程安全的.

特殊場景須要自行加鎖. 這裏不過多扯. 之後有機會會詳細分析 Go 中鎖源碼實現. 最後經過

上面 list 包真實現一個 LRU Cache

package cache

import (
    "container/list"
    "sync"
)

// entry 存儲的實體
type entry struct {
    key, val interface{}
}

// Cache 緩存結構
type Cache struct {
    // m 保證 LRU Cache 訪問線程安全
    m sync.Mutex

    // max 標識緩存容量的最大值, 0 標識無限緩存
    max uint

    // list 是 entry 循環雙向鏈表
    list *list.List

    // pond entry 緩存池子 key -> entry
    pond map[interface{}]*list.Element
}

// New 構建 LRU Cache 緩存結構
func New(max uint) *Cache {
    return &Cache{
        max:  max,
        list: list.New(),
        pond: make(map[interface{}]*list.Element),
    }
}

// Set 設置緩存
func (c *Cache) Set(key, val interface{}) {
    c.m.Lock()
    defer c.m.Unlock()

    element, ok := c.pond[key]
    if ok {
        // set key nil 進入刪除邏輯
        if val == nil {
            delete(c.pond, key)
            c.list.Remove(element)
            return
        }

        // 從新設置 value 數據
        (element.Value.(*entry)).val = val
        // set key nil exists 進入 update 邏輯
        c.list.MoveToFront(element)
        return
    }

    if val == nil {
        return
    }

    // 首次添加
    c.pond[key] = c.list.PushFront(&entry{key, val})

    // 數據過多, 刪除尾巴數據
    if uint(c.list.Len()) > c.max && c.max != 0 {
        delete(c.pond, (c.list.Remove(c.list.Back()).(*entry)).key)
    }
}

// Get 獲取緩存
func (c *Cache) Get(key interface{}) (val interface{}, ok bool) {
    c.m.Lock()
    defer c.m.Unlock()

    if element, ok := c.pond[key]; ok {
        // 獲取指定緩存值
        val, ok = (element.Value.(*entry)).val, true
        // 調整緩存熱點
        c.list.MoveToFront(element)
    }
    return
}

用起來很容易

	c := cache.New(1)
	c.Set("123", "123")
	c.Set("234", "234")
	fmt.Println(c.Get("123"))
	fmt.Println(c.Get("234"))

是否是離開了 C, 整個世界也很簡單. 沒啥設計模式, 有的是性能還能夠, 也能用. 但願能幫到有心人 ~

也能夠看看 Go 標準庫中關於 LRU 局部源碼, 也有些參照意義 (ˇˍˇ) ~

package transport

import "container/list"

//
// $(GOPATH)/src/net/http/transport.go
//

type persistConn struct {
    // ...
}

type connLRU struct {
    ll *list.List // list.Element.Value type is of *persistConn
    m  map[*persistConn]*list.Element
}

// add adds pc to the head of the linked list.
func (cl *connLRU) add(pc *persistConn) {
    if cl.ll == nil {
        cl.ll = list.New()
        cl.m = make(map[*persistConn]*list.Element)
    }
    ele := cl.ll.PushFront(pc)
    if _, ok := cl.m[pc]; ok {
        panic("persistConn was already in LRU")
    }
    cl.m[pc] = ele
}

func (cl *connLRU) removeOldest() *persistConn {
    ele := cl.ll.Back()
    pc := ele.Value.(*persistConn)
    cl.ll.Remove(ele)
    delete(cl.m, pc)
    return pc
}

// remove removes pc frpm cl.
func (cl *connLRU) remove(pc *persistConn) {
    if ele, ok := cl.m[pc]; ok {
        cl.ll.Remove(ele)
        delete(cl.m, pc)
    }
}

// len returns the number of items in the cache.
func (cl *connLRU) len() int {
    return len(cl.m)
}

 

後記 - 那個打開的大門

你曾是少年 - https://music.163.com/#/song?id=426027293

每一個男人內心都有一塊淨土, 只不過生活所逼硬生生的, 藏在心底最深處 . ... ..

相關文章
相關標籤/搜索