c語言(二叉查找樹代碼分析)

知識提要:

1.隨機訪問:對數組而言,可以使用數組下標直接訪問該數組中的任意元素,這叫做隨機訪問.
2.順序訪問:對鏈表而言,必須從鏈表首節點開始,逐個節點移動到要訪問的節點,這叫做順序訪問.
3.順序查找:從列表的開頭按順序查找.
4.二分查找:(1)規定待查找的項爲目標項;(2)假設列表中的各項按字母排序,比較列表中的中間項和目標項;(3)如果相等,則查找結束;如果目標項在列表中,中間項在目標項前面,則目標項一定在列表的後半部分…(4)再取後半部分的中間項,一直按這種思路取下去…
5.數組很好完成這種二分查找,但是鏈表不容易完成-------所以誕生了一種既支持頻繁插入和刪除項(鏈表輕鬆勝任)又支持頻繁查找的(數組輕鬆勝任)數據形式------二叉查找樹.
這個圖輔佐代碼分析
以C Prime Plus 第17章的代碼------寵物店分析舉例:
下面進行代碼分析:(只分析二叉查找樹的定義代碼(不分析聲明代碼和具體實現代碼))

一.數據結構的定義:

1.項的建立

/*---項的建立*/
#define SLEN 20//成員中的數組的長度

typedef struct item
{
	char petname[SLEN];
	char petkind[SLEN];
}Item;

2.節點的建立

/*---節點的建立*/
typedef struct trnode
{
	Item item;//項
	struct trnode* left;//左節點指針
	struct trnode* right;//右節點指針
}Trnode;

3.樹的建立

/*---樹的建立*/
typedef struct tree
{
	Trnode * root;//樹指針
	int size;//樹中節點數
}Tree;

二.二叉樹函數的一些定義

1.幾個簡單的"屬性類(反映一些樹的信息)"函數

void InitializeTree(Tree* ptree)
{
	ptree->root = NULL;
	ptree->size = 0;
}
bool TreeIsEmpty(const Tree* ptree)
{
	if (ptree->size == 0)
		return true;
	else
		return false;
}
bool TreeIsFull(const Tree* ptree)
{
	if (ptree->size == MAXITEMS)
		return true;
	else
		return false;
}
int TreeItemCount(const Tree* ptree)
{
	return ptree->size;
}

(1)初始化:讓指向樹的指針(ptree->root)指向null,樹中的項(ptree->size)爲0;
(2)判斷------樹是否是空,滿和樹中的節點數:通過判斷樹中的項數(ptree->size)的情況判斷.

2.複雜一點的"屬性類"操作------判斷具體的項是否在數中

bool InTree(const Item* pitem, const Tree* ptree)
{
	/*需要進行尋找--1.需要定義函數實現---以上*/

	return (SeekItem(pitem, ptree)).chlid == NULL ? false : true;//.chlid就是表示沒找到  找到的話這個值不會爲null(兩種情況 1.還沒開始找(就不存在樹) 2.找了沒找到(樹裏面沒有))
}

這裏用到了一個輔助函數SeekItem(pitem, ptree)

static Pair SeekItem(const Item* pitem, const Tree* ptree)
{
	//聲明變量---pair結構
	Pair look;

	//初始化變量
	look.chlid = ptree->root;//從頂點開始尋找
	look.parent = NULL;

	//如果沒有對應的樹---開始讓chlid節點指向樹的根節點---沒有頂點
	if (look.chlid == NULL)
		return look;//終止函數  提前返回

	//如果有對應的樹---直到指針爲空
	while (look.chlid != NULL)
	{
		/*向左邊尋找*/
		if (ToLeft(pitem, &look.chlid->item))//帶尋找項與樹中的項相比  如果在樹中項順序的後邊
		{
			//向左蔓延
			look.parent = look.chlid;
			look.chlid = look.chlid->left;
		}
		else if (ToRight(pitem, &look.chlid->item))
		{
			//向右蔓延
			look.parent = look.chlid;
			look.chlid = look.chlid->right;
		}
		else
			break;// 找到了 不用向下探尋了(沒有重複項)  當前子節點就是找到的節點
	}

	return look;

}

這個輔助函數中定義了一種輔助工具(Pair look)來幫忙查找

typedef struct  pair
{
	Trnode* parent;//記錄作用
	Trnode* chlid;//子節點爲探照節點 與樹中的節點進行信息比對
}Pair;

這個數據中由兩個指向節點的指針構成.用來查找和記錄位置使用

這個輔助函數中用到了其他的輔助函數ToLeft,ToRight

static bool ToLeft(const Item*item1, const Item*item2)
{
	//定義strcmp接受變量
	int cmp1, cmp2;
	//比較兩個指針指向的字符串  ---如果第一個字符串在前面  就返回true   (strcmp函數返回<0)
	//---寵物名的比較(先比寵物名)
	if ((cmp1 = strcmp(item1->petname, item2->petname)) < 0)
		return true;
	//---寵物種類的比較(寵物名相同就比寵物種類)
	else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) < 0))
		return true;
	else
		return false;

}

static bool ToRight(const Item*item1, const Item*item2)
{
	//定義strcmp接受變量
	int cmp1, cmp2;
	//比較兩個指針指向的字符串  ---如果第一個字符串在前面  就返回true   (strcmp函數返回<0)
	//---寵物名的比較(先比寵物名)
	if ((cmp1 = strcmp(item1->petname, item2->petname)) > 0)
		return true;
	//---寵物種類的比較(寵物名相同就比寵物種類)
	else if ((cmp1 == 0) && ((cmp1 = strcmp(item1->petkind, item2->petkind)) > 0))
		return true;
	else
		return false;
}

這裏只分析ToLeft函數,(ToRight函數同理)
這個函數比較比較項1和項2中的內容字母大小的排序(名字:strcmp(item1->petname, item2->petname))(名字相同就比種類(cmp1 = strcmp(item1->petkind, item2->petkind)) < 0)):結果是如果第一項小就返回true…
回到尋找節點函數SeekItem:現在分析這個函數的流程:

1.聲明查找結構並進行初始化

2.判斷是否有查找的主體(也就是確定樹存不存在)

3.如果樹存在就按照規定的方向查找(ToLeft,ToRight),這個就是二分查找的精髓

(1)將要查找的項先從根節點開始查找不停地用二分法查找
(2)每進行一個比對就進行查找數據的更新(父指針指向子指針,子指針指向子指針的查找方向)
(3)直到找到目標項或者找遍了所有節點.------找到了會返回查找數據的(.chlid就是指向目標節點)
在回到InTree函數:如果找到了(查找數據的.chlid成員變量不爲空)就會返回true.

3.添加項函數

bool AddItem(const Item* pitem, Tree* ptree)
{
	//創建一個新的節點(添加項是通過添加節點實現)---這裏並不是創建 而是提出一個概念(因爲不一定能創建  看有沒有違反要求) 真正的創建要進行分配空間和初始化  3.創建節點函數實現
	Trnode * new_node;
	//判斷樹的情況---滿了是不能添加節點的
	if (TreeIsFull(ptree))
	{
		fprintf(stderr, "樹是滿的  不能添加項了 你的明白???\n");
		return false;
	}
	//如果要添加的項已經存在  就不能進行添加----不能有重複項
	if (SeekItem(pitem, ptree).chlid != NULL)
	{
		fprintf(stderr, "你添加的項在樹中已經存在 莫添加了 ok?\n");
		return false;
	}
	//節點的正式創建---分配內存  初始化
	new_node = MakeNode(pitem);
	//如果創建失敗---分配空間失敗
	if (new_node == NULL)
	{
		fprintf(stderr, "哦豁,創建節點失敗~\n");
		return false;
	}
	//成功創建節點

	//---樹中的節點樹增加
	ptree->size++;
	//---如果樹是空的---------------------不能用TreeIsEmpty判斷   因爲這個函數是通過判斷size來判斷的  而現在的情況是 先初始化了size
	if (ptree->root==NULL)
		//------樹的根節點就是這個新節點
		ptree->root = new_node;
	else//---如果樹不是空的
	{
		//------執行添加節點函數-----創建4.添加節點函數
		AddNode(new_node, ptree->root);
	}
}

添加項目:

(1) 創建指向節點的指針

(2)如果創建成功(樹中的節點沒滿),判斷這個項是否存在(SeekItem)

(3)這個項如果不存在就進行初始化節點(MakeNode(pitem)😉----用到了輔助函數

static Trnode* MakeNode(const Item* pitem)
{
	//創建一個節點
	Trnode* new_node;
	//分配空間
	new_node = (Trnode*)malloc(sizeof(Trnode));
	//成員變量的初始化
	if (new_node != NULL)
	{
		new_node->item = *pitem;
		new_node->left = NULL;
		new_node->right = NULL;
	}
	return new_node;
}


(4)成功創建節點後,判斷節點的位置(作爲根節點還是子節點),如果是根節點(就讓樹的根節點指向這個新節點),如果是子節點則執行子節點操作函數-------輔助函數添加子節點

static void AddNode(Trnode* new_node, Trnode* root)
{
	/*要把新節點中的內容與樹中節點進行比較後才能插入*/

	//到達一個節點時 如果新節點的內容比樹節點的內容小
	if (ToLeft(&new_node->item, &root->item))
	{
		//--如果這個樹節點的左側是空的
		if (root->left == NULL)
		{
			//---就把新節點添加到樹節點的左側
			root->left = new_node;
		}

		else 
		{
			//--左側不是空的
			//---繼續調用這個函數尋找---這次跟樹節點的左邊指向的節點比
			AddNode(new_node, root->left);
		}
		
	}
	else if(ToRight(&new_node->item,&root->item))//如果發現這個新節點的內容比這個節點下面所有的節點內容都大
	{
		
		//--如果右側是空的
		if (root->right == NULL)
		{
			//就把這個新節點放在此位置
			root->right = new_node;
		}
		else//--如果不是空的
		{
			//就調用這個函數繼續尋找
			AddNode(new_node, root->right);
		}
	}
	else//如果發現新節點的內容不大不小  ---就是內容重複--輸出錯誤
	{
		fprintf(stderr, "你添加的東西重複了 懂?\n");
		exit(EXIT_FAILURE);
	}


}

這個輔助函數使用了遞歸來添加,具體流程是:

(1)將要添加的節點的項的內容與樹中節點的項的內容做對比(插入也要根據二分法插入)

(2)如果比樹中的當前節點小

1.如果當前節點左邊是空的(root->left == NULL)那麼把要添加的項放入左邊
2.如果不是空的,那麼將對比的內容換成--------需要添加的項與此節點左指針指向的節點進行比較

(3)如果找不到就進行右邊的尋找.

4.刪除項函數

bool DeleteItem(const Item* pitem, Tree* ptree)
{
	/*在樹中尋找到了目標項才能進行刪除*/
	Pair look;


	//創建尋找體
	//執行尋找函數 將結果反饋到尋找體中
	look = SeekItem(pitem, ptree);
	//如果沒有樹(尋找體子成員爲null)
	if (look.chlid == NULL)
		return false;
	//如果有樹  找到了對應項---如果是根這個點
	if (look.parent == NULL)
	{
		//---刪除根節點-----5.創建刪除節點函數
		DeletNode(&ptree->root);
	}
	//如果是子節點
	else if (look.parent->left == look.chlid)//--左節點
		DeletNode(&look.parent->left);
	else
		DeletNode(&look.parent->right);//---右節點

	ptree->size--;
	return true;
}

流程是:先查找你要刪除的項所在的節點,確認後執行刪除節點函數.-------刪除節點函數是個輔助函數

static void DeletNode(Trnode** ptree)
{
	//創建節點指針----要刪除就是釋放內存 釋放的就是內容所對應的地址
	Trnode * ptemp;
	//如果要刪除的節點左指針是空的  右指針不是空的---讓它的右指針來接上
	if ((*ptree)->left == NULL)
	{
		//先把這個指針保存下來
		ptemp = *ptree;
		//讓這個指針的右指針替換這個指針
		*ptree = (*ptree)->right;
		//釋放替死鬼
		free(ptemp);
	}
	else if((*ptree)->right == NULL)//如果要刪除的節點右指針是空的   左指針不是空的---讓它的左指針來接上
	{
		ptemp = *ptree;
		*ptree = (*ptree)->left;
		free(ptemp);
	}
	else
	{
		/*如果要刪除的節點  左右指針都不是空的 ---左指針接上  右指針去找尋離右指針最近的左指針指向節點的子節點的null  接上*/
		
		//尋找右節點能接上的位置----這裏替死鬼先當了一回查找員  查找到符合條件的節點  自己複製它  方便右節點接上
		for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)
		{
			continue;
		}
		//找到這個節點後---刪除節點的右節點接到這個節點上(ptemp就是找到的節點 它的右節點是null)
		ptemp->right = (*ptree)->right;
		

		//讓替死鬼指向要刪除的節點
		ptemp = *ptree;

		//---本來就接上自己的左節點----要記住要先讓替死鬼記錄要刪除節點的信息  在去讓要刪除節點的左指針上位
		*ptree = (*ptree)->left;


		//---釋放替死鬼
		free(ptemp);

	}
}

刪除節點函數的流程是:-----------------這裏操作的是節點的地址(Trnode ptree)而不是直接操作節點----因爲刪除節點就是通過釋放節點所在的地址來進行刪除的**

(1)創建一個指向節點的指針(通過釋放這個指針來釋放內容)

(2)對要刪除的節點進行判斷:
1.如果這個節點的左邊是空的((*ptree)->left == NULL),右邊不是空的---------那麼

  • 先將這個要刪除的節點保存(ptemp = *ptree;)
  • 再將此節點的右指針指向的節點接到這個指針上(*ptree = (*ptree)->right;)新,這一步是保存信息
  • 現在可以安全的釋放這個臨時工指針來達到刪除的目的(free(ptemp)😉

2.如果這個節點是右邊空的,左邊不是空的-----原理同上
3.如果這個節點左邊,右邊都不是空的--------這種情況,節點左邊的節點樹直接接上,右邊的節點要接到左邊節點樹中(依次向下尋找過程中)那種節點的右指針爲空的節點上,所以第一步要找出這種節點
for (ptemp = (*ptree)->left;ptemp->right!=NULL;ptemp = ptemp->right)-------這裏這個臨時節點充當符合條件的節點;然後讓要刪除節點的右邊((*ptree)->right;)指向這個臨時節點的右邊(ptemp->right),從而完成對接成功.
4.最後讓臨時工重操舊業,讓他去指向要刪除的節點,通過釋放臨時工來進行刪除操作.然後項數減1

5.用函數作用節點

void Traverse(const Tree* ptree, const void(*pfun)(Item item))
{
	//如果樹存在
	if(ptree->root!=NULL)
	{
		//---按順序作用每一個節點----創建按順序作用的函數
		InOrder(ptree->root, pfun);
	}
}

函數作用也會按照順序作用的,所以用到了一個輔助函數:InOrder

static void InOrder(const Trnode* ptree, const void(*pfun)(Item item))
{
	//如果樹節點不爲空
	if (ptree != NULL)
	{
		InOrder(ptree->left, pfun);//1.左邊被作用2.改變節點---原節點變爲原節點的左邊
		(*pfun)(ptree->item);//這個節點的項被作用
		InOrder(ptree->right, pfun);//1.右邊被作用 2.改變節點---原節點變爲原節點的右邊
	}
	/*相當於一隻尋找這個節點的左邊部分的節點 知道找到了那個節點左邊指向爲空的地方----------------注意遞歸的邊界
	然後用函數作用這個節點的項
	然後去找這個節點(不是原節點)它右邊的部分
	同樣把這個節點看做新的節點  然後還是從左邊找*/
}

這個函數也是通過遞歸來實現--------先作用它的左邊,然後作用它的項,最後作用它的右邊-------這個順序是從最下面的節點開始作用(遞歸的特點).

6.最後是刪除函數------刪除所有節點

void DeleteAll(Tree* ptree)
{
	//如果樹不是空的  就刪除所有節點
	if (ptree != NULL)
		DeletAllNote(ptree->root);//------------------6.創建刪除所有節點的函數
	//---樹指針指向空
	ptree->root = NULL;
	//---項數爲0
	ptree->size = 0;
}

這裏使用了輔助函數DeletAllNote-----刪除所有節點

void DeletAllNote(Trnode* pnode)
{
	//創建節點---替死鬼
	Trnode * pchange;
	//如果要刪除的節點不是空的---刪除分兩步  一步保存  二步刪保存的
	if (pnode != NULL)
	{
		//---根節點的右指針 指向替死鬼---先保存了右邊---並沒有刪除
		pchange = pnode->right;
		//刪除這個節點的左邊---遞歸自己----這樣就刪了一半----實質上是刪了所有節點的左節點(當然是從最底層開始) 通過釋放內存 刪除了右節點
		DeletAllNote(pnode->left);
		//--釋放這個節點的內存
		free(pnode);
		//最後調用函數清除替死鬼
		DeletAllNote(pchange);//這裏就是清除了另一半
	}


}

這裏也是利用遞歸來實現--------遞歸這東西在二叉樹中真的很常用(但也可以不用遞歸來實現)

具體操作:
1.設置"臨時工"-------Trnode * pchange;通過釋放它指向的節點來刪除節點裏的信息
2.進行遞歸:
(1)確定遞歸停止條件:樹中節點爲空的時候停止(pnode != NULL)

(2)讓臨時工保存信息(這裏保存了右邊的信息pchange = pnode->right;)(刪除也是通過二分法來進行的,但是刪除節點會刪除節點的全部信息,這樣會使得這個節點與其他節點的關係斷裂,從而其他節點得不到操作),刪除是一半一半的刪.

(3)確定操作方向(pnode->left)-----刪除節點的左邊的東西(即使本來就爲空,遞歸從最後面開始操作)

(4)釋放這個節點的內容(free(pnode)😉

這裏有些複雜(剛接觸遞歸其實到現在都沒整明白):我儘量表述的清楚一些:其實這個函數做了兩件事:保存信息和釋放內存
它先把原始點的右邊部分保存起來,然後向這個點的左邊開始尋找
而且邊尋找邊保存當前節點的右邊部分的信息,直到找到一個節點(這個節點的左指針是空)
然後釋放這個節點的地址(就是刪除了這個節點)
然後又從當前(刪掉的)節點的右指針指向的節點開始經過循環…
這樣就相當於從第一項刪除到了最後一項

沒有做動畫 將就着看吧
可以發現一個規律:每次對樹進行操作,在項的處理封裝一層(在最外面),在節點的處理封裝一層(在裏面),統籌性的工作比如刪除樹,會影響樹的信息(樹包括節點和節點數-------節點包括項和指針-------項就只包括基本數據,但是最直觀的確實操作項)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章