1. 程式人生 > >二叉查詢樹的插入和刪除詳解

二叉查詢樹的插入和刪除詳解

(1)  左子樹不空,則左子樹上的所有結點的值均小於根結點的值

(2)  右子樹不空,則右子樹上的所有結點的值均大於根結點的值

二叉查詢樹可以為空,二叉查詢樹是遞迴定義的,也就是說其左右子樹也為二叉查詢樹。

二叉查詢樹是一種動態查詢表,可以進行動態地插入和刪除。前面的定義中我們假定二叉查詢樹不含有相同元素。

由定義可知二叉查詢樹的中序序列為一個遞增序列

常見的二叉查詢樹操作有求最小元素findMin(),求最大元素findMax(),判斷查詢樹是否非空isEmpty(),判斷是否包含給定元素contains(),輸出所有元素printTree(),置空查詢樹makeEmpty(),向查詢樹中插入給定元素x,insert(x),在查詢樹中刪除給定元素x,remove(x)。

下面先討論最重要的插入和刪除操作,接著給出完整的C風格實現

1、基本結構定義

class Student
{
	public:
		int key;
		string major;
		Student(int k=int(),string s="") : key(k), major(s){}
		void operator=(const Student& rhs);
};
typedef Student ElementType;
typedef int KeyType;

typedef struct BSTNode
{
	ElementType data;
    struct BSTNode* lchild;
    struct BSTNode* rchild;
}BSTNode, *BST;

詳細資訊請看後面完整程式

2、插入

(1)  不允許插入相同關鍵字,若二叉查詢樹中存在該關鍵字,則不插入

(2)  我們可以先檢索二叉樹,看查詢樹中是否含有該關鍵字,若不存在,再做一次掃描將結點插入到適當位置。使用這種方式,為插入一個該關鍵字,做了兩次掃描。

(3)  注意到,插入的結點總是作為某一葉子節點的子結點,我們可以在第一次掃描過程中就確定待插入的位置,即把查詢是否存在該關鍵字查詢可能的插入點在一次掃描中完成,提高插入效率。

(4)  查詢遞迴實現

/*
  description:在以t為根結點的二叉查詢樹中,查詢關鍵字為key的結點
  若查詢成功,指標p指向該結點,返回true,
  否則指向查詢路徑上的最後一個結點並返回false
  指標f指向根結點t的父節點 
*/
bool searchBST_recursion(BST t, KeyType key, BSTNode* f, BSTNode*& p)
{
	if(t == NULL)//查詢失敗 
	{
		p = f;
		return false;
	}
	else if(key == t->data.key)//查詢成功 
	{
		p = t;
		return true;
	}
	else if(key < t->data.key)
		return searchBST_recursion(t->lchild,key,t,p);//左子樹中繼續查詢 
	else 
		return searchBST_recursion(t->rchild,key,t,p);//右子樹中繼續查詢 
}

(5)  查詢非遞迴實現

bool searchBST(BST t, KeyType key, BSTNode* f, BSTNode* &p)
{
	while(t && key != t->data.key)
	{
		f = t;
		if(key < t->data.key)
		{
			t = t->lchild;
		}
		else
		{
			t = t->rchild;
		}			
	}
	if(t)//查詢成功 
	{
		p = t;
		return true;
	}
	else
	{
		p = f;
		return false;
	}
}

(6)  插入給定元素

void insertBST(BST& t,ElementType elem)
{
	BSTNode * p = NULL;
	if(!searchBST(t, elem.key, NULL, p))//查詢失敗,不含該關鍵字,可以插入 
	{
		BSTNode * s = new BSTNode;
		s->data = elem;//可能需要過載= 
		s->lchild = NULL;
		s->rchild = NULL;
		
		if(p == NULL)//查詢樹為空 
		{
			t = s;//置s為根結點 
		}
		else if(elem.key < p->data.key)
		{
			p->lchild = s;	//*s為p左結點 
		}
		else
		{
			p->rchild = s;	//*s為p右結點 
		}
	}
}

3、刪除

在二叉查詢樹中刪除一個給定的結點p有三種情況

(1)  結點p無左右子樹,則直接刪除該結點,修改父節點相應指標

(2)  結點p有左子樹(右子樹),則把p的左子樹(右子樹)接到p的父節點上

(3)  左右子樹同時存在,則有三種處理方式

a.      找到結點p的中序直接前驅結點s,把結點s的資料轉移到結點p,然後刪除結點s,由於結點s為p的左子樹中最右的結點,因而s無右子樹,刪除結點s可以歸結到情況(2)。嚴蔚敏資料結構P230-231就是該處理方式。

b.     找到結點p的中序直接後繼結點s,把結點s的資料轉移到結點p,然後刪除結點s,由於結點s為p的右子樹總最左的結點,因而s無左子樹,刪除結點s可以歸結到情況(2)。演算法導論第2版P156-157該是該處理方式。

c.      找到p的中序直接前驅s,將p的左子樹接到父節點上,將p的右子樹接到s的右子樹上,然後刪除結點p。

使用處理方式a的程式碼如下:

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,刪除前驅方式 
void removeNode1(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->lchild;		//左子樹根結點
		while(s->rchild)	//尋找結點*p的中序前驅結點,					
		{					//也就是以p->lchild為根結點的子樹中最右的結點 
			q = s;			//*s指向*p的中序前驅 
			s = s->rchild;	//*q指向*s的父節點 
		}
		p->data = s->data;	//*s結點中的資料轉移到*p結點,然後刪除*s
		if(q != p)			//p->lchild右子樹非空 
		{
			q->rchild = s->lchild;//把*s的左子樹接到*q的右子樹上 
		}
		else				//p->lchild右子樹為空 ,此時q ==p 
		{
			q->lchild = s->lchild;//把*s的左子樹接到*q的左子樹上 
		}
		delete s;			//刪除結點*s 
	}
}

使用處理方式b的程式碼如下:

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,刪除後繼方式 
void removeNode2(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->rchild;		//右子樹根結點
		while(s->lchild)	//尋找結點*p的中序後繼結點,					
		{					//也就是以p->rchild為根結點的子樹中最左的結點 
			q = s;			//*s指向*p的中序後繼 
			s = s->lchild;	//*q指向*s的父節點 
		}
		p->data = s->data;	//*s結點中的資料轉移到*p結點,然後刪除*s
		if(q != p)			//p->rchild左子樹非空 
		{
			q->lchild = s->rchild;//把*s的右子樹接到*q的左子樹上 
		}
		else				//p->rchild左子樹為空 ,此時q ==p 
		{
			q->rchild = s->rchild;//把*s的右子樹接到*q的右子樹上 
		}
		delete s;			//刪除結點*s 
	}
}

使用處理方式c的程式碼如下:

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,直接刪除p的方式 
void removeNode3(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->lchild;		//左子樹根結點
		while(s->rchild)	//尋找結點*p的中序前驅結點,					
		{					//也就是以*s為根結點的子樹中最右的結點 
			s = s->rchild;	 
		}
		s->rchild = p->rchild;//*p的右子樹接到*s的右子樹上 
		p = p->lchild;			//*p的左子樹接到父節點上 
		delete q;			//刪除結點*q 
	}
}

在二叉查詢樹中刪除含有給定關鍵字的元素結點的遞迴函式如下:

//刪除關鍵字為key的元素結點 -遞迴 
void removeBST_recursion(BST& t, KeyType key)
{
	if(t)
	{
		if(key < t->data.key)
		{
			removeBST_recursion(t->lchild,key);
		}
		else if(t->data.key < key)
		{
		 	removeBST_recursion(t->rchild,key);
		}
		else//找到關鍵字為key的元素 
		{
//			removeNode1(t);//刪除結點*t 
//			removeNode2(t);
			removeNode3(t);
		}
    }
}
注:

(1)由於該函式是遞迴的,且用到了指標引用,我們在前面的刪除給定結點p的函式中,修改p就相當於修改了父節點,這是和C中不同的地方,在C中要達到這種效果,可以使用指向指標的指標變數。

(2)只給出了該函式的遞迴實現,沒有給出非遞迴實現,是為了避免把程式碼弄亂。要實現非遞迴,可以修改前面的刪除結點p的函式,新增一個指向結點p的父節點的指標引用f。同時要修改查詢是否存在關鍵字為key的非遞迴查詢函式,使其在得到p的同時,可以得到其父節點。這樣我們在實現非遞迴時,只需掃描一遍,即呼叫查詢函式,如果有該關鍵字,我們就可以得到指向該結點的指標p和指向結點p的父節點f,然後呼叫刪除給定結點p的函式即可。

4、完整測試程式

#include <cstdlib>
#include <iostream>
#include <string>

using namespace std;

class Student
{
	public:
		int key;
		string major;
		//other data
		Student(int k=int(),string s="") : key(k), major(s){}
		//過載賦值運算子 
		void operator=(const Student& rhs)
	    {
			if(this != &rhs)
			{
				key = rhs.key;
				major = rhs.major;
			}
		}	
};
//過載<<,便於輸出自定義類物件 
ostream& operator<<(ostream &out, const Student& s)
{
	out<<"("<<s.key<<","<<s.major<<")";
}

typedef Student ElementType;
typedef int KeyType;

typedef struct BSTNode
{
	ElementType data;
    struct BSTNode* lchild;
    struct BSTNode* rchild;
}BSTNode, *BST;

	
/*
  description:在以t為根結點的二叉查詢樹中,查詢關鍵字為key的結點
  若查詢成功,指標p指向該結點,返回true,
  否則指向查詢路徑上的最後一個結點並返回false
  指標f指向根結點t的父節點 
*/
//bool searchBST_recursion(BST t, KeyType key, BSTNode* f, BSTNode*& p)
//{
//	if(t == NULL)//查詢失敗 
//	{
//		p = f;
//		return false;
//	}
//	else if(key == t->data.key)//查詢成功 
//	{
//		p = t;
//		return true;
//	}
//	else if(key < t->data.key)
//		return searchBST_recursion(t->lchild,key,t,p);//左子樹中繼續查詢 
//	else 
//		return searchBST_recursion(t->rchild,key,t,p);//右子樹中繼續查詢 
//}

//非遞迴查詢 
bool searchBST(BST t, KeyType key, BSTNode* f, BSTNode* &p)
{
	while(t && key != t->data.key)
	{
		f = t;
		if(key < t->data.key)
		{
			t = t->lchild;
		}
		else
		{
			t = t->rchild;
		}			
	}
	if(t)//查詢成功 
	{
		p = t;
		return true;
	}
	else
	{
		p = f;
		return false;
	}
}

//插入給定元素 
void insertBST(BST& t,ElementType elem)
{
	BSTNode * p = NULL;

	if(!searchBST(t, elem.key, NULL, p))//查詢失敗,不含該關鍵字,可以插入 
	{
		BSTNode * s = new BSTNode;
		s->data = elem;//可能需要過載= 
		s->lchild = NULL;
		s->rchild = NULL;
		
		if(p == NULL)//查詢樹為空 
		{
			t = s;//置s為根結點 
		}
		else if(elem.key < p->data.key)
		{
			p->lchild = s;	//*s為p左結點 
		}
		else
		{
			p->rchild = s;	//*s為p右結點 
		}
	}
}

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,刪除前驅方式 
void removeNode1(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->lchild;		//左子樹根結點
		while(s->rchild)	//尋找結點*p的中序前驅結點,					
		{					//也就是以p->lchild為根結點的子樹中最右的結點 
			q = s;			//*s指向*p的中序前驅 
			s = s->rchild;	//*q指向*s的父節點 
		}
		p->data = s->data;	//*s結點中的資料轉移到*p結點,然後刪除*s
		if(q != p)			//p->lchild右子樹非空 
		{
			q->rchild = s->lchild;//把*s的左子樹接到*q的右子樹上 
		}
		else				//p->lchild右子樹為空 ,此時q ==p 
		{
			q->lchild = s->lchild;//把*s的左子樹接到*q的左子樹上 
		}
		delete s;			//刪除結點*s 
	}
}

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,刪除後繼方式 
void removeNode2(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->rchild;		//右子樹根結點
		while(s->lchild)	//尋找結點*p的中序後繼結點,					
		{					//也就是以p->rchild為根結點的子樹中最左的結點 
			q = s;			//*s指向*p的中序後繼 
			s = s->lchild;	//*q指向*s的父節點 
		}
		p->data = s->data;	//*s結點中的資料轉移到*p結點,然後刪除*s
		if(q != p)			//p->rchild左子樹非空 
		{
			q->lchild = s->rchild;//把*s的右子樹接到*q的左子樹上 
		}
		else				//p->rchild左子樹為空 ,此時q ==p 
		{
			q->rchild = s->rchild;//把*s的右子樹接到*q的右子樹上 
		}
		delete s;			//刪除結點*s 
	}
}

//從二叉查詢樹中刪除指標p所指向的結點 ,p非空,直接刪除p的方式 
void removeNode3(BSTNode *& p)
{
	BSTNode *q = NULL;
	if(!p->rchild)//*p的右子樹為空 
	{
		q = p;
		p = p->lchild;
		delete q;
	}
	else if(!p->lchild)//*p的左子樹為空 
	{
		q = p;
		p = p->rchild;
		delete q;
	}
	else//左右子樹均不空 
	{
		BSTNode *s = NULL;
		q = p;
		s = p->lchild;		//左子樹根結點
		while(s->rchild)	//尋找結點*p的中序前驅結點,					
		{					//也就是以*s為根結點的子樹中最右的結點 
			s = s->rchild;	 
		}
		s->rchild = p->rchild;//*p的右子樹接到*s的右子樹上 
		p = p->lchild;			//*p的左子樹接到父節點上 
		delete q;			//刪除結點*q 
	}
}

//刪除關鍵字為key的元素結點 -遞迴 
void removeBST_recursion(BST& t, KeyType key)
{
	if(t)
	{
		if(key < t->data.key)
		{
			removeBST_recursion(t->lchild,key);
		}
		else if(t->data.key < key)
		{
		 	removeBST_recursion(t->rchild,key);
		}
		else//找到關鍵字為key的元素 
		{
//			removeNode1(t);//刪除結點*t 
//			removeNode2(t);
			removeNode3(t);
		}
    }
}


//輸出二叉查詢樹,中序遞迴 
void printTree(const BST & t)
{
	if(t)
	{
		printTree(t->lchild);
		cout<<t->data<<" ";
		printTree(t->rchild);
	}
}

int main(int argc, char *argv[])
{
	const int N = 10;
	BST root = NULL;
	
	for(int i=1; i<=N; i++)
	{
		Student s(i,"cs");//關鍵字為1-10 
		insertBST(root,s);
	}
	cout<<"after insert: "<<endl;
	printTree(root);
	cout<<endl<<endl;
	
	for(int i=1;i<=N;i+=2)
	{
		removeBST_recursion(root,i);//刪除關鍵字為1-3-5-7-9的結點 
	}
	cout<<"after delete: "<<endl;
	printTree(root);
	cout<<endl<<endl;
	
    system("PAUSE");
    return EXIT_SUCCESS;
}
參考資料:

[1]嚴蔚敏《資料結構(C語言版)》

[2]演算法導論(第2版)