「學習筆記」淺析BST二叉搜索樹


2020-11-12 update:修了一操作的鍋

題目傳送門

Q: 學習二叉搜索樹有什么用?

A: 我們平常所說的"平衡樹"(伸展樹Splay,替罪羊樹等)實際上都屬於"平衡二叉搜索樹",也就是既滿足"平衡樹"又滿足"二叉搜索樹"。二叉搜索樹的效率比平衡二叉搜索樹的效率低很多,但是在學習平衡二叉搜索樹之前也要理解二叉搜索樹的實現原理,此文就是來幫助理解的。

Q: 需要背過代碼嗎?

A: 不需要,相比背過二叉搜索樹,不如多學一兩個平衡樹。


暴力BST最壞時間復雜度是 \(\mathcal{O(n^2)}\)

BST就是二叉搜索樹,這里講的是最普通的BST。


BST(Binary Search Tree),二叉搜索樹,又叫二叉排序樹

是一棵空樹或具有以下幾種性質的樹:

  1. 若左子樹不空,則左子樹上所有結點的值均小於它的根結點的值

  2. 若右子樹不空,則右子樹上所有結點的值均大於它的根結點的值

  3. 左、右子樹也分別為二叉排序樹

  4. 沒有權值相等的結點。

看到第4條,我們會有一個疑問,在數據中遇到多個相等的數該怎么辦呢,顯然我們可以多加一個計數器,就是當前這個值出現了幾遍。

那么我們的每一個節點都包含以下幾個信息:

  1. 當前節點的權值,也就是序列里的數

  2. 左孩子的下標和右孩子的下標,如果沒有則為0

  3. 計數器,代表當前的值出現了幾遍

  4. 子樹大小和自己的大小的和

至於為什么要有4.我們放到后面講。

節點是這樣的:

struct node{
	int val,ls,rs,cnt,siz;
}tree[500010];

其中 \(val\) 是權值,\(ls\) / \(rs\) 是左/右 孩子的下標,\(cnt\) 是當前的權值出現了幾次,\(siz\) 是子樹大小和自己的大小的和。

以下均以遞歸方式呈現。


插入:

\(x\) 是當前節點的下標,\(v\) 是要插入的值。要在樹上插入一個 \(v\) 的值,就要找到一個合適 \(v\) 的位置,如果本身樹的節點內有代表 \(v\) 的值的節點,就把該節點的計數器加 \(1\) ,否則一直向下尋找,直到找到葉子節點,這個時候就可以從這個葉子節點連出一個兒子,代表 \(v\) 的節點。具體向下尋找該走左兒子還是右兒子是根據二叉搜索樹的性質來的。

void add(int x,int v)
{
	tree[x].siz++;
	//如果查到這個節點,說明這個節點的子樹里面肯定是有v的,所以siz++
	if(tree[x].val==v){
		//如果恰好有重復的數,就把cnt++,退出即可,因為我們要滿足第四條性質
		tree[x].cnt++;
		return ;
	}
	if(tree[x].val>v){//如果v<tree[x].val,說明v實在x的左子樹里
		if(tree[x].ls!=0)
		  add(tree[x].ls,v);//如果x有左子樹,就去x的左子樹
		else{//如果不是,v就是x的左子樹的權值
			cont++;//cont是目前BST一共有幾個節點
			tree[cont].val=v;
			tree[cont].siz=tree[cont].cnt=1;
			tree[x].ls=cont;
		}
	}
	else{//右子樹同理
		if(tree[x].rs!=0)
		  add(tree[x].rs,v);
		else{
			cont++;
			tree[cont].val=v;
			tree[cont].siz=tree[cont].cnt=1;
			tree[x].rs=cont;
		}
	}
}

找前驅:

\(x\) 是當前的節點的下標,\(val\) 是要找前驅的值,\(ans\) 是目前找到的比 \(val\) 小的數的最大值。

找前驅的方法也是不斷的在樹上向下爬找具體節點,具體爬的方法可以參考代碼注釋部分。

int queryfr(int x, int val, int ans) {
	if (tree[x].val>=val)
	{//如果當前值大於val,就說明查的數大了,所以要往左子樹找
		if (tree[x].ls==0)//如果沒有左子樹就直接返回找到的ans
			return ans;
		else//如果不是的話,去查左子樹
			return queryfr(tree[x].ls,val,ans);
	}
	else
	{//如果當前值小於val,就說明我們找比val小的了
		if (tree[x].rs==0)//如果沒有右孩子,就返回tree[x].val,因為走到這一步時,我們后找到的一定比先找到的大(參考第二條性質)
			return (tree[x].val<val) ? tree[x].val : ans
		//如果有右孩子,,我們還要找這個節點的右子樹,因為萬一右子樹有比當前節點還大並且小於要找的val的話,ans需要更新
		if (tree[x].cnt!=0)//如果當前節數的個數不為0,ans就可以更新為tree[x].val
			return queryfr(tree[x].rs,val,tree[x].val);
		else//反之ans不需要更新
			return queryfr(tree[x].rs,val,ans);
	}
}

找后繼

與找前驅同理,只不過反過來了,在這里我就不多贅述了。

int queryne(int x, int val, int ans) {
	if (tree[x].val<=val)
	{
		if (tree[x].rs==0)
			return ans;
		else
			return queryne(tree[x].rs,val,ans);
	}
	else
	{
		if (tree[x].ls==0)
			return (tree[x].val>val)? tree[x].val : ans;
		if (tree[x].cnt!=0)
			return queryne(tree[x].ls,val,tree[x].val);
		else
			return queryne(tree[x].ls,val,ans);
	}
}

按值找排名:

這里我們就要用到 \(siz\) 了,排名就是比這個值要小的數的個數再 \(+1\),所以我們按值找排名,就可以看做找比這個值小的數的個數,最后加上 \(1\) 即可。

int queryval(int x,int val)
{
	if(x==0) return 0;//沒有排名 
	if(val==tree[x].val) return tree[tree[x].ls].siz;
	//如果當前節點值=val,則我們加上現在比val小的數的個數,也就是它左子樹的大小 
	if(val<tree[x].val) return queryval(tree[x].ls,val);
	//如果當前節點值比val大了,我們就去它的左子樹找val,因為左子樹的節點值一定是小的 
	return queryval(tree[x].rs,val)+tree[tree[x].ls].siz+tree[x].cnt;
	//如果當前節點值比val小了,我們就去它的右子樹找val,同時加上左子樹的大小和這個節點的值出現次數 
	//因為這個節點的值小於val,這個節點的左子樹的各個節點的值一定也小於val 
}
//注:這里最終返回的是排名-1,也就是比val小的數的個數,在輸出的時候記得+1

按排名找值:

因為性質1和性質2,我們發現排名為 \(n\) 的數在BST上是第 \(n\) 靠左的數。或者說排名為 \(n\) 的數的節點在BST中,它的左子樹的 \(siz\) 與它的各個祖先的左子樹的 \(siz\) 相加恰好 \(=n\) (這里相加是要減去重復部分)。

所以問題又轉化成上一段 或者說 的后面的部分

\(rk\) 是要找的排名

int queryrk(int x,int rk)
{
	if(x==0) return INF; 
	if(tree[tree[x].ls].siz>=rk)//如果左子樹大小>=rk了,就說明答案在左子樹里 
		return queryrk(tree[x].ls,rk);//查左子樹 
	if(tree[tree[x].ls].siz+tree[x].cnt>=rk)//如果左子樹大小加上當前的數的多少恰好>=k,說明我們找到答案了 
		return tree[x].val;//直接返回權值 
	return queryrk(tree[x].rs,rk-tree[tree[x].ls].siz-tree[x].cnt);
	//否則就查右子樹,同時減去當前節點的次數與左子樹的大小 
}

刪除:

具體就是利用二叉搜索樹的性質在樹上向下爬找到具體節點,把計數器-1。與上文同理就不粘貼代碼了


BST的弊端: 時間復雜度最壞為 \(\mathcal{O(n^2)}\)

看完上文,你一定理解了二叉搜索樹的具體實現原理和方法,但是如果構建出的一棵BST是個鏈的話,時間復雜度就會退化到 \(\mathcal{O(n^2)}\) 級別,因為如果每次都查找鏈最低端的葉子節點的復雜度是 \(\mathcal{O(n)}\) 的。而去保持這個樹是個平衡樹,就可以防止出現這個錯誤的復雜度。這個時候就有了平常所說的平衡樹


完整版代碼,僅供參考。

\(\mathcal{Code}:\)

#include<iostream>
#include<cstdio>
#define re register
using namespace std;
const int INF=0x7fffffff;
int cont;
struct node{
    int val,siz,cnt,ls,rs;
}tree[1000010];
int n,opt,xx;
inline void add(int x,int v)
{
    tree[x].siz++;
    if(tree[x].val==v){
        tree[x].cnt++;
        return ;
    }
    if(tree[x].val>v){
        if(tree[x].ls!=0)
          add(tree[x].ls,v);
        else{
            cont++;
            tree[cont].val=v;
            tree[cont].siz=tree[cont].cnt=1;
            tree[x].ls=cont;
        }
    }
    else{
        if(tree[x].rs!=0)
          add(tree[x].rs,v);
        else{
            cont++;
            tree[cont].val=v;
            tree[cont].siz=tree[cont].cnt=1;
            tree[x].rs=cont;
        }
    }
}
int queryfr(int x, int val, int ans) {
    if (tree[x].val>=val)
    {
        if (tree[x].ls==0)
            return ans;
        else
            return queryfr(tree[x].ls,val,ans);
    }
    else
    {
        if (tree[x].rs==0)
            return tree[x].val;
        return queryfr(tree[x].rs,val,tree[x].val);
    }
}
int queryne(int x, int val, int ans) {
    if (tree[x].val<=val)
    {
        if (tree[x].rs==0)
            return ans;
        else
            return queryne(tree[x].rs,val,ans);
    }
    else
    {
        if (tree[x].ls==0)
            return tree[x].val;
        return queryne(tree[x].ls,val,tree[x].val);
    }
}
int queryrk(int x,int rk)
{
    if(x==0) return INF;
    if(tree[tree[x].ls].siz>=rk)
        return queryrk(tree[x].ls,rk);
    if(tree[tree[x].ls].siz+tree[x].cnt>=rk)
        return tree[x].val;
    return queryrk(tree[x].rs,rk-tree[tree[x].ls].siz-tree[x].cnt);
}
int queryval(int x,int val)
{
    if(x==0) return 0;
    if(val==tree[x].val) return tree[tree[x].ls].siz;
    if(val<tree[x].val) return queryval(tree[x].ls,val);
    return queryval(tree[x].rs,val)+tree[tree[x].ls].siz+tree[x].cnt;
}
inline int read()
{
    re int r=0;
    re char ch=getchar();
    while(ch<'0'||ch>'9')
        ch=getchar();
    while(ch>='0'&&ch<='9'){
        r=(r<<3)+(r<<1)+(ch^48);
        ch=getchar();
    }
    return r;
}
signed main()
{
    n=read();
    while(n--){
        opt=read();xx=read();
        if(opt==1) printf("%d\n",queryval(1,xx)+1);
        else if(opt==2) printf("%d\n",queryrk(1,xx));
        else if(opt==3) printf("%d\n",queryfr(1,xx,-INF));
        else if(opt==4) printf("%d\n",queryne(1,xx,INF));
        else{
            if(cont==0){
                cont++;
                tree[cont].cnt=tree[cont].siz=1;
                tree[cont].val=xx;
            }
            else add(1,xx);
        }
    }
    return 0;
}

相信你已經掌握了二叉搜索樹的基本實現方法,也可以來嘗試循環實現的BST:

#include<iostream>
#include<cstdio>
#include<vector>
#define pb push_back
const int N = 10010;
const int INF = 0x7fffffff;
inline int read() {
	int r = 0; bool w = 0; char ch = getchar();
	while(ch < '0' || ch > '9') w = ch == '-' ? 1 : w, ch = getchar();
	while(ch >= '0' && ch <= '9') r = (r << 3) + (r << 1) + (ch ^ 48), ch = getchar();
	return w ? ~r + 1 : r;
}
#define ls tree[x].son[0]
#define rs tree[x].son[1]
struct Node {
	int val, siz, cnt, son[2];
}tree[N];
int n, root, tot;
inline void add(int v) {
	if(!tot) {
		root = ++tot;
		tree[tot].cnt = tree[tot].siz = 1;
		tree[tot].son[0] = tree[tot].son[1] = 0;
		tree[tot].val = v;
		return ;
	}
	int x = root, last = 0;
	do {
		++tree[x].siz;
		if(tree[x].val == v) {
			++tree[x].cnt;
			break;
		}
		last = x;
		x = tree[last].son[v > tree[last].val];
		if(!x) {
			tree[last].son[v > tree[last].val] = ++tot;
			tree[tot].son[0] = tree[tot].son[1] = 0;
			tree[tot].val = v;
			tree[tot].cnt = tree[tot].siz = 1;
			break;
		}
	} while(true);//Code by do_while_true qwq
}
int queryfr(int val) {
	int x = root, ans = -INF;
	do {
		if(x == 0) return ans;
		if(tree[x].val >= val) {
			if(ls == 0) return ans;
			x = ls;
		}
		else {
			if(rs == 0) return tree[x].val;
			ans = tree[x].val;
			x = rs;
		}
	} while(true);
}
int queryne(int v) {
	int x = root, ans = INF;
	do {
		if(x == 0) return ans;
		if(tree[x].val <= v) {
			if(rs == 0) return ans;
			x = rs;
		}
		else {
			if(ls == 0) return tree[x].val;
			ans = tree[x].val;
			x = ls;
		}
	} while(true);
}
int queryrk(int rk) {
	int x = root;
	do {
		if(x == 0) return INF;
		if(tree[ls].siz >= rk) x = ls;
		else if(tree[ls].siz + tree[x].cnt >= rk) return tree[x].val;
		else rk -= tree[ls].siz + tree[x].cnt, x = rs;
	} while(true);
}
int queryval(int v) {
	int x = root, ans = 0;
	do {
		if(x == 0) return ans;
		if(tree[x].val == v) return ans + tree[ls].siz;
		else if(tree[x].val > v) x = ls;
		else ans += tree[ls].siz + tree[x].cnt, x = rs;
	} while(true);
}
int main() {
	n = read();
	while(n--) {
		int opt = read(), x = read();
		if(opt == 1) printf("%d\n", queryval(x) + 1);
		if(opt == 2) printf("%d\n", queryrk(x));
		if(opt == 3) printf("%d\n", queryfr(x));
		if(opt == 4) printf("%d\n", queryne(x));
		if(opt == 5) add(x);
	}
	return 0;
}


免責聲明!

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



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