C 資料結構堆
引言 - 資料結構堆
堆結構都很耳熟, 從堆排序到優先順序佇列, 我們總會看見它的身影. 相關的資料太多了,
堆 - https://zh.wikipedia.org/wiki/%E5%A0%86%E7%A9%8D
無數漂亮的圖片接二連三, 但目前沒搜到一個工程中可以舒服用的程式碼庫. 本文由此痛點而來.
寫一篇奇妙資料結構堆的終結程式碼. 耳熟終究比不過手熱 ->---
對於 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); }
隨後將迎接這個終結者堆的全貌. 此刻讀者可以先喝口水 : )
前言 - 寫一段終結程式碼
堆結構中最核心兩處就是向下調整和向上調整過程程式碼
// 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 interface{} value interface{} } // Cache LRU 快取實現 type Cache struct { // x 保證 LRU 訪問安全 m sync.Mutex // max 表示快取容量的最大值, 0 表示無限快取 max uint // list 迴圈雙向連結串列 list *list.List // pond 快取的池子 pond map[interface{}]*list.Element } // New 新建一個 LRU 快取物件 func New(max uint) *Cache { return &Cache{ max: max, list: list.New(), pond: make(map[interface{}]*list.Element), } } // remove 通過 *list.Element 刪除 func (c *Cache) remove(e *list.Element) { n, ok := c.list.Remove(e).(*entry) if ok { delete(c.pond, n.key) } } // Set 新增快取 func (c *Cache) Set(key, value interface{}) { c.m.Lock() defer c.m.Unlock() if e, ok := c.pond[key]; ok { if value == nil { // Set key nil <=> Remove key c.remove(e) } else { e.Value = value c.list.MoveToFront(e) } return } // 如果是首次新增 c.pond[key] = c.list.PushFront(&entry{key, value}) // 如果超出池子快取開始清理 if c.max != 0 && uint(c.list.Len()) > c.max { c.remove(c.list.Back()) } } // Get 獲取快取 func (c *Cache) Get(key interface{}) (interface{}, bool) { c.m.Lock() defer c.m.Unlock() if e, ok := c.pond[key]; ok { c.list.MoveToFront(e) return e.Value.(*entry).value, true } return nil, false }
用起來很容易
c := cache.New(1) c.Set("123", "123") c.Set("234", "234") fmt.Println(c.Get("123")) fmt.Println(c.Get("234"))
是不是離開了 C, 整個世界也很簡單. 沒啥設計模式, 有的是效能還可以, 也能用.
希望能幫到有心人 ~
後記 - 那個開啟的大門
你曾是少年 - https://music.163.com/#/song?id=426027293
每個男人心裡都有一塊淨土, 只不過生活所逼硬生生的, 藏在心底最深處 . ... ..