二叉搜索樹學習筆記


一. 定義

  • 二叉搜索樹,是指具有如下性質(稱作”BST”性質)的二叉樹:
  • 給定一棵二叉樹,每個結點帶有一個數值,稱作這個結點的“關鍵碼”(或”關鍵字”、”鍵值”等,英文是”key”)
  • BST性質:對於樹中的任意結點,滿足以下兩條性質
    • 它的關鍵碼不小於左子樹中任何結點的關鍵碼
    • 它的關鍵碼不大於右子樹中任何結點的關鍵碼

二. 支持的操作

  1. insert():新增一個關鍵碼為 \(val\) 的結點
  2. get():查找關鍵碼為 \(val\) 的結點
  3. getnext():查找 \(val\) 的后繼
  4. getpre():查找 \(val\) 的前驅
  5. remove():刪除 \(val\) 的結點
  6. getrank():查找 \(val\) 的排名
  7. getkth():查找第 \(k\) 大的 \(val\)

三. 二叉搜索樹的存儲與初始化

  1. 存儲:
const int MAXLEN=100000;
struct NODE 
{
  int l,r;//左右孩子編號,0代表孩子不存在
  int val;//關鍵碼
}tree[MAXLEN+2];
int tot;//當前結點總數
int root;//根結點下標
  1. 初始化
  • 為了避免越界,減少邊界情況的特殊判斷,一般在BST中額外插入一個關鍵碼為正無窮和一個關鍵碼為負無窮的節點。僅由這兩個節點構成的BST就是一棵初始的空BST。
int newnode(int val)//新建一個節點,返回其編號
{
	tree[++tot].val=val;
	tree[tot].l=tree[tot].r=0;
	return tot;
}
void build()//建樹
{
	newnode(-INF);
	newnode(INF);
	root=1;
	tree[1].r=2;
}

四. 二叉搜索樹樹的檢索

  • \(BST\) 中檢索是否存在關鍵碼為 \(val\) 的節點。

  • 設變量 \(p\) 等於根節點 \(root\),執行以下過程:

    • \(p\) 的關鍵碼等於 \(val\),則已經找到
    • \(p\) 的關鍵碼大於 \(val\)
    • \(p\) 的左子節點為空,則說明不存在 \(val\)
    • \(p\) 的左子節點不空,在 \(p\) 的左子樹中遞歸進行檢索
  • \(p\) 的關鍵碼小於 \(val\)

    • \(p\) 的右子節點為空,則說明不存在 \(val\)
    • \(p\) 的右子節點不空,在 \(p\) 的右子樹中遞歸進行檢索

//在以tree[p]為根的子樹中查找val
//主函數調用get(root,val)
int get(int p,int val)
{
	if(p==0) return 0;
	if(tree[p].val==val) return p;
	else if(tree[p].val>val) return get(tree[p].l,val);
	else return get(tree[p].r,val);
}

五. 二叉搜索樹的插入(新增)

  • 在BST中插入一個新的值 \(val\)(假設目前BST中不存在關鍵碼為 \(val\) 的節點,若存在則不插入),與BST的檢索過程類似。
  • 在發現要走向的 \(p\) 的子節點為空,說明 \(val\) 不存在時,直接建立關鍵碼為 \(val\) 的新節點作為 \(p\) 的子節點
  • 例如插入 \(3\)\(8\)
//在以tree[p]為根的子樹中插入val
//主函數調用insert(root,val)
void insert(int& p,int val)
{
	if(p==0)
	{
		p=newnode(val);//注意p是引用,其父節點的l或r會被同時更新

		return ;
	}
	if(tree[p].val==val) return ;
	if(tree[p].val>val) insert(tree[p].l,val);
	else insert(tree[p].r,val);	
}

六. 二叉查找樹的找最小最大

  • 任意子樹中的最小值,是其左鏈的頂點

  • 任意子樹中的最大值,是其右鏈的頂點

////在以tree[p]為根的子樹中找最小值結點
int getmin(int p)
{
	if(tree[p].l==0) return p;
	return getmin(tree[p].l);
}
////在以tree[p]為根的子樹中找最大值結點
int getmax(int p)
{
	if(tree[p].r==0) return p;
	return getmax(tree[p].r);
}

七. 二叉查找樹中的前驅與后繼

后繼:

  • 如果BST中存在val
    • 如果val有右子樹,那么后繼是val右子樹的最小值結點
    • 如果val沒有右子樹,那么后繼是val所有祖先結點中大於val的最小值結點
  • 如果BST中不存在val
    • 后繼是查找val的路徑上的所有結點中大於val的最小值結點

證明:

  • 首先后繼不可能在 \(val\) 左子樹
  • 如果后繼不在 \(val\) 左右子樹、也不在 \(val~root\) 的路徑上,那么
    • \(s\)\(val\) 的后繼,考慮 \(s\)\(val\) 的最近公共祖先 \(a\)
    • 因為 \(a\)\(s\)\(val\) 最近公共祖先,所以 \(s\)\(val\) 分別屬於 \(a\) 左右子樹
    • 如果 \(s\)\(val\) 右,\(s\) 不會比 \(val\) 大,不可能是后繼
    • 如果 \(s\)\(val\) 左,那 \(a\)\(s\) 小,比 \(val\) 大,\(s\) 也不可能是 \(val\) 后繼
  • 最后證明當 \(val\) 有右子樹時,后繼結點一定是右子樹的最小值結點
    • 因為祖先結點中所有大於 \(val\) 的結點也都大於它
int getnext(int val)
{
	int p=root;
	int ans=2;//tree[2]是INF
	while(p!=0)
	{
		if(tree[p].val==val)//BST中存在val
		{
			if(tree[p].r!=0)//val有右子樹
			{
				p=tree[p].r;
				while(tree[p].l!=0) p=tree[p].l;
				return p;
			}
			break;//val 沒有右子樹,此時答案已經在ans中
		}
                //路徑經過的結點都檢查一遍
		if(tree[p].val>val and tree[p].val<tree[ans].val) ans=p;
		if(tree[p].val>val) p=tree[p].l;
		else p=tree[p].r;
	}
	return ans;//BST中沒有val,此時答案以在 ans 中
	
}

同理可得前驅。

  • 如果 \(BST\) 中存在 \(val\)
    • 如果 \(val\) 有左子樹,那么前驅是 \(val\) 左子樹的最大值結點
    • 如果 \(val\) 沒有左子樹,那么前驅是 \(val\) 所有祖先結點中小於 \(val\) 的最大值結點
  • 如果 \(BST\) 中不存在 \(val\)
    • 后繼是查找 \(val\) 的路徑上的所有結點中小於 \(val\) 的最大值結點
int getpre(int val)
{
	int p=root;
	int ans=1;
	while(p!=0)
	{
		if(tree[p].val==val)
		{
			if(tree[p].l!=0)
			{
				p=tree[p].l;
				while(tree[p].r!=0) p=tree[p].r;
				return p;
			}
			break;
		}
		if(tree[p].val<val and tree[p].val>tree[ans].val) ans=p;
		if(tree[p].val>val) p=tree[p].l;
		else p=tree[p].r;
	}
	return ans;
}

七. 二叉查找樹的刪除

  • 從BST中刪除關鍵碼為 \(val\) 的節點
  • 首先,在 \(BST\) 中檢索 \(val\) ,得到節點 \(p\)
    • \(p\) 的子節點個數小於 \(2\),則直接刪除 \(p\),並令 \(p\) 的子節點代替 \(p\) 的位置,與 \(p\) 的父節點相連。
    • \(p\) 既有左子樹又有右子樹,則在 \(BST\) 中求出 \(val\) 的后繼節點 \(next\) 。因為 \(next\) 沒有左子樹,所以可以直接刪除 \(next\),並令 \(next\) 的右子樹代替 \(next\) 的位置。最后,再讓\(next\) 節點代替 \(p\) 節點,刪除 \(p\) 即可。

void remove(int &p, int val) { 
    if (p == 0) return;
    if (val == tree[p].val) { // 已經檢索到值為val的節點
        if (tree[p].l == 0) { // 沒有左子樹
            p = tree[p].r; // 右子樹代替p的位置,注意p是引用
        }
        else if (tree[p].r == 0) { // 沒有右子樹
            p = tree[p].l; // 左子樹代替p的位置,注意p是引用
        }
        else { // 既有左子樹又有右子樹
            // 求后繼節點
            int next = tree[p].r;
            while (tree[next].l > 0) next = tree[next].l;
            // next一定沒有左子樹,直接刪除
            remove(tree[p].r, tree[next].val);
            // 令節點next代替節點p的位置
            tree[next].l = tree[p].l;
            tree[next].r = tree[p].r;
            p = next; // 注意p是引用
        }
        return;
    }
    if (val < tree[p].val) {
        remove(tree[p].l, val);
    } else {
        remove(tree[p].r, val);
    }
}

八. 二叉查找樹的效率

  • 二叉查找樹的操作都和樹的高度 \(h\) 有關,是 \(O(h)\)
  • 新增元素可能會增加 \(BST\) 的高度
  • 如果新增順序比較隨機,\(BST\) 期望高度是 \(O(logn)\)
  • 如果按從小到大(或從大到小)順序新增元素,\(BST\) 高度可能退化到 \(O(n)\)
  • 某些數據結構可以在新增元素時能保證高度維持在 \(O(logn)\) 級別,這樣的數據結構叫做平衡二叉樹。

以上就是二叉查找樹的基本操作,但我們仍有很多問題沒解決,因此我們考慮擴展 \(BST\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM