資料結構與演算法分析 讀書筆記(樹)
摘要:本節包含二叉查詢樹,AVL樹,伸展樹以及B樹
標籤: Data-Structures
注意,筆記不是講各種概念的,只是記錄一下個人學習過程中遇到的問題和實現程式碼。需要知道基本概念,理解各種樹的原理。
1 二叉查詢樹
struct TreeNode{
int Elem;
struct Treenode *Left,*Right;
};
SearchTree MakeEmpty(SearchTree T){
if (T != NULL){
MakeEmpty(T->Left);
MakeEmpty(T->Right);
free(T);
}
return NULL;
}
Position Find(int X, SearchTree T){
if (T == NULL){
return NULL;
}
if (X < T->Elem)
return Find(X,T->Left);
else if(X > T->Elem)
return Find(X,T->Right);
else
return T;
}
//遞迴實現查詢最小值
Position FindMin(SearchTree T){
if (T == NULL)
return NULL;
else if(T->Left == NULL)
return T;
else
return FindMin(T->Left);
}
//非遞迴查詢最大值
Position FindMax(SearchTree T){
if(T != NULL){
while(T->Right != NULL)
T = T->Right;
}
return T;
}
SearchTree Insert(int X, SearchTree T){
if (T == NULL){
T = malloc(sizeof(struct TreeNode));
T->Elem = X;
T->Left = T->Right = NULL;
}
else if(X < T->Elem)
T->Left = Insert(X,T->Left);
else if (X>T->Elem)
T->Right = Insert(X,T->Right);
return T;
}
SearchTree Delete(int X,SearchTree T){
Position P;
if (T==NULL)
return NULL;
else if (X < T->Elem){
T->Left = Delete(X,T->Left);
}else if(X > T->Elem)
T->Right = Delete(X, T->Right);
else if (T->Left && T->Right){
P = FindMin(T->Right);
T->Elem = P->Elem;
Delete(P->Elem,T->Right);
}else{
P = T;
if (T->Left== NULL)
T = T->Right;
else if (T->Right == NULL)
T = T->Left;
free(P);
}
return T;
}
實現Delete時,有點不明白,在刪除只有一個子節點的 T 節點時,為什麼
T = T->Right;
後來仔細想了一下,紙上畫了一下,還是因為指標的本質導致的。
指標本質就是地址,當刪除子節點 T 時,父節點 parent 節點的某個指標域需要賦值,而這個值則是孫節點的地址.程式碼中 T 指標指向 兒子節點,此時 T 變數被賦值為兒子節點的地址,return T意味著返回兒子節點的地址。函式返回時,父節點parent的子樹指標域被賦值為孫節點的地址,變成了兒子節點。
感想:可以從二叉查詢樹的實現程式碼中看到很多遞迴呼叫,極大的簡化了實現。不小心指標和遞迴繞到。
2 AVL樹
AVL樹是帶有平衡條件的二叉查詢樹。在插入和刪除節點時,必須保持這個平衡條件-每個節點的左子樹和右子樹的高度最多相差為1。需要給每個節點加個附加域-高度域
sttuct AvlNode{
int Elem;
AvlTree *Left,*Right;
int Height;
}
當插入節點後,需要更新插入節點處到根節點的路徑上的所有平衡資訊。如果破化了AVL樹的平衡性,需要進行旋轉來保持平衡條件。
當在某個節點 T 的某個子樹中插入節點後,高度不平衡。
不平衡會出現4種情況:
- T節點的左兒子的左子樹插入節點
- T節點的左兒子的右子樹插入節點
- T節點的右兒子的左子樹插入節點
- T節點的右子樹的右子樹插入節點
4種情況是2種映象情況的對稱,這裡只考慮左左和左右2種情況,可以分為單旋轉和雙旋轉。
單旋轉
雙旋轉
基本例程和二叉查詢樹一樣,不過多了幾個用於AVL樹旋轉的例程.
雙旋轉可以通過2次單旋轉實現,也可以通過直接調整指標。下面的旋轉例程採用了2種方式。
int Height(Position P){
if (P == NULL)
return -1;
else
return P->Height;
}
int Max(int a,int b){
return a>b?a:b;
}
//左旋轉
Position SingleRotationWithLeft(Position K2){//情況 1
Position K1;
K1 = K2->Left;
K2->Left = K1->Right;
K1->Right = K2;
K2->Height = Max(Height(K2->Left),Height(K2->Right)) + 1;
K1->Height = Max(Height(K1->Left),K2->Height) + 1;
return K1;
}
//左雙旋轉
Position DoubleRotationWithLeft(Position K3){//情況 2
// K3->Left = SingleRotationWithRight(K3->Left);
// return SingleRotationWithLeft(K3);
Position K1, K2;
K1 = K3->Left;
K2 = K1->Right;
K1->Right = K2->Left;
K3->Left = K2->Right;
K2->Left = K1;
K2->Right = K3;
K1->Height = Max( Height(K1->Left), Height(K1->Right) ) + 1;
K3->Height = Max( Height(K3->Left), Height(K3->Right) ) + 1;
K2->Height = Max( K1->Height, K3->Height ) + 1;
return K3;
}
Position DoubleRotationWithRight(Position K3){//情況 3
K3->Right = SingleRotationWithLeft(K3->Right);
return SingleRotationWithRight(K3);
}
Position SingleRotationWithRight(Position K2){//情況 4
Position K1;
K1 = K2->Right;
K2->Right = K1->Left;
K1->Left = K2;
K2->Height = Max(Height(K2->Left),Height(K2->Right))+1;
K1->Height = Max(K2->Height,Height(K1->Right))+1;
return K1;
}
插入和刪除:
AvlTree Insert(int X, AvlTree T){
if (T == NULL){
T = malloc(sizeof(struct AvlNode));
T->Elem = X;
T->Left = T->Right = NULL;
T->Height = 0;
}else if (X < T->Elem){
T->Left = Insert(X, T->Left);
if (Height(T->Left) - Height(T->Right) == 2)
if (X < T->Left->Elem)
T = SingleRotationWithLeft(T);
else
T = DoubleRotationWithLeft(T);
}else if ( X > T->Elem){
T ->Right = Insert(X, T->Right);
if (Height(T->Right) - Height(T->Left) == 2)
if (X > T->Right->Elem)
T = SingleRotationWithRight(T);
else
T = DoubleRotationWithRight(T);
}
T->Height = Max(Height(T->Left),Height(T->Right))+1;
return T;
}
AvlTree Delete(int X, AvlTree T){
Position P;
if (T == NULL)
return NULL;
else if( X < T->Elem){
T->Left = Delete(X, T->Left);
if (Height(T->Right) - Height(T->Left) == 2)
T = SingleRotationWithRight(T);
}
else if (X > T->Elem){
T->Right = Delete(X, T->Right);
if (Height(T->Left) - Height(T->Right) == 2)
T = SingleRotationWithLeft(T);
}else if(T->Left && T->Right){
P = FindMin(T->Right);
T->Elem = P->Elem;
T->Right = Delete(P->Elem,T->Right);
}else{
P = T;
if (T->Left == NULL)
T = T->Right;
else if (T->Right == NULL)
T = T->Left;
free(P);
}
return T;
}
在實現時,依次插入節點 1 2 3 4 5 6 7 16 17,節點17插入後,需要對7、16、17進行右單旋轉調整保持平衡,但由於對遞迴和指標不熟悉,未寫某個關鍵程式碼如下
//錯誤程式碼
SingleRotationWithRight(T);
//正確程式碼
T = SingleRotationWithRight(T);
直到單步除錯時,發現單旋轉沒有錯誤,但是函式返回後,T節點依然是7,而且丟失了右子樹,才反應過來調整後的樹的根沒有被指向。
遍歷:
void PrintTree(AvlTree T){
if (T != NULL){
PrintTree(T->Left);
printf(" %d ",T->Elem);
PrintTree(T->Right);
}
}
3 伸展樹
伸展樹建立在二叉查詢樹的基礎上,不過為了避免每次操作最壞時間 O(N)。
通過將一個被訪問過的節點旋轉到根上,那麼對於原先處於深度比較深的其它節點在被訪問時,會降低訪問時間。區域性性原理(剛被訪問的內容下次可能仍會被訪問)。
將要查詢的節點自底向上旋轉,直至該節點成為樹根。
保證了在任意 M 次訪問時,最多花費 O(MlogN) 的執行時間,伸展樹的每次操作的攤還代價為 O(logN)。
很遺憾,伸展樹的思路實現還是很好理解的,但是在實現過程中,遇到了一個很嚴重的問題。
比如依次插入7 6 5 4 3 2 1,當查詢 1 時,只能將3 2 1進行一字型旋轉後,就不能進行下去了。
錯誤程式碼
Position Find(int X, SplayTree T){
if (T == NULL || T->Elem == X)
return T;
else if (X < T -> Elem ){
if( T->Left->Elem == X){//根的左子節點就是所要查詢的節點,則和根旋轉,單旋轉
T->Left = T->Left->Right;
T->Left->Right = T;
return T->Left;
}else if (X < T->Left->Elem){
if( X == T->Left->Left->Elem)
T = OneRotationWithLeft(T);
else
T->Left = Find(X,T->Left);
}else if(X > T->Left->Elem){
if( X == T->Left->Right->Elem){
T = DoubleRotationWithLeft(T);
}else
T->Left = Find(X,T->Left);
}
}else if (X > T->Elem){
if (T->Right->Elem == X){
T->Right = T->Right->Left;
T->Right->Left = T;
return T->Right;
}else if(X < T->Right->Elem){
if (X == T->Right->Left->Elem){
T = DoubleRotationWithRight(T);
}else{
T->Right = Find(X,T->Right);
}
}else if (X > T->Right->Elem){
if ( X == T->Right->Right->Elem){
T = OneRotationWithRight(T);
}else{
T->Right = Find(X,T->Right);
}
}
}
return T;
}
寫得很囉嗦,判斷很多種情況。其中幾個旋轉例程是針對4種情況的,Zig-zag和Zig-zig,以及對稱情況
//旋轉例程,分為Zig-zag和Zig-zig,即之字型旋轉和一字型旋轉
//左右雙旋轉
Position DoubleRotationWithLeft(Position G){
Position P,X;
P = G->Left;
X = P->Right;
P->Right = X->Left;
X->Left = P;
G->Left = X->Right;
X->Right = G;
return X;
}
//右左雙旋轉
Position DoubleRotationWithRight(Position G){
Position P,X;
P = G->Right;
X = P->Left;
G->Right = X->Left;
X->Left = G;
P->Left = X->Right;
X->Right = P;
return X;
}
//左一字型
Position OneRotationWithLeft(Position G){
Position P,X;
P = G->Left;
X = P->Left;
G->Left = P->Right;
P->Left = X->Right;
P->Right = G;
X->Right = P;
return X;
}
//右一字型
Position OneRotationWithRight(Position G){
Position P,X;
P = G->Right;
X = P->Right;
G->Right = P->Left;
P->Left = G;
P->Right = X->Left;
X->Left = P;
}
反思自己的實現過程中的錯誤,幾個旋轉例程很容易實現,不過建議在紙上畫一下如何旋轉。關鍵是如何處理好將找到的節點不斷地進行回溯旋轉。
前面是自己寫的程式碼,下列是參考程式碼
也被折磨好幾天,最後參考了部落格
仔細看了一遍,再回來看看自己那程式碼實現太簡陋。
主要是給節點多附加了一個指向父節點的域
struct SplayNode{
Tree parent; //該結點的父節點,方便操作
ElementType val; //結點值
Tree lchild;
Tree rchild;
SplayNode(int val=0) //預設建構函式
{
parent=NULL;
lchild=rchild=NULL;
this->val=val;
}
};
其次,在處理節點時,通過劃分模組,邏輯很清晰。
void SplayTree(Tree &root,Tree node)
{
while (root->lchild!=node && root->rchild!=node && root!=node) //當前結點不是根,或者不是其根的左右孩子,則根據情況進行旋轉操作
up(root, node);
if (root->lchild==node) //當前結點為根的左孩子,只需進行一次單右旋
root=right_single_rotate(root, node);
else if(root->rchild==node) //當前結點為根的右孩子,只需進行一次單左旋
root=left_single_rotate(root, node);
}
//根據情況,選擇不同的旋轉方式
void up(Tree &root,Tree node)
{
Tree parent,grandparent;
int i,j;
parent=node->parent;
grandparent=parent->parent;
i=grandparent->lchild==parent ? -1:1;
j=parent->lchild==node ?-1:1;
if (i==-1 && j==-1) //AVL樹中的LL型
right_double_rotate(root, node);
else if(i==-1 && j==1) //AVL樹中的LR型
LR_rotate(root, node);
else if(i==1 && j==-1) //AVL樹中的RL型
RL_rotate(root, node);
else //AVL樹中的RR型
left_double_rotate(root, node);
}
**總結:
1 遺憾自己沒有寫出伸展樹
2 小Tip:當覺得自己對二叉查詢樹和AVL樹有了一定程度瞭解後,建議嘗試實現伸展樹**
4 B樹
B樹基本操作,查詢,插入和刪除
- 查詢類似於二叉查詢樹,不過不是二路,而是多路;一趟查詢,要麼找到關鍵字的位置,要麼能確定關鍵字所在的子樹的指標
- 插入關鍵字:按照查詢的思路,從根節點開始查詢,遇到節點的關鍵字已滿2M-1,需要分裂出新節點,每個節點包含M-1個節點,將中間節點移到父節點,在2個節點指標之間。為何是M-1,因為B樹的性質保證B樹除根節點外的內部節點的關鍵字數必須介於2*M-1和M-1之間,M稱為B樹的階
- 刪除關鍵字:分為幾種情況 1 : 當前T節點為葉子節點,關鍵字key在T節點中,直接刪除 2 : 當前T節點為內部節點,關鍵字key在T節點中,根據子節點關鍵字的數目和M-1比較,決定將關鍵字key左右子樹中的一個關鍵字替換key,或者需要合併節點 3 : 關鍵字不在當前內部節點T中,需要確定可能包含關鍵字key的子樹節點。再根據子樹關鍵字數目,確定是否需要從左右兄弟節點中移動一個節點替代父節點中關鍵字,把父節點中被替代的關鍵字放在被刪除的位置上
在實現的過程中,一個關鍵問題就是插入要保證不會滿,否則分類;刪除要保證節點的關鍵字數目不能低於M-1,否則破壞B樹性質。
B樹以及相關變體,關鍵在插入關鍵字判斷是否需要分裂節點,向上回溯直到滿足B樹性質,刪除關鍵字的節點是否需要合併。
此部分參考部落格以及演算法導論18章
struct BNodeRecord{
int num;//關鍵字個數
int leaf;// 1 表示葉子 0 表示內部節點
int key[2*M - 1];
BNode c[2*M];
};
建立B樹
BTree CreateBTree(){
BTree T;
T = malloc(sizeof(struct BNodeRecord));
T->num = 0;
T->leaf = 1;
return T;
}
在T節點中查詢關鍵字key的索引
BNode BTreeSearch(BTree T, int key, int *index){
int i = 0;
while(i < T->num && T->key[i] < key)
i++;
if (i < T->num && T->key[i] == key){
*index = i;
return T;
}
else if ( T->leaf == 1){
printf("no key in BTree\n");
return NULL;
}
return BTreeSearch(T->c[i], key, index);
}
分裂節點
BTree BTreeSplitChild(BTree T, int index){
BNode Y,Z;//Y表示滿節點,Z表示新節點
Y = T->c[index];
Z = malloc(sizeof(struct BNodeRecord));
Z-> num = M - 1;
Y-> num = M - 1;
Z->leaf = Y->leaf;
//關鍵字轉移
for (int i = M; i < 2*M-1; ++i) {
Z->key[i-M] = Y->key[i];
}
//如果是內部節點則進行指標轉移
if (!Y->leaf){
for (int j = M; j < 2*M; ++j) {
Z->c[j-M] = Y->c[j];
}
}
for(int k = T->num-1; k>= index; k--){
T->key[k+1] = T->key[k];
}
for (int l = T->num; l >= index+1; ++l) {
T->c[l+1] = T->c[l];
}
T->key[index] = Y->key[M-1];
T->c[index+1] = Z;
T->num++;
return T;
}
插入
BTree BTreeInsert(BTree T, int key){
if(T->num == 2*M - 1){
BNode S;
S = malloc(sizeof(struct BNodeRecord));
S->num = 0;
S->leaf = 0;
S->c[0] = T;
T = BTreeSplitChild(S,0);
BTreeInsertNotFull(T,key);
}else
BTreeInsertNotFull(T,key);
}
刪除
BTree BTreeDelete(BTree T, int key){
int index, i, flag;
//在T節點中查詢關鍵字是否存在,在的話,返回其下標index,
index = BTreeSearchIndex(T, key, &flag);
//falg標誌T->key[index] == key,即節點T中有關鍵字key
/* 第一種情況:關鍵字在葉子節點中,直接刪除它 */
if (T->leaf == 1 && flag == 0){
//flag=0表示T節點中有關鍵字,且T是葉子節點
//把index後面的所有關鍵字向前移動一位,關鍵字數目減1
memmove(&T->key[index], &T->key[index+1], sizeof(int) * (T->num - index -1));
T->num--;
return T;
}else if (T->leaf && flag != 0){
//關鍵字不在T節點中
return T;
}
/* 第二種情況:關鍵字在內部節點 T 中,根據3種情況- T 的子節點的關鍵字數目來刪除和移動*/
if (flag != 0){
// Y :T->key[index]的前一個指標所指向的子節點,Z :T->key[index]的後一個指標所指向的子節點
// T->key[index](關鍵字)
// T->c[index](T中指標) T->c[index+1](T中指標)
// | |
// Y Z
BNode Y, Z;
Y = T->c[index];
Z = T->c[index + 1];
// a : Y中至少有M個關鍵字,在Y子樹中找到key的前驅關鍵字key',遞迴刪除key',將key'移到key中;
if (Y->num >= M){
int pre_key = Y->key[Y->num - 1];
//Y子樹中刪除pre_key
T->c[index] = BTreeDelete(Y, pre_key);
T->key[index] = pre_key;
return T;
}
// b : Y中關鍵字數目低於M個,對稱地將 Z子樹 中的第一個節點移動到T->key[index]中
if (Z->num >= M){
int next_key = Z->key[0];
//Z子樹中刪除next_key
T->c[index+1] = BTreeDelete(Z,next_key);
T->key[index] = next_key;
return T;
}
// c : Y和Z每一個關鍵字數目都是M - 1,則合併Y、Z以及關鍵字key,在Y中遞迴刪除關鍵字Key
if( (Y->num == M-1) && (Z->num == M-1)){
//將關鍵字key放在Y和Z關鍵字之間
Y->key[Y->num++] = key;
//關鍵字複製
memmove(&Y->key[Y->num], &Z->key[0], sizeof(int) * Z->num);
//指標複製
memmove(&Y->c[Y->num], &Z->key[0], sizeof(struct BNodeRecord) * (Z->num + 1));
Y->num += Z->num;
//判斷T節點刪除一個關鍵字後是否為空,是則Y子節點作為當前的返回節點
if (T->num > 1){
memmove(&T->key[index], &T->key[index+1], sizeof(int) * (T->num - index - 1));
memmove(&T->c[index+1], &T->c[index+2], sizeof(struct BNodeRecord)*(T->num - index - 1));
T->num--;
}else{//為空,釋放節點
free(T);
T = Y;
}
free(Z);
BTreeDelete(Y, key);
return T;
}
}
// 第三種情況 : 關鍵字key不在內部節點T中,如果key關鍵字在T的子節點T->c[index]中,根據子節點的關鍵字數目是否低於M,是則需要父節點中一個關鍵字移動到兄弟節點子節點T->c[index]中,再把兄弟節點中一個關鍵字移動到父節點,
BNode child;//可能含有關鍵字的子節點
if ((child = T->c[index]) && child->num == M-1){
// 1:child子節點中含有M-1個關鍵字,相鄰左或者右兄弟節點中含有至少M個節點,先將父節點中移一個關鍵字到child節點中,再將兄弟節點中一個關鍵字移到父節點中
// 先假設左兄弟節點至少有M個節點
BNode sibling;
if ( (index > 0) && (sibling = T->c[index-1]) && sibling->num >= M){
//child子節點關鍵字和指標向後移動一位,父節點關鍵字放入child關鍵字下表0的位置
memmove(&child->key[1],&child->key[0], sizeof(int) * child->num);
memmove(&child->c[1],&child->c[0], sizeof(struct BNodeRecord) * (child->num + 1));
child->key[0] = T->key[index-1];
T->key[index-1] = sibling->key[sibling->num-1];
child->c[0] = sibling->c[sibling->num];
child->num++;
sibling->num--;
}else
//右兄弟節點至少有M個節點
if((index<T->num) && (sibling = T->c[index+1]) && (sibling->num >= M)){
child->key[child->num++] = T->key[index];
T->key[index] = sibling->key[0];
child->c[child->num] = sibling->c[0];
sibling->num--;
memmove(&sibling->key[0],&sibling->key[1], sizeof(int) *(sibling->num));
memmove(&sibling->c[0],&sibling->c[1], sizeof(struct BNodeRecord) * sibling->num+1);
}
// 2 :左右兄弟節點關鍵字數目都是M-1,合併其中一個兄弟節點和child節點
//合併child節點和右兄弟節點
else if ((index<T->num) && (sibling = T->c[index+1]) && (sibling->num == M-1)){
child->key[child->num++] = T->key[index];
memmove(&child->key[child->num],&sibling->key[0], sizeof(int) * sibling->num);
memmove(&child->c[child->num], &sibling->c[0], sizeof(struct BNodeRecord) * (sibling->num + 1));
child->num +=sibling->num;
if (T->num > 1){
memmove(&T->key[index],&T->key[index+1], sizeof(int) * (T->num - index - 1));
memmove(&T->c[index+1],&T->c[index+2], sizeof(struct BNodeRecord) * (T->num-index -1));
T->num--;
}else{
free(T);
T= child;
}
free(sibling);
}else if((index>0) && (sibling = T->c[index-1]) && (sibling->num == M-1)){
sibling->key[sibling->num++] = T->key[index - 1];
memmove(&sibling->key[sibling->num], &child->key[0], sizeof(int) * child->num);
memmove(&sibling->c[sibling->num], &child->c[0], sizeof(struct BNodeRecord) * (child->num + 1));
sibling->num += child->num;
if (T->num - 1 > 0)
{
memmove(&T->key[index - 1], &T->key[index], sizeof(int) * (T->num - index));
memmove(&T->c[index], &T->c[index + 1], sizeof(struct BNodeRecord) * (T->num - index));
T->num--;
}
else
{
free(T);
T = sibling;
}
free(child);
child = sibling;
}
}
BTreeDelete(child,key);
return T;
}
給非空節點插入
BTree BTreeInsertNotFull(BTree T, int key){
int i = T->num - 1;
if (T->leaf){
while(i>=0 && T->key[i]>key){
T->key[i+1] = T->key[i];
i--;
}
T->key[i+1] = key;
T->num++;
}else{
while(i>=0 && T->key[i]>key)
i--;
i++;
if (T->c[i]->num == 2*M-1){
BTreeSplitChild(T,i);
if (key > T->key[i])
i++;
}
BTreeInsertNotFull(T->c[i],key);
}
return T;
}
查詢節點T是否包含關鍵字key
int BTreeSearchIndex(BTree T, int key, int *flag){
int i;
for (int i = 0; i < T->num && (*flag = T->key[i] - key) < 0; ++i) {
;
}
return i;
}
總結: B樹很不好理解,花了好幾天,從概念上理解,在細節上注意,最好每一步都在圖上畫,尤其是刪除部分,許多情況,結構複雜易出錯。此外,最讓我頭疼的是,那個移動關鍵字很是小心,最後發現,完全可以提取出來作為一個函式,不用每次都重新實現。
相關文章
- 資料結構與演算法讀書筆記 - 004 -C++遞迴資料結構演算法筆記C++遞迴
- 《資料結構與演算法分析》學習筆記-第四章-樹資料結構演算法筆記
- 《Hbase原理與實踐》讀書筆記——2.基礎資料結構與演算法筆記資料結構演算法
- 資料結構與演算法分析學習筆記(四) 棧資料結構演算法筆記
- 《資料結構與演算法之美》資料結構與演算法學習書單 (讀後感)資料結構演算法
- 樹 【資料結構與演算法分析 c 語言描述】資料結構演算法
- 《Python 簡明教程》讀書筆記系列四 —— 資料結構Python筆記資料結構
- 資料結構與演算法:AVL樹資料結構演算法
- 《Python資料分析與挖掘實戰》-- 讀書筆記(2)-- 2019Python筆記
- AVL 樹 【資料結構與演算法分析 c 語言描述】資料結構演算法
- 伸展樹 【資料結構與演算法分析 c 語言描述】資料結構演算法
- 資料結構與演算法-學習筆記(二)資料結構演算法筆記
- 資料結構與演算法-學習筆記(16)資料結構演算法筆記
- 資料結構與演算法學習筆記01資料結構演算法筆記
- 資料結構與演算法課程筆記(二)資料結構演算法筆記
- 《資料結構與演算法分析》學習筆記-第七章-排序資料結構演算法筆記排序
- 資料結構和演算法學習筆記十六:紅黑樹資料結構演算法筆記
- 資料結構:初識(資料結構、演算法與演算法分析)資料結構演算法
- 《戀上資料結構與演算法》筆記(九):二叉搜尋樹 II資料結構演算法筆記
- 資料結構筆記——樹的基本概念資料結構筆記
- 《資料結構與演算法分析》學習筆記-第五章-雜湊資料結構演算法筆記
- 資料結構與演算法分析——棧資料結構演算法
- 《讀書與做人》讀書筆記筆記
- 05 Javascript資料結構與演算法 之 樹JavaScript資料結構演算法
- 資料結構與演算法:哈夫曼樹資料結構演算法
- 【資料結構與演算法】二叉樹資料結構演算法二叉樹
- MySQL 讀書筆記 (一) 體系結構MySql筆記
- 資料結構與演算法分析——連結串列資料結構演算法
- 《JavaScript資料結構與演算法》筆記——第3章 棧JavaScript資料結構演算法筆記
- 《JavaScript資料結構與演算法》筆記——第6章 集合JavaScript資料結構演算法筆記
- 大二上 資料結構與演算法筆記 20241024資料結構演算法筆記
- [學習筆記] Splay & Treap 平衡樹 - 資料結構筆記資料結構
- 【演算法學習筆記】動態規劃與資料結構的結合,在樹上做DP演算法筆記動態規劃資料結構
- JVM讀書筆記之java記憶體結構JVM筆記Java記憶體
- 《重構》讀書筆記筆記
- 資料結構與演算法分析——佇列資料結構演算法佇列
- python之資料結構與演算法分析Python資料結構演算法
- python演算法與資料結構-資料結構中常用樹的介紹(45)Python演算法資料結構
- 演算法與資料結構--簡析紅黑樹演算法資料結構