掉一根头发,搞定二叉排序(搜索)树
文章已收錄在 數(shù)據(jù)結(jié)構(gòu)與算法學(xué)習(xí)倉(cāng)庫(kù)
前言
前面介紹學(xué)習(xí)的大多是線(xiàn)性表相關(guān)的內(nèi)容,把指針搞懂后其實(shí)也沒(méi)有什么難度,規(guī)則相對(duì)是簡(jiǎn)單的,后面會(huì)講解一些比較常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),用多圖的方式讓大家更容易吸收。
在數(shù)據(jù)結(jié)構(gòu)與算法中,樹(shù)是一個(gè)比較大的家族,家族中有很多厲害的成員,這些成員有二叉樹(shù)和多叉樹(shù)(例如B+樹(shù)等),而二叉樹(shù)的大家族中,二叉搜索樹(shù)(又稱(chēng)二叉排序樹(shù))是最最基礎(chǔ)的,在這基礎(chǔ)上才能繼續(xù)拓展學(xué)習(xí)AVL(二叉平衡樹(shù))、紅黑樹(shù)等知識(shí)。
對(duì)于二叉排序樹(shù)而言,本章重點(diǎn)關(guān)注其實(shí)現(xiàn)方式以及插入、刪除步驟流程,我們會(huì)手寫(xiě)一個(gè)二叉排序樹(shù),二叉樹(shù)遍歷部分的內(nèi)容比較多會(huì)單獨(dú)詳細(xì)講解。
什么是樹(shù)
樹(shù)是一種數(shù)據(jù)結(jié)構(gòu),它是由n(n>=1)個(gè)有限結(jié)點(diǎn)組成一個(gè)具有層次關(guān)系的集合。把它叫做“樹(shù)”是因?yàn)樗雌饋?lái)像一棵倒掛的樹(shù),也就是說(shuō)它是根朝上,而葉朝下的。
樹(shù)是遞歸的,將樹(shù)的任何一個(gè)節(jié)點(diǎn)以及節(jié)點(diǎn)下的節(jié)點(diǎn)都能組合成一個(gè)新的樹(shù),所以樹(shù)的很多問(wèn)題都是使用遞歸去完成。
根節(jié)點(diǎn): 最上面的那個(gè)節(jié)點(diǎn)(root),根節(jié)點(diǎn)沒(méi)有父節(jié)點(diǎn),只有子節(jié)點(diǎn)(0個(gè)或多個(gè)都可以)
層數(shù): 一般認(rèn)為根節(jié)點(diǎn)是第1層(有的也說(shuō)第0層),而樹(shù)的高度就是層數(shù)最高(上圖層數(shù)開(kāi)始為1)節(jié)點(diǎn)的層數(shù)
節(jié)點(diǎn)關(guān)系:
- 父節(jié)點(diǎn):連接該節(jié)點(diǎn)的上一層節(jié)點(diǎn),
- 孩子節(jié)點(diǎn): 和父節(jié)點(diǎn)對(duì)應(yīng),上下關(guān)系。而祖先節(jié)點(diǎn)是父節(jié)點(diǎn)的父節(jié)點(diǎn)(或者祖先)節(jié)點(diǎn)。
- 兄弟節(jié)點(diǎn):擁有同一個(gè)父節(jié)點(diǎn)的節(jié)點(diǎn)們!
節(jié)點(diǎn)的度: 就是節(jié)點(diǎn)擁有孩子節(jié)點(diǎn)的個(gè)數(shù)(是直接連接的孩子不是子孫).
樹(shù)的度: 就是所有節(jié)點(diǎn)中最大 (節(jié)點(diǎn)的度)。同時(shí),如果度大于0的節(jié)點(diǎn)是分支節(jié)點(diǎn),度等于0的節(jié)點(diǎn)是葉子節(jié)點(diǎn)(沒(méi)有子孫)。
相關(guān)性質(zhì):
二叉樹(shù)
二叉樹(shù)是一樹(shù)的一種,但應(yīng)用比較多,所以需要深入學(xué)習(xí),二叉樹(shù)的每個(gè)節(jié)點(diǎn)最多只有兩個(gè)子節(jié)點(diǎn)(但不一定非得要有兩個(gè)節(jié)點(diǎn))。
二叉樹(shù)與度為2的樹(shù)的區(qū)別:
1、度為2的的樹(shù)必須有三個(gè)節(jié)點(diǎn)以上(否則就不叫度為二了,一定要先存在),二叉樹(shù)可以為空。
2、二叉樹(shù)的度不一定為2,比如斜樹(shù)。
3、二叉樹(shù)有左右節(jié)點(diǎn)區(qū)分,而度為2的樹(shù)沒(méi)有左右節(jié)點(diǎn)的區(qū)分。
幾種特殊二叉樹(shù):
滿(mǎn)二叉樹(shù):高度為n的滿(mǎn)二叉樹(shù)有(2^n) -1個(gè)節(jié)點(diǎn)
完全二叉樹(shù):上面一層全部滿(mǎn),最下一層從左到右順序排列
二叉排序樹(shù):樹(shù)按照一定規(guī)則插入排序(本文詳解)。
平衡二叉樹(shù):樹(shù)上任意節(jié)點(diǎn)左子樹(shù)和右子樹(shù)深度差距不超過(guò)1(后文詳解).
二叉樹(shù)性質(zhì):
1、二叉樹(shù)有用樹(shù)的性質(zhì)
2、非空二叉樹(shù)葉子節(jié)點(diǎn)數(shù)=度為2的節(jié)點(diǎn)數(shù)+1.本來(lái)一個(gè)節(jié)點(diǎn)如果度為1.那么一直延續(xù)就一個(gè)葉子,但如果出現(xiàn)一個(gè)度為2除了延續(xù)原來(lái)的一個(gè)節(jié)點(diǎn),會(huì)多出一個(gè)節(jié)點(diǎn)需要維系。所以到最后會(huì)多出一個(gè)葉子。
3、非空第i層最多有2^(i-1)個(gè)節(jié)點(diǎn)。
4、高為h的樹(shù)最多有(2^h)-1個(gè)節(jié)點(diǎn)(等比求和)。
二叉樹(shù)一般用鏈?zhǔn)酱鎯?chǔ),這樣內(nèi)存利用更高,但二叉樹(shù)也可以用數(shù)組存儲(chǔ)的(經(jīng)常會(huì)遇到),各個(gè)節(jié)點(diǎn)對(duì)應(yīng)的下標(biāo)是可以計(jì)算出來(lái)的,就拿一個(gè)完全二叉樹(shù)若從左往右,從上到下編號(hào)如圖:
二叉排序(搜索)樹(shù)
概念
前面鋪墊那么多,咱們言歸正傳,詳細(xì)講解并實(shí)現(xiàn)一個(gè)二叉排序樹(shù),二叉搜索樹(shù)擁有二叉樹(shù)的性質(zhì),同時(shí)有一些自己的規(guī)則:
首先要了解二叉排序樹(shù)的規(guī)則:從任意節(jié)點(diǎn)開(kāi)始,節(jié)點(diǎn)左側(cè)節(jié)點(diǎn)值總比節(jié)點(diǎn)右側(cè)值要小。
例如一個(gè)二叉排序樹(shù)依次插入15,6,23,7,4,71,5,50會(huì)形成下圖順序
構(gòu)造
二叉排序樹(shù)是由若干節(jié)點(diǎn)(node)構(gòu)成的,對(duì)于node需要這些屬性:left,right,和value。其中l(wèi)eft和right是左右指針指向左右孩子子樹(shù),而value是儲(chǔ)存的數(shù)據(jù),這里用int 類(lèi)型。
node類(lèi)構(gòu)造為:
class node {//結(jié)點(diǎn)public int value;public node left;public node right;public node(){}public node(int value){this.value=value;this.left=null;this.right=null;}public node(int value,node l,node r){this.value=value;this.left=l;this.right=r;} }既然節(jié)點(diǎn)構(gòu)造好了,那么就需要節(jié)點(diǎn)等其他信息構(gòu)造成樹(shù),有了鏈表構(gòu)造經(jīng)驗(yàn),很容易得知一棵樹(shù)最主要的還是root根節(jié)點(diǎn)。
所以樹(shù)的構(gòu)造為:
主要方法
既然已經(jīng)構(gòu)造好一棵樹(shù),那么就需要實(shí)現(xiàn)主要的方法,因?yàn)槎媾判驑?shù)中每個(gè)節(jié)點(diǎn)都能看作一棵樹(shù)。所以我們創(chuàng)建方法的是時(shí)候加上節(jié)點(diǎn)參數(shù)(方便一些遞歸調(diào)用)
findmax(),findmin()
findmin()找到最小節(jié)點(diǎn):
因?yàn)樗泄?jié)點(diǎn)的最小都是往左插入,所以只需要找到最左側(cè)的返回即可,具體實(shí)現(xiàn)可使用遞歸也可非遞歸while循環(huán)。
findmax()找到最大節(jié)點(diǎn):
因?yàn)樗泄?jié)點(diǎn)大的都是往右面插入,所以只需要找到最右側(cè)的返回即可,實(shí)現(xiàn)方法與findmin()方法一致。
代碼使用遞歸函數(shù)
public node findmin(node t)//查找最小返回值是node,調(diào)用查看結(jié)果時(shí)需要.value {if(t==null) {return null;}else if(t.left==null) {return t;}else return(findmin(t.left)); } public node findmax(node t)//查找最大 {if(t==null) {return null;}else if(t.right==null) {return t;}else return(findmax(t.right)); }isContains(int x)
這里的意思是查找二叉查找樹(shù)中是否存在值為x的節(jié)點(diǎn)。
在具體實(shí)現(xiàn)上,根據(jù)二叉排序樹(shù)左側(cè)更小,右側(cè)更大的性質(zhì)進(jìn)行往下查找,如果找到值為x的節(jié)點(diǎn)則返回true,如果找不到就返回false,當(dāng)然實(shí)現(xiàn)上可以采用遞歸或者非遞歸,我這里使用非遞歸的方式。
public boolean isContains(int x)//是否存在 {node current=root;if(root==null) {return false;}while(current.value!=x&¤t!=null) {if(x<current.value) {current=current.left;}if(x>current.value) {current=current.right;}if(current==null) {return false;}//在里面判斷如果超直接返回}//如果在這個(gè)位置判斷是否為空會(huì)導(dǎo)致current.value不存在報(bào)錯(cuò)if(current.value==x) {return true;} return false; }insert(int x)
插入的思想和前面isContains(int x)類(lèi)似,找到自己的位置(空位置)插入。
但是具體實(shí)現(xiàn)上有需要注意的地方,我們要到待插入位置上一層節(jié)點(diǎn),你可能會(huì)疑問(wèn)為什么不直接找到最后一個(gè)空,然后將current賦值過(guò)去current=new node(x),這樣的化current就相當(dāng)于指向一個(gè)new node(x)節(jié)點(diǎn),和原來(lái)樹(shù)就脫離關(guān)系(原樹(shù)相當(dāng)于沒(méi)有任何操作),所以要提前通過(guò)父節(jié)點(diǎn)判定是否為空找到位置,找到合適位置通過(guò)父節(jié)點(diǎn)的left或者right節(jié)點(diǎn)指向新創(chuàng)建的節(jié)點(diǎn)才能完成插入的操作。
public node insert(int x)// 插入 t是root的引用 {node current = root;if (root == null) {root = new node(x);return root;}while (current != null) {if (x < current.value) {if (current.left == null) {return current.left = new node(x);}else current = current.left;}else if (x > current.value) {if (current.right == null) {return current.right = new node(x);}else current = current.right;}}return current;//其中用不到 }比如說(shuō)上面樹(shù)插入值為51的節(jié)點(diǎn)。
delete(int x)
刪除操作算是一個(gè)相對(duì)較難理解的操作了,因?yàn)榇齽h除的點(diǎn)可能在不同位置所以具體處理的方式也不同,如果是葉子即可可直接刪除,有一個(gè)孩子節(jié)點(diǎn)用子節(jié)點(diǎn)替換即可,有兩個(gè)子節(jié)點(diǎn)的就要先找到值距離待刪除節(jié)點(diǎn)最近的點(diǎn)(左子樹(shù)最大點(diǎn)或者右子樹(shù)最小點(diǎn)),將值替換掉然后遞歸操作在子樹(shù)中刪除已經(jīng)替換的節(jié)點(diǎn),當(dāng)然沒(méi)具體分析可以看下面:
刪除的節(jié)點(diǎn)沒(méi)有子孫:
這種情況不需要考慮,直接刪除即可(節(jié)點(diǎn)=null即可)(圖中紅色點(diǎn)均滿(mǎn)足這種方式)。
一個(gè)子節(jié)點(diǎn)為空:
此種情況也很容易,直接將刪除點(diǎn)的子節(jié)點(diǎn)放到被刪除位置即可。
左右節(jié)點(diǎn)均不空
左右孩子節(jié)點(diǎn)都不為空這種情況是相對(duì)比較復(fù)雜的,因?yàn)椴荒苤苯佑闷渲幸粋€(gè)孩子節(jié)點(diǎn)替代當(dāng)前節(jié)點(diǎn)(放不下,如果孩子節(jié)點(diǎn)也有兩個(gè)孩子那么有一個(gè)節(jié)點(diǎn)無(wú)法放,例如拿下面71節(jié)點(diǎn)替代)
如果拿19或者71節(jié)點(diǎn)填補(bǔ)。雖然可以保證部分側(cè)大于小于該節(jié)點(diǎn),但是會(huì)引起合并的混亂.比如你若用71替代23節(jié)點(diǎn)。那么你需要考慮三個(gè)節(jié)點(diǎn)(19,50,75)之間如何處理,還要考慮他們是否滿(mǎn),是否有子女,這是個(gè)復(fù)雜的過(guò)程,不適合考慮。
所以,我們要分析我們要的這個(gè)點(diǎn)的屬性:能夠保證該點(diǎn)在這個(gè)位置仍滿(mǎn)足二叉搜索樹(shù)的性質(zhì)(找到值最近的),那么子樹(shù)中哪個(gè)節(jié)點(diǎn)滿(mǎn)足這樣的關(guān)系呢?
左子樹(shù)中最右側(cè)節(jié)點(diǎn)或者右子樹(shù)中最左側(cè)節(jié)點(diǎn)都滿(mǎn)足,我們可以選一個(gè)節(jié)點(diǎn)將待刪除節(jié)點(diǎn)值替換掉(這里替換成左子樹(shù)最右側(cè)節(jié)點(diǎn))。
這個(gè)點(diǎn)替換之后該怎么辦呢?很簡(jiǎn)單啊,二叉樹(shù)用遞歸思路解決問(wèn)題,再次調(diào)用刪除函數(shù)在左子樹(shù)中刪除替換的節(jié)點(diǎn)即可。
這里演示是選取左子樹(shù)最大節(jié)點(diǎn)(最右側(cè))替代,當(dāng)然使用右子樹(shù)最小節(jié)點(diǎn)也能滿(mǎn)足在這待刪除的大小關(guān)系,原理一致。整個(gè)刪除算法流程為:
這部分操作的代碼為:
public node remove(int x, node t)// 刪除節(jié)點(diǎn) {if (t == null) {return null;}if (x < t.value) {t.left = remove(x, t.left);} else if (x > t.value) {t.right = remove(x, t.right);} else if (t.left != null && t.right != null)// 左右節(jié)點(diǎn)均不空{t.value = findmin(t.right).value;// 找到右側(cè)最小值替代t.right = remove(t.value, t.right);} else // 左右單空或者左右都空{if (t.left == null && t.right == null) {t = null;} else if (t.right != null) {t = t.right;} else if (t.left != null) {t = t.left;}return t;}return t; }完整代碼
這個(gè)完整代碼是筆者在大三時(shí)候?qū)懙?#xff0c;可能有不少疏漏或者不規(guī)范的地方,僅供學(xué)習(xí)參考,如有疏漏錯(cuò)誤還請(qǐng)指正。
二叉排序樹(shù)完整代碼為:
package 二叉樹(shù);import java.util.ArrayDeque; import java.util.Queue; import java.util.Stack;public class BinarySortTree {class node {// 結(jié)點(diǎn)public int value;public node left;public node right;public node() {}public node(int value) {this.value = value;this.left = null;this.right = null;}public node(int value, node l, node r) {this.value = value;this.left = l;this.right = r;}}node root;// 根public BinarySortTree() {root = null;}public void makeEmpty()// 變空{root = null;}public boolean isEmpty()// 查看是否為空{return root == null;}public node findmin(node t)// 查找最小返回值是node,調(diào)用查看結(jié)果時(shí)需要.value{if (t == null) {return null;} else if (t.left == null) {return t;} elsereturn (findmin(t.left));}public node findmax(node t)// 查找最大{if (t == null) {return null;} else if (t.right == null) {return t;} elsereturn (findmax(t.right));}public boolean isContains(int x)// 是否存在{node current = root;if (root == null) {return false;}while (current.value != x && current != null) {if (x < current.value) {current = current.left;}if (x > current.value) {current = current.right;}if (current == null) {return false;} // 在里面判斷如果超直接返回}// 如果在這個(gè)位置判斷是否為空會(huì)導(dǎo)致current.value不存在報(bào)錯(cuò)if (current.value == x) {return true;}return false;}public node insert(int x)// 插入 t是root的引用{node current = root;if (root == null) {root = new node(x);return root;}while (current != null) {if (x < current.value) {if (current.left == null) {return current.left = new node(x);}else current = current.left;}else if (x > current.value) {if (current.right == null) {return current.right = new node(x);}else current = current.right;}}return current;//其中用不到}public node remove(int x, node t)// 刪除節(jié)點(diǎn){if (t == null) {return null;}if (x < t.value) {t.left = remove(x, t.left);} else if (x > t.value) {t.right = remove(x, t.right);} else if (t.left != null && t.right != null)// 左右節(jié)點(diǎn)均不空{t.value = findmin(t.right).value;// 找到右側(cè)最小值替代t.right = remove(t.value, t.right);} else // 左右單空或者左右都空{if (t.left == null && t.right == null) {t = null;} else if (t.right != null) {t = t.right;} else if (t.left != null) {t = t.left;}return t;}return t;} }結(jié)語(yǔ)
這里我們學(xué)習(xí)了解了樹(shù)、二叉樹(shù)、以及二叉搜素樹(shù),對(duì)于二叉搜素樹(shù)插入查找比較容易理解,但是實(shí)現(xiàn)的時(shí)候要注意函數(shù)參數(shù)的引用等等。
偏有難度的是二叉樹(shù)的刪除,利用一個(gè)遞歸的思想,分類(lèi)討論待刪除情況,要找到特殊情況和普通情況,遞歸一定程度也是問(wèn)題的轉(zhuǎn)化(轉(zhuǎn)成自己相同問(wèn)題,作用域減小)需要思考。
下面還會(huì)介紹二叉樹(shù)的三序遍歷(遞歸和非遞歸)和層序遍歷。這些都是比較經(jīng)典熱門(mén)的問(wèn)題需要深入了解。
如果看了本文覺(jué)得有收獲歡迎 點(diǎn)贊、收藏、分享一波,也歡迎加我v信好友(q1315426911)一起學(xué)習(xí)交流,我也創(chuàng)了一個(gè)力扣打卡群,里面很多熱情的伙伴希望一起進(jìn)步!
總結(jié)
以上是生活随笔為你收集整理的掉一根头发,搞定二叉排序(搜索)树的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 备战实习求职的一些感想(已拿阿里offe
- 下一篇: 结合大学四年经验,带你揭秘高效自学Jav