前言
最經想改寫C用的配置讀取接口, 准備采用hash或二叉樹提到原先用的鏈表,提高查找效率.
就回顧一下二叉樹,這裡分享一下二叉查找樹,代碼很精簡的, 適合學習運用二叉樹查找.
需要基礎
1.具備C基礎知識
2.知道數據結構,最好知道一點二叉樹結構
能夠學到
1.穩固二叉查找樹
2.C良好編碼格式習慣
3.tree 數據結構幾種流行套路(設計)
參照
1.二叉查找樹簡單分析 http://www.cppblog.com/cxiaojia/archive/2012/08/09/186752.html
(上面那個博文, 圖形講解的恨透,但是那種tree數據結構,不要參照)
正文
1 直接奔主題 說二叉查找樹難點
1.1 簡單說一下二叉查找樹原理和突破
二叉樹也是個經典的數據結構,但是工作中用的場景不多,但是我們常用過,例如map,自帶排序的k-v結構.
二叉樹相比雙向鏈表在改變了插入和刪除方式,使查找代價變小.因而適用領域在快速查找的領域.對於那種快速刪除,
快速插入的領域並不適合.
我們今天主要回顧的是二叉查找(搜索)樹. 首先看看數據結構如下
/* * 這裡簡單的溫故一下 , 二叉查找樹 *一切從簡單的來吧 */ typedef int node_t; typedef struct tree { node_t v; //這裡簡單測試一下吧,從簡單做起 struct tree* lc; struct tree* rc; } *tree_t;
上面比較簡陋,不是很通用,方便了解原理設計,最後會帶大家設計一些通用的二叉樹結構. 這裡扯一點,
結構會影響算法,算法依賴特定的結構.蛋和雞的問題,先有一個不好的蛋,孵化一個不好的雞,後來雞下了很多蛋,其中一個蛋很好,
孵化了一個很好的雞,最後蛋雞良好循環出現了.
對於二叉查找樹,除了刪除比較復雜一點,其它的還是很大眾的代碼,這裡從如何找到一個結點的父節點出發.看下面代碼
/* * 查找這個結點的父結點 * root : 頭結點 * v : 查找的結點 * : 返回這個v值的父親結點,找不見返回NULL,可以返回孩子結點 */ tree_t tree_parent(tree_t root, node_t v, tree_t* pn) { tree_t p = NULL; while (root) { if (root->v == v) break; p = root; if (root->v > v) root = root->lc; else root = root->rc; } if (pn) //返回它孩子結點 *pn = root; return p; }
本質思路是,構建一個指針p保存上一個結點.這個函數相比其它函數 tree_parent 這裡多返回當前的孩子結點.一個函數順帶做了兩件事.
這是一個突破.推薦學習,同等代價做的事多了,價值也就提升了.
下面說一下 二叉查找樹 刪除原理(從上面參照文中截得,這個比較詳細,但是代碼寫的水)
代碼實現如下,有點精簡,多看幾遍,或者調試幾遍理解更容易寫.
/* * 刪除結點 * proot : 指向頭結點的結點 * v : 待刪除的值 */ void tree_delete(tree_t* proot, node_t v) { tree_t root, n, p, t;//n表示v結點,p表示父親結點 if ((!proot) || !(root = *proot)) return; //這裡就找見 v結點 n和它的父親結點p p = tree_parent(root, v, &n); if (!n) //第零情況 沒有找見這個結點直接返回 return; //第一種情況,刪除葉子結點,直接刪除就可以此時t=NULL; 第二情況 只有一個葉子結點 if (!n->lc || !n->rc) { if (!(t = n->lc)) //找見當前結點n的唯一孩子結點 t = n->rc; if (!p) *proot = NULL; else { if (p->lc == n) //讓當前結點的父親收養這個它唯一的孩子 p->lc = t; else p->rc = t; } //刪除當前結點並返回,C要是支持 return void; 語法就好了 free(n); return; } //第三種情況, 刪除的結點有兩個孩子 //將當前結點 右子樹中最小值替代為它,繼承王位,它沒有左兒子 for (t = n->rc; t->lc; t = t->lc) ; n->v = t->v;//用nr替代n了,高效,並讓n指向找到t的唯一右子樹, tree_delete(&n->rc, t->v);//遞歸刪除n右子樹中最小值, 從t開始,很高效 }
第一步找見這個結點和它父親結點,沒找見它直接返回,父親結點為了重新配置繼承關系.
對於 要刪除 葉子結點或只有孩子的結點, 刪除 走 if(!n->lc || !n->rc) 分支不同是t
當只為葉子結點 t = NULL, 當有一個孩子結點, t = 後繼結點,將其二和一了,是一個突破.
最後 刪除 有兩個孩子的結點, 我們的做法,將 它 右子樹中最小值找到,讓其替代自己, 後面在右子樹中刪除 那個結點.
1.2 簡單擴展一下 遞歸的潛規則
遞歸大多數流程如下
//數據打印函數,全部輸出,不會打印回車,中序遞歸 void tree_print(tree_t root) { if (root) { //簡單中序找到最左結點,打印 tree_print(root->lc); printf("%d ", root->v); tree_print(root->rc); } }
這樣的遞歸的方式 是
tree_print_0 => tree_print_1 => tree_print_2 => tree_print_3 => tree_print_2 => tree_print_1 => tree_print_0
先入函數棧後出函數棧,遞歸深度太長會爆棧.上面就是大多數遞歸的方式.
遞歸中有一種特殊的尾遞歸.不需要依賴遞歸返回結果.一般遞歸代碼在函數最尾端.例如上 刪除代碼,結構如下
tree_delete_0 => tree_delete_0 => tree_delete_1 => tree_delete_1 => tree_delete_2 => tree_delete_2 => tree_delete_3 =>
這裡代碼就是入棧出棧,跳轉到新的遞歸中.屬於編譯器關於遞歸的優化,不依賴遞歸返回的結果,最後一行,一般都優化為尾遞歸很安全.
入不同行開發,潛規則還是比較多的.扯一點, 一天晚上出租車回來和司機瞎扯淡, 他說有一天帶一個導演,那個導演打電話給一個女孩父親,
告訴他,他女兒今天晚上來他房間,痛斥一頓讓她走了,後面就聯系女孩父親,女孩父親神回復,導演你該潛你就潛. 估計當時那個導演心裡就有
一萬個草泥馬奔過,怎麼就有這麼一對活寶父女.
人生活寶多才歡樂,快樂才會笑著帶著'class'.
1.3 說一下接口和測試代碼
一般良好安全的編程喜歡是,先寫接口,再寫總的測試代碼,後面代碼接口打樁挨個測試. 這裡總的接口和測試代碼如下
/* * 在二叉查找樹中插入結點 * proot : 頭結點的指針 * v : 待插入變量值,會自動分配內存 */ void tree_insert(tree_t* proot, node_t v); //數據打印函數,全部輸出,不會打印回車,中序遞歸 void tree_print(tree_t root); /* * 在這個二叉查找樹中查找 值為v的結點,找不見返回NULL * root : 頭結點 * v : 查找結點值 * : 返回值為查找到的結點,找不見返回NULL */ tree_t tree_search(tree_t root, node_t v); /* * 查找這個結點的父結點 * root : 頭結點 * v : 查找的結點 * : 返回這個v值的父親結點,找不見返回NULL,可以返回孩子結點 */ tree_t tree_parent(tree_t root, node_t v, tree_t* pn); /* * 刪除結點 * proot : 指向頭結點的結點 * v : 待刪除的值 */ void tree_delete(tree_t* proot, node_t v); /* * 刪除這個二叉查找樹,並把根結點置空 * proot : 指向根結點的指針 */ void tree_destroy(tree_t* proot); //簡單輸出幫助宏 #define TREE_PRINT(root) \ puts("當前二叉查找樹的中序數據如下:"), tree_print(root), putchar('\n') //簡單的主函數邏輯 int main(int argc, char* argv[]) { tree_t root = NULL; //先創建一個二叉樹 試試 node_t a[] = { 8,4,5,11,2,3,7,-1,9,0,1,13,12,10 }; //中間臨時變量 tree_t tmp; node_t n; int i = -1; //插入數據 while (++i<sizeof(a) / sizeof(*a)) tree_insert(&root, a[i]); //簡單輸出數據 TREE_PRINT(root); //這裡查找數據,刪除數據打印數據 n = 5; tmp = tree_search(root, n); if (tmp == NULL) printf("root is no find %d!\n", n); else printf("root is find %d, is %p,%d!\n", n, tmp, tmp->v); //查找父親結點 n = 12; tmp = tree_parent(root, n, NULL); if (tmp == NULL) printf("root is no find %d!\n", n); else printf("root is find parent %d, is %p,%d!\n", n, tmp, tmp->v); //刪除測試 n = 8; tree_delete(&root, n); TREE_PRINT(root); n = 9; tree_delete(&root, n); TREE_PRINT(root); //釋放資源 tree_destroy(&root); system("pause"); return 0; }
測試代碼就是把聲明的接口挨個測試一遍.對於代碼打樁意思就是簡單的實現接口,讓其能編譯通過.如下
/* * 在這個二叉查找樹中查找 值為v的結點,找不見返回NULL * root : 頭結點 * v : 查找結點值 * : 返回值為查找到的結點,找不見返回NULL */ tree_t tree_search(tree_t root, node_t v) { return NULL; }
就是打樁. 到這裡基本都萬事具備了.設計思路有了,原理也明白了,下面上一個完整案例看結果.
2.匯總代碼, 看運行結果
首先看運行結果截圖
查找,刪除,打印都來了一遍, 具體的實現代碼如下
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> //控制台打印錯誤信息, fmt必須是雙引號括起來的宏 #ifndef CERR #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno), ##__VA_ARGS__) //檢測並退出的宏 #define CERR_EXIT(fmt, ...) \ CERR(fmt, ##__VA_ARGS__), exit(EXIT_FAILURE) #endif/* !CERR */ /* * 這裡簡單的溫故一下 , 二叉查找樹 *一切從簡單的來吧 */ typedef int node_t; typedef struct tree { node_t v; //這裡簡單測試一下吧,從簡單做起 struct tree* lc; struct tree* rc; } *tree_t; /* * 在二叉查找樹中插入結點 * proot : 頭結點的指針 * v : 待插入變量值,會自動分配內存 */ void tree_insert(tree_t* proot, node_t v); //數據打印函數,全部輸出,不會打印回車,中序遞歸 void tree_print(tree_t root); /* * 在這個二叉查找樹中查找 值為v的結點,找不見返回NULL * root : 頭結點 * v : 查找結點值 * : 返回值為查找到的結點,找不見返回NULL */ tree_t tree_search(tree_t root, node_t v); /* * 查找這個結點的父結點 * root : 頭結點 * v : 查找的結點 * : 返回這個v值的父親結點,找不見返回NULL,可以返回孩子結點 */ tree_t tree_parent(tree_t root, node_t v, tree_t* pn); /* * 刪除結點 * proot : 指向頭結點的結點 * v : 待刪除的值 */ void tree_delete(tree_t* proot, node_t v); /* * 刪除這個二叉查找樹,並把根結點置空 * proot : 指向根結點的指針 */ void tree_destroy(tree_t* proot); //簡單輸出幫助宏 #define TREE_PRINT(root) \ puts("當前二叉查找樹的中序數據如下:"), tree_print(root), putchar('\n') //簡單的主函數邏輯 int main(int argc, char* argv[]) { tree_t root = NULL; //先創建一個二叉樹 試試 node_t a[] = { 8,4,5,11,2,3,7,-1,9,0,1,13,12,10 }; //中間臨時變量 tree_t tmp; node_t n; int i = -1; //插入數據 while (++i<sizeof(a) / sizeof(*a)) tree_insert(&root, a[i]); //簡單輸出數據 TREE_PRINT(root); //這裡查找數據,刪除數據打印數據 n = 5; tmp = tree_search(root, n); if (tmp == NULL) printf("root is no find %d!\n", n); else printf("root is find %d, is %p,%d!\n", n, tmp, tmp->v); //查找父親結點 n = 12; tmp = tree_parent(root, n, NULL); if (tmp == NULL) printf("root is no find %d!\n", n); else printf("root is find parent %d, is %p,%d!\n", n, tmp, tmp->v); //刪除測試 n = 8; tree_delete(&root, n); TREE_PRINT(root); n = 9; tree_delete(&root, n); TREE_PRINT(root); //釋放資源 tree_destroy(&root); system("pause"); return 0; } /* * 在二叉查找樹中插入結點 * proot : 頭結點的指針 * v : 待插入變量值,會自動分配內存 */ void tree_insert(tree_t* proot, node_t v) { tree_t n, p = NULL, t = *proot; while (t) { if (t->v == v) //不讓它插入重復數據 return; p = t; //記錄上一個結點 t = t->v > v ? t->lc : t->rc; } //這裡創建結點,創建失敗直接退出C++都是這種做法 n = calloc(sizeof(struct tree), 1); if (NULL == n) CERR_EXIT("calloc struct tree error!"); n->v = v; //這裡插入了,開始第一個是頭結點 if (NULL == p) { *proot = n; return; } if (p->v > v) p->lc = n; else p->rc = n; } //數據打印函數,全部輸出,不會打印回車,中序遞歸 void tree_print(tree_t root) { if (root) { //簡單中序找到最左結點,打印 tree_print(root->lc); printf("%d ", root->v); tree_print(root->rc); } } /* * 在這個二叉查找樹中查找 值為v的結點,找不見返回NULL * root : 頭結點 * v : 查找結點值 * : 返回值為查找到的結點,找不見返回NULL */ tree_t tree_search(tree_t root, node_t v) { while (root) { if (root->v == v) return root; if (root->v > v) root = root->lc; else root = root->rc; } return NULL; } /* * 查找這個結點的父結點 * root : 頭結點 * v : 查找的結點 * : 返回這個v值的父親結點,找不見返回NULL,可以返回孩子結點 */ tree_t tree_parent(tree_t root, node_t v, tree_t* pn) { tree_t p = NULL; while (root) { if (root->v == v) break; p = root; if (root->v > v) root = root->lc; else root = root->rc; } if (pn) //返回它孩子結點 *pn = root; return p; } /* * 刪除結點 * proot : 指向頭結點的結點 * v : 待刪除的值 */ void tree_delete(tree_t* proot, node_t v) { tree_t root, n, p, t;//n表示v結點,p表示父親結點 if ((!proot) || !(root = *proot)) return; //這裡就找見 v結點 n和它的父親結點p p = tree_parent(root, v, &n); if (!n) //第零情況 沒有找見這個結點直接返回 return; //第一種情況,刪除葉子結點,直接刪除就可以此時t=NULL; 第二情況 只有一個葉子結點 if (!n->lc || !n->rc) { if (!(t = n->lc)) //找見當前結點n的唯一孩子結點 t = n->rc; if (!p) *proot = NULL; else { if (p->lc == n) //讓當前結點的父親收養這個它唯一的孩子 p->lc = t; else p->rc = t; } //刪除當前結點並返回,C要是支持 return void; 語法就好了 free(n); return; } //第三種情況, 刪除的結點有兩個孩子 //將當前結點 右子樹中最小值替代為它,繼承王位,它沒有左兒子 for (t = n->rc; t->lc; t = t->lc) ; n->v = t->v;//用nr替代n了,高效,並讓n指向找到t的唯一右子樹, tree_delete(&n->rc, t->v);//遞歸刪除n右子樹中最小值, 從t開始,很高效 } //采用後序刪除 static void __tree_destroy(tree_t root) { if (root) { __tree_destroy(root->lc); __tree_destroy(root->rc); free(root); } } /* * 刪除這個二叉查找樹,並把根結點置空 * proot : 指向根結點的指針 */ void tree_destroy(tree_t* proot) { if (proot) __tree_destroy(*proot); *proot = NULL; }
大家自己聯系一下,代碼不多,容易學習順帶回顧一下數據結構中二叉樹結構,關於其中 tree_destroy 編碼方式,是個人的編程習慣.
在C中變量聲明後沒有默認初始化, 所以習慣有這樣的代碼
struct sockaddr_in sddr; memset(&sddr, 0, sizeof sddr);
我覺得這樣麻煩,我習慣的寫法是
struct sockaddr_in saddr = { AF_INET };
利用了一個C聲明初始化潛規則,上面和下面代碼轉成匯編後也許都相似.後面寫法,默認編譯器幫我們把它後面沒初始化部分置成0.
還有一個習慣,可以允許一個爛的開始,必須要有一個perfect結束,參照老C++版本的智能指針,也叫破壞指針. 做法就是
char* p = malloc(1); free(p); p = NULL;
防止野指針.一種粗暴的做法,所以個人習慣在結束的時候多'浪費'一點時間回顧一下以前,再將其徹底抹除,等同於亞洲飛人直接刪除所有回憶的做法.
編程的實現.最後再吐槽一下,為什麼C++很爛,因為看了無數的書,還是不知道它要鬧哪樣.它就是一本易筋經,左練右練上練下練都可以,終於練成了
恭喜你,這張一張殘廢證收下.
再扯一點, 為什麼C++中叫模板,上層語言中叫泛型? 哈哈,可以參照全特化和偏(范)特化.這裡賣一個關子,但是本文中最後會有案例解決.
3.繼往開來,了解一些數據結構設計的模式
上面基本都扯的差不多了,這裡分享C中幾種的數據結構設計模式.
第一種 一切解'對象'
/* * C中如何封裝一個 tree '結構'(結構決定算法) */ /* * 第一種思路是 一切皆'對象' */ struct otree { void* obj; struct otree* lc; struct otree* rc; }; struct onode { int id; char* name; }; // obj => &struct onde的思路,浪費了4字節,方便管理
大家看到那個 void*應該就明白了吧等同於上層語言中Object對象.
第二種 萬物皆'泛型'
/* * 第二種思路是 萬物皆'泛型' */ struct tree_node { struct tree_node *lc; struct tree_node *rc; }; #define TREE_NODE \ struct tree_node *__tn struct ttree { TREE_NODE; //必須在第一行,不在第一行需要計算偏移量 offset //後面就是結構了 int id; char* name; };
下面這種相比上面這種節約4字節.缺點調試難.還有好多種例如模板流,特定寫死流. 這裡擴展一下另一個技巧
關於C中宏簡化結構的代碼
/* IPv6 address */ struct in6_addr { union { uint8_t __u6_addr8[16]; #if defined __USE_MISC || defined __USE_GNU uint16_t __u6_addr16[8]; uint32_t __u6_addr32[4]; #endif } __in6_u; #define s6_addr __in6_u.__u6_addr8 #if defined __USE_MISC || defined __USE_GNU # define s6_addr16 __in6_u.__u6_addr16 # define s6_addr32 __in6_u.__u6_addr32 #endif };
是不是很新奇,但是這樣的代碼,上層包塊庫都不推薦用,這些都是內核層的定義.用的越多越容易出錯.
到這裡基本就快結束了,上面介紹的幾種結構設計思路,大家需要自己揣摩. 特別有價值.搞明白.
再扯一點,很久以前對這樣的結構不明白
struct mem_storage{ union { int again; void* str; } mem; ..... };
上面again 是干什麼的,後來才明白了,主要作用是設定內存對齊的字節數.方便移植.使其結構體內存結構是一樣,也方便CPU讀取.
思考了很多但是還不明白, 那就對了,說明你還有追求!
這裡再擴展一下, 有時候
/* 常遇見下面代碼 */ void ss_free(void* arg) { if(....){ ..... free(arg); return; } .... }
真心 希望 C中提供 return void; 語法,
這樣就可以寫成
return free(arg); //或者 return (void)check(arg);
這樣代碼會更精簡, 更好看. 這裡也可以通過宏設計處理
#define return_func(f, ...) \ f(##__VA_ARGS__); \ return
屬於偽造吧,希望C委員會提供 return void; 語法!!
後記
錯誤是難免的,有問題提示馬上改. 下次有機會將二叉樹講透,關於設計開發庫中用的二叉樹結構都來一遍,最後分享一下,實際運用的
庫案例.拜~,
有時候在想如果不以經濟建設為中心,是不是人會更有意思一點? 有一款小網游叫中國, 挖了無數坑,就希望大R去充值,diao絲去陪練.哈哈