<ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>



      看官,不要生氣,我沒有罵你也沒有鄙視你的意思,今天就是想單純的給大伙分享一下樹的相關(guān)知識(shí),但是我還是想說作為一名程序員,自己心里有沒有點(diǎn)樹?你會(huì)沒點(diǎn)數(shù)嗎?言歸正傳,樹是我們常用的數(shù)據(jù)結(jié)構(gòu)之一,樹的種類很多有二叉樹、二叉查找樹、平衡二叉樹、紅黑樹、B樹、B+樹等等,我們今天就來聊聊二叉樹相關(guān)的樹。

      什么是樹?


      首先我們要知道什么是樹?我們平常中的樹是往上長有分支的而卻不會(huì)形成閉環(huán),數(shù)據(jù)結(jié)構(gòu)中的樹跟我們我們平時(shí)看到的樹類似,確切的說是跟樹根長得類似,我畫了一幅圖,讓大家更好的理解樹。



      圖1、圖2都是樹,圖3不是樹。每個(gè)紅色的圓圈我們稱之為元素也叫節(jié)點(diǎn),用線將兩個(gè)節(jié)點(diǎn)連接起來,這兩個(gè)節(jié)點(diǎn)就形成了父子關(guān)系,同一個(gè)父節(jié)點(diǎn)的子節(jié)點(diǎn)成為兄弟節(jié)點(diǎn),這跟我們家族關(guān)系一樣,同一個(gè)父親的叫做兄弟姐妹,在家族里面最大的稱為老子,樹里面也是一樣的,只是不叫老子,叫做跟節(jié)點(diǎn),沒有子節(jié)點(diǎn)的叫做葉子節(jié)點(diǎn)。我們拿圖1來做示例,A為根節(jié)點(diǎn),B、C為兄弟節(jié)點(diǎn),E、F為葉子節(jié)點(diǎn)。

      一顆樹還會(huì)涉及到三個(gè)概念高度、深度、層,我們先來看看這三個(gè)名詞的定義:

      高度:節(jié)點(diǎn)到葉子節(jié)點(diǎn)的最長路徑,從0開始計(jì)數(shù)

      深度:跟節(jié)點(diǎn)到這個(gè)節(jié)點(diǎn)所經(jīng)歷的邊數(shù),從0開始計(jì)數(shù)

      層:節(jié)點(diǎn)距離根節(jié)點(diǎn)的距離,從1開始計(jì)數(shù)

      知道了三個(gè)名詞的概念之后,我們用一張圖來更加形象的表示這三個(gè)概念。



      以上就是樹的基本概念,樹的種類很多,我們主要來學(xué)學(xué)二叉樹。

      二叉樹


      二叉樹就像它的名字一樣,每個(gè)元素最多有兩個(gè)節(jié)點(diǎn),分別稱為左節(jié)點(diǎn)和右節(jié)點(diǎn)。當(dāng)然并不是每個(gè)元素都需要有兩個(gè)節(jié)點(diǎn),有的可能只有左節(jié)點(diǎn),有的可能只有右節(jié)點(diǎn)。就像國家開放二胎一樣,也不是每個(gè)人都需要生兩個(gè)孩子。下面我們來看看一顆典型的二叉樹。



      基于樹的存儲(chǔ)模式的不同,為了更好的利用存儲(chǔ)空間,二叉樹又分為完全二叉樹和非完全二叉樹,我們先來看看什么是完全二叉樹、非完全二叉樹?

      完全二叉樹的定義:葉子節(jié)點(diǎn)都在最底下兩層,最后一層的葉子節(jié)點(diǎn)都靠左排列,并且除了最后一層,其他層的節(jié)點(diǎn)個(gè)數(shù)都要達(dá)到最大

      也許單看定義會(huì)看不明白,我們來看幾張圖,你就能夠明白什么是完全二叉樹、非完全二叉樹。

      1、完全二叉樹



      2、非完全二叉樹



      上面我們說了基于樹的存儲(chǔ)模式不同,而分為完全二叉樹和非完全二叉樹,那我們接下來來看看樹的存儲(chǔ)模式。

      二叉樹的存儲(chǔ)模式

      二叉樹的存儲(chǔ)模式有兩種,一種是基于指針或者引用的二叉鏈?zhǔn)酱鎯?chǔ)法,一種是基于數(shù)組的順序存儲(chǔ)法

      二叉鏈?zhǔn)酱鎯?chǔ)法


      鏈?zhǔn)酱鎯?chǔ)法相對(duì)比較簡單,理解起來也非常容易,每一個(gè)節(jié)點(diǎn)都有三個(gè)字段,一個(gè)字段存儲(chǔ)著該節(jié)點(diǎn)的值,另外兩個(gè)字段存儲(chǔ)著左右節(jié)點(diǎn)的引用。我們順著跟字節(jié)就可以很輕松的把整棵樹串起來,鏈?zhǔn)酱鎯?chǔ)法的結(jié)構(gòu)大概長成這樣。



      順序存儲(chǔ)法

      順序存儲(chǔ)法是基于數(shù)組實(shí)現(xiàn)的,數(shù)組是一段有序的內(nèi)存空間,如果我們把跟節(jié)點(diǎn)的坐標(biāo)定位i=1,左節(jié)點(diǎn)就是 2 * i = 2,右節(jié)點(diǎn) 2 * i+ 1 =
      3,以此類推,每個(gè)節(jié)點(diǎn)都這么算,然后就將樹轉(zhuǎn)化成數(shù)組了,反過來,按照這種規(guī)則我們也能將數(shù)組轉(zhuǎn)化成一棵樹??吹竭@里我想你一定看出了一些弊端,
      如果這是一顆不平衡的二叉樹是不是會(huì)造成大量的空間浪費(fèi)呢?沒錯(cuò),這就是為什么需要分完全二叉樹和非完全二叉樹。分別來看看這兩種樹基于數(shù)組的存儲(chǔ)模式。
      完全二叉樹順序存儲(chǔ)法

      非完全二叉樹順序存儲(chǔ)法


      從圖中將樹轉(zhuǎn)化成數(shù)組之后可以看出,完全二叉樹用數(shù)組來存儲(chǔ)只浪費(fèi)了一個(gè)下標(biāo)為0的存儲(chǔ)空間,二非完全二叉樹則浪費(fèi)了大量的空間。
      如果樹為完全二叉樹,用數(shù)組存儲(chǔ)比鏈?zhǔn)酱鎯?chǔ)節(jié)約空間,因?yàn)閿?shù)組存儲(chǔ)不需要存儲(chǔ)左右節(jié)點(diǎn)的信息

      上面我們了解了二叉樹的定義、類型、存儲(chǔ)方式,接下來我們一起了解一下二叉樹的遍歷,二叉樹的遍歷也是面試中經(jīng)常遇到的問題。

      二叉樹遍歷


      要了解二叉樹的遍歷,我們首先需要實(shí)例化出一顆二叉樹,我們采用鏈?zhǔn)酱鎯?chǔ)的方式來定義樹,實(shí)例化樹需要樹的節(jié)點(diǎn)信息,用來存放該節(jié)點(diǎn)的信息,因?yàn)槲覀儾庞玫氖擎準(zhǔn)酱鎯?chǔ),所以我們的節(jié)點(diǎn)信息如下。
      /** * 定義一棵樹 */ public class TreeNode { // 存儲(chǔ)值 public int data; // 存儲(chǔ)左節(jié)點(diǎn) public
      TreeNode left; // 存儲(chǔ)右節(jié)點(diǎn) public TreeNode right; public TreeNode(int data) {
      this.data = data; } }
      定義完節(jié)點(diǎn)信息之后,我們就可以初始化一顆樹啦,下面是初始化樹的過程:
      public static TreeNode buildTree() { // 創(chuàng)建測試用的二叉樹 TreeNode t1 = new
      TreeNode(1); TreeNode t2 = new TreeNode(2); TreeNode t3 = new TreeNode(3);
      TreeNode t4 = new TreeNode(4); TreeNode t5 = new TreeNode(5); TreeNode t6 = new
      TreeNode(6); TreeNode t7 = new TreeNode(7); TreeNode t8 = new TreeNode(8);
      t1.left = t2; t1.right = t3; t2.left = t4; t4.right = t7; t3.left = t5;
      t3.right = t6; t6.left = t8; return t1; }
      經(jīng)過上面步驟之后,我們的樹就長成下圖所示的樣子,數(shù)字代表該節(jié)點(diǎn)的值。



      有了樹之后,我們就可以對(duì)樹進(jìn)行遍歷啦,二叉樹的遍歷有三種方式,前序遍歷、中序遍歷、后續(xù)遍歷三種遍歷方式,三種遍歷方式與節(jié)點(diǎn)輸出的順序有關(guān)系。下面我們分別來看看這三種遍歷方式。

      前序遍歷

      前序遍歷:對(duì)于樹中的任意節(jié)點(diǎn)來說,先打印這個(gè)節(jié)點(diǎn),然后再打印它的左子樹,最后打印它的右子樹。

      為了方便大家的理解,我基于上面我們定義的二叉樹,對(duì)三種遍歷方式的執(zhí)行流程都制作了動(dòng)態(tài)圖,希望對(duì)你的閱讀有所幫助,我們先來看看前序遍歷的執(zhí)行流程動(dòng)態(tài)圖。


      理解了前序遍歷的概念和看完前序遍歷執(zhí)行流程動(dòng)態(tài)圖之后,你心里一定很想知道,在代碼中如何怎么實(shí)現(xiàn)樹的前序遍歷?二叉樹的遍歷非常簡單,一般都是采用遞歸的方式進(jìn)行遍歷,我們來看看前序遍歷的代碼:
      // 先序遍歷,遞歸實(shí)現(xiàn) 先打印本身,再打印左節(jié)點(diǎn),在打印右節(jié)點(diǎn) public static void preOrder(TreeNode root) {
      if (root == null) { return; } // 輸出本身 System.out.print(root.data + " "); //
      遍歷左節(jié)點(diǎn) preOrder(root.left); // 遍歷右節(jié)點(diǎn) preOrder(root.right); }
      中序遍歷

      中序遍歷:對(duì)于樹中的任意節(jié)點(diǎn)來說,先打印它的左子樹,然后再打印它本身,最后打印它的右子樹。

      跟前序遍歷一樣,我們來看看中序遍歷的執(zhí)行流程動(dòng)態(tài)圖。

      中序遍歷的代碼:
      // 中序遍歷 先打印左節(jié)點(diǎn),再輸出本身,最后輸出右節(jié)點(diǎn) public static void inOrder(TreeNode root) { if
      (root == null) { return; } inOrder(root.left); System.out.print(root.data + "
      "); inOrder(root.right); }
      后序遍歷

      后序遍歷:對(duì)于樹中的任意節(jié)點(diǎn)來說,先打印它的左子樹,然后再打印它的右子樹,最后打印這個(gè)節(jié)點(diǎn)本身。

      跟前兩種遍歷一樣,理解概念之后,我們還是先來看張圖。

      后序遍歷的實(shí)現(xiàn)代碼:
      // 后序遍歷 先打印左節(jié)點(diǎn),再輸出右節(jié)點(diǎn),最后才輸出本身 public static void postOrder(TreeNode root) { if
      (root == null) { return; } postOrder(root.left); postOrder(root.right);
      System.out.print(root.data + " "); }

      二叉樹的遍歷還是非常簡單的,雖然有三種遍歷方式,但都是一樣的,只是輸出的順序不一樣而已,經(jīng)過了上面這么多的學(xué)習(xí),我相信你一定對(duì)二叉樹有不少的認(rèn)識(shí),接下來我們來了解一種常用而且比較特殊的二叉樹:
      二叉查找樹

      二叉查找樹


      二叉查找樹又叫二叉搜索樹,從名字中我們就能夠知道,這種樹在查找方面一定有過人的優(yōu)勢,事實(shí)確實(shí)如此,二叉查找樹確實(shí)是為查找而生的樹,但是它不僅僅支持快速查找數(shù)據(jù),還支持快速插入、刪除一個(gè)數(shù)據(jù)。那它是怎么做到這些的呢?我們先從二叉查找樹的概念開始了解。

      二叉查找樹:在樹中的任意一個(gè)節(jié)點(diǎn),其左子樹中的每個(gè)節(jié)點(diǎn)的值,都要小于這個(gè)節(jié)點(diǎn)的值,而右子樹節(jié)點(diǎn)的值都大于這個(gè)節(jié)點(diǎn)的值。

      難以理解?記不?。繘]關(guān)系的,下面我定義了一顆二叉查找樹,我們對(duì)著樹,來慢慢理解。


      根據(jù)二叉查找樹的定義,每棵樹的左節(jié)點(diǎn)的值要小于這父節(jié)點(diǎn),右節(jié)點(diǎn)的值要大于父節(jié)點(diǎn)。62節(jié)點(diǎn)的 所有左節(jié)點(diǎn)的值都要小于 62 ,所有右節(jié)點(diǎn) 的值都要大于 62
      。對(duì)于這顆樹上的每一個(gè)節(jié)點(diǎn)都要滿足這個(gè)條件,我們拿我們樹上的另一個(gè)節(jié)點(diǎn) 35 來說,它的右子樹上的節(jié)點(diǎn)值最大不能超過 47 ,因?yàn)?35 是 47
      的左子樹,根據(jù)二叉搜索樹的規(guī)則,左子樹的值要小于節(jié)點(diǎn)值。

      二叉查找樹既然名字中帶有查找兩字,那我們就從二叉查找樹的查找開始學(xué)習(xí)二叉查找樹吧。

      二叉查找樹的查找操作


      由于二叉查找樹的特性,我們需要查找一個(gè)數(shù)據(jù),先跟跟節(jié)點(diǎn)比較,如果值等于跟節(jié)點(diǎn),則返回根節(jié)點(diǎn),如果小于根節(jié)點(diǎn),則必然在左子樹這邊,只要遞歸查找左子樹就行,如果大于,這在右子樹這邊,遞歸右子樹即可。這樣就能夠?qū)崿F(xiàn)快速查找,因?yàn)槊看尾檎叶紲p少了一半的數(shù)據(jù),跟二分查找有點(diǎn)相似,快速插入、刪除都是居于這個(gè)特性實(shí)現(xiàn)的。

      下面我們用一幅動(dòng)態(tài)圖來加強(qiáng)對(duì)二叉查找樹查找流程的理解,我們需要在上面的這顆二叉查找樹中找出值等于 37 的節(jié)點(diǎn),我們一起來看看流程圖是怎么實(shí)現(xiàn)的。


      * 1、先用 37 跟 62 比較,37 < 62 ,在左子樹中繼續(xù)查找
      * 2、左子樹的節(jié)點(diǎn)值為 58,37 < 58 ,繼續(xù)在左子樹中查找
      * 3、左子樹的節(jié)點(diǎn)值為 47,37 < 47,繼續(xù)在左子樹中查找
      * 4、左子樹的節(jié)點(diǎn)值為 35,37 > 35,在右子樹中查找
      * 5、右子樹中的節(jié)點(diǎn)值為 37,37 = 37 ,返回該節(jié)點(diǎn)
      講完了查找的概念之后,我們一起來看看二叉查找樹的查找操作的代碼實(shí)現(xiàn)
      /** * 根據(jù)值查找樹 * @param data 值 * @return */ public TreeNode find(int data) {
      TreeNode p = tree; while (p != null) { if (data < p.data) p = p.left; else if
      (data > p.data) p = p.right; else return p; } return null; }
      二叉查找樹的插入操作


      插入跟查找差不多,也是從根節(jié)點(diǎn)開始找,如果要插入的數(shù)據(jù)比節(jié)點(diǎn)的數(shù)據(jù)大,并且節(jié)點(diǎn)的右子樹為空,就將新數(shù)據(jù)直接插到右子節(jié)點(diǎn)的位置;如果不為空,就再遞歸遍歷右子樹,查找插入位置。同理,如果要插入的數(shù)據(jù)比節(jié)點(diǎn)數(shù)值小,并且節(jié)點(diǎn)的左子樹為空,就將新數(shù)據(jù)插入到左子節(jié)點(diǎn)的位置;如果不為空,就再遞歸遍歷左子樹,查找插入位置。

      假設(shè)我們要插入 63 ,我們用一張動(dòng)態(tài)圖來看看插入的流程。


      * 1、63 > 62 ,在樹的右子樹繼續(xù)查找.
      * 2、63 < 88 ,在樹的左子樹繼續(xù)查找
      * 3、63 < 73 ,因?yàn)?73 是葉子節(jié)點(diǎn),所以 63 就成為了 73 的左子樹。
      我們來看看二叉查找樹的插入操作實(shí)現(xiàn)代碼
      /** * 插入樹 * @param data */ public void insert(int data) { if (tree == null) {
      tree = new TreeNode(data); return; } TreeNode p = tree; while (p != null) { //
      如果值大于節(jié)點(diǎn)的值,則新樹為節(jié)點(diǎn)的右子樹 if (data > p.data) { if (p.right == null) { p.right = new
      TreeNode(data); return; } p = p.right; } else { // data < p.data if (p.left ==
      null) { p.left = new TreeNode(data); return; } p = p.left; } } }
      二叉查找樹的刪除操作

      刪除的邏輯要比查找和插入復(fù)雜一些,刪除分一下三種情況:

      第一種情況:如果要?jiǎng)h除的節(jié)點(diǎn)沒有子節(jié)點(diǎn),我們只需要直接將父節(jié)點(diǎn)中,指向要?jiǎng)h除節(jié)點(diǎn)的指針置為 null。比如圖中的刪除節(jié)點(diǎn) 51。

      第二種情況
      :如果要?jiǎng)h除的節(jié)點(diǎn)只有一個(gè)子節(jié)點(diǎn)(只有左子節(jié)點(diǎn)或者右子節(jié)點(diǎn)),我們只需要更新父節(jié)點(diǎn)中,指向要?jiǎng)h除節(jié)點(diǎn)的指針,讓它指向要?jiǎng)h除節(jié)點(diǎn)的子節(jié)點(diǎn)就可以了。比如圖中的刪除節(jié)點(diǎn)
      35。

      第三種情況
      :如果要?jiǎng)h除的節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn),這就比較復(fù)雜了。我們需要找到這個(gè)節(jié)點(diǎn)的右子樹中的最小節(jié)點(diǎn),把它替換到要?jiǎng)h除的節(jié)點(diǎn)上。然后再刪除掉這個(gè)最小節(jié)點(diǎn),因?yàn)樽钚」?jié)點(diǎn)肯定沒有左子節(jié)點(diǎn)(如果有左子結(jié)點(diǎn),那就不是最小節(jié)點(diǎn)了),所以,我們可以應(yīng)用上面兩條規(guī)則來刪除這個(gè)最小節(jié)點(diǎn)。比如圖中的刪除節(jié)點(diǎn)
      88

      前面兩種情況稍微簡單一些,第三種情況,我制作了一張動(dòng)態(tài)圖,希望能對(duì)你有所幫助。


      我們來看看二叉查找樹的刪除操作實(shí)現(xiàn)代碼
      public void delete(int data) { TreeNode p = tree; // p指向要?jiǎng)h除的節(jié)點(diǎn),初始化指向根節(jié)點(diǎn)
      TreeNode pp = null; // pp記錄的是p的父節(jié)點(diǎn) while (p != null && p.data != data) { pp =
      p; if (data > p.data) p = p.right; else p = p.left; } if (p == null) return; //
      沒有找到 // 要?jiǎng)h除的節(jié)點(diǎn)有兩個(gè)子節(jié)點(diǎn) if (p.left != null && p.right != null) { // 查找右子樹中最小節(jié)點(diǎn)
      TreeNode minP = p.right; TreeNode minPP = p; // minPP表示minP的父節(jié)點(diǎn) while
      (minP.left != null) { minPP = minP; minP = minP.left; } p.data = minP.data; //
      將minP的數(shù)據(jù)替換到p中 p = minP; // 下面就變成了刪除minP了 pp = minPP; } // 刪除節(jié)點(diǎn)是葉子節(jié)點(diǎn)或者僅有一個(gè)子節(jié)點(diǎn)
      TreeNode child; // p的子節(jié)點(diǎn) if (p.left != null) child = p.left; else if (p.right
      != null) child = p.right; else child = null; if (pp == null) tree = child; //
      刪除的是根節(jié)點(diǎn) else if (pp.left == p) pp.left = child; else pp.right = child; }

      我們上面了解了一些二叉查找樹的相關(guān)知識(shí),由于二叉查找樹在極端情況下會(huì)退化成鏈表,例如每個(gè)節(jié)點(diǎn)都只有一個(gè)左節(jié)點(diǎn),這是時(shí)間復(fù)雜度就變成了O(n),為了避免這種情況,又出現(xiàn)了一種新的樹叫
      平衡二叉查找樹,由于本文篇幅有點(diǎn)長了,相信看到這里的各位小伙伴已經(jīng)有點(diǎn)疲憊了,關(guān)于平衡二叉查找樹的相關(guān)知識(shí)我就不在這里介紹了。

      最后

      打個(gè)小廣告,金九銀十跳槽季,平頭哥給大家整理了一份較全面的 Java 學(xué)習(xí)資料,歡迎掃碼關(guān)注微信公眾號(hào):「平頭哥的技術(shù)博文」領(lǐng)取,祝各位升職加薪。

      友情鏈接
      ioDraw流程圖
      API參考文檔
      OK工具箱
      云服務(wù)器優(yōu)惠
      阿里云優(yōu)惠券
      騰訊云優(yōu)惠券
      京東云優(yōu)惠券
      站點(diǎn)信息
      問題反饋
      郵箱:[email protected]
      QQ群:637538335
      關(guān)注微信

        <ul id="qxxfc"><fieldset id="qxxfc"><tr id="qxxfc"></tr></fieldset></ul>
          中文字幕 欧美 日韩 | 欧美做爱免费视频 | 日批视频免费 | 欧美日韩精品在线视频 | 亚洲www视频 | 影音先锋黄色资源 | 91足交 | 青娱乐永久在线视频 | 国产成人无码精品aa免费 | 八重神子乳液 |