【追求卓越11】算法--二叉樹

引導

????????接下來的幾節我們開始介紹非線性的數據結構--樹。樹的內容比較多也比較復雜。本節,我們只需要了解關于樹的一些基本概念。以及再進一步了解樹的相關內容--搜索二叉樹。該類型二叉樹在工作中,是我們常接觸的。該節我們介紹關于搜索二叉樹的相關操作:查找,插入,刪除。

什么是樹?

關于樹的定義:(摘自百度百科)

  1. 每個元素稱為結點(node)
  2. 有一個特定的結點被稱為根結點或樹根(root)
  3. 除根結點之外的其余數據元素被分為m(m≥0)個互不相交的集合T1,T2,……Tm-1,其中每一個集合Ti(1<=i<=m)本身也是一棵樹,被稱作原樹的子樹(subtree)

說實話,這段定義我看了好幾遍才能理解,對于剛接觸的同學,結合圖來理解是最好的了。

樹中有很多需要我們了解的名詞,我們結合下面的樹來介紹:

????????在上圖中,A節點就是B節點的父節點,B節點是A節點的子節點。B,C,D這三個節點有共同的父節點,他們是兄弟節點。我們把沒有父節點的節點稱作為根節點,也就是圖中的E節點。我們把沒有子節點的的節點稱作葉子節點或葉節點。

此外還有三個相似的概念:高度,深度,層。

節點的高度:節點到葉子節點最長路徑。比如,上圖中A節點的高度就是2.

節點的深度:根節點到節點的所經歷的邊數。比如,上圖中B節點的深度是2.

節點的層:節點的深度+1。比如,上圖中B子節點的層是3.

樹的高度:根節點的高度。比如,上圖中的樹的高度是3.

以上就是關于樹的部分名詞定義了。但是我們工作中常接觸的是二叉樹。接下來,我們來正式進入今天的主題。

二叉樹

????????二叉樹顧名思義,每個節點最多有兩個分支,即僅有兩個子節點。同理,有四個,八個分支的樹就是四叉樹,八叉樹。是不是很容易理解?如圖:

????????上圖中三個樹都是二叉樹,但是有兩個樹有比較特殊,需要注意。

????????其中2號樹中,葉子節點都在最低層,并且除了葉子節點,每個節點都有左右兩個字節點。這種二叉樹稱作為滿二叉樹

????????其中3號樹中,葉子節點在最底下兩層,最后一層的葉子節點都是靠左排列,并且除了最后一層,其他層的節點個數達到最大數,這種樹稱作完全二叉樹。這個解釋我覺得還不是很清晰,摘自百度百科:對于深度為K的,有n個結點的二叉樹,當且僅當其每一個結點都與深度為K的滿二叉樹中編號從1n的結點一一對應時稱之為完全二叉樹

????????到了這里,你可能會有疑問:滿二叉樹很容易理解,是二叉樹的一種特例。但是完全二叉樹存在的意義是什么

二叉樹的存儲方式

????????我們常見的存儲方式是基于指針或者引用的二叉鏈式存儲法。他比較簡單和直觀,每個節點有一個數據段和兩個左右指針。如圖:

????????另一種就是基于數組的順序存儲法。我們把根節點存儲在下標i=1的位置,那左子節點存儲的下標2 * i=2的位置,右子節點存儲的下標2 * i+1的位置。

????????根據這樣的存儲方式,剛剛的完全二叉樹僅僅會浪費下標未0的空間,若不是完全二叉樹就會出現浪費內存空間的現象。如圖:

????????所以對于完全二叉樹,使用數組進行存儲是最省內存的一種方式。因為順序存儲方式比鏈式存儲方式更生內存,它不需要每個節點保存兩個指針變量。

二叉樹的遍歷

對于二叉樹的遍歷,這個也是非常常見的面試題。常見的遍歷方式是前序遍歷,中序遍歷,后序遍歷。

前序遍歷:對樹中任意節點,先打印這個節點,然后打印它的左子樹,最后打印右子樹

中序遍歷:對于樹中任意節點,先打印這個節點的左子樹,然后打印該節點,最后打印右子樹

后序遍歷:對于樹中任意節點,先打印這個節點的左子樹,然后打印右子樹,最后打印改節點

對于遍歷,實現方式也很簡單,這里僅僅給出偽代碼

void preOrder(Node* root) {
? if (root == null) return;
? print root //
此處為偽代碼,表示打印root節點
? preOrder(root->left);
? preOrder(root->right);
}
void inOrder(Node* root) {
? if (root == null) return;
? inOrder(root->left);
? print root // 此處為偽代碼,表示打印root節點
? inOrder(root->right);
}
void postOrder(Node* root) {
? if (root == null) return;
? postOrder(root->left);
? postOrder(root->right);
? print root // 此處為偽代碼,表示打印root節點
}

每個節點最多被訪問兩次,故時間復雜度是O(n)。

搜索二叉樹

????????搜索二叉樹又稱為二叉查找樹。顧名思義,二叉查找樹是為了快速查找而誕生的。它不僅僅支持快速查找,還支持插入,刪除。

定義:二叉查找樹要求,在樹中任意一個節點,其左子樹中的每個節點的值,都要小于這個節點的值。而右子樹節點的值都大于這個節點的值

可參考下圖:

搜索二叉樹的查找

  1. 先一個節點(一般是root節點),判斷是否是我們要查找的數據?
  2. 如果要查找的數據比該節點的大,我們就在右子樹中繼續查找
  3. 如果要查找的數據比該節點的小,我們就在左子樹中繼續查找
  4. 若未查找到,則返回空

從上面的思路可以看出,我們可以用遞歸或者循環來實現,下面貼上代碼(由于環境原因,代碼都沒有運行,不過代碼邏輯應該是這樣的):

typedef strcut node{
??? int data;
??? struct node left;
??? struct node right;
}Node;
/
*while
循環*
/
Node * BinarySearchTree(Node* root , int target)
{
??? Node * p = root;
??? while(p != NULL)
??? {
??????? if(p.data == target)
??????????? return p;
??????? else if (p.data > target)
??????????? p = p.left;
??????? else
??????????? p = p.right;
??? }
??? return NULL;
}
/
*遞歸*
/
Node * BinarySearchTree(Node* root , int target)
{
??? if(root == NULL)
??????? return NULL;

??? if(root.data == target)
??? {
??????? return root;
??? }
??? else if(root.data > target)
??? {
??????? return BinarySearchTree(root.left,target);
??? }
??? else if(root.data < target)
??? {
??????? return BinarysearchTree(root.right,target);
??? }
}

  1. 搜索二叉樹的插入

????????插入操作和查找操作類似,新插入的數據一般都是保存在葉子節點上的。這句話,我剛開始是抱有懷疑態度,通過自己的思考也就理解了。建議大家先理解這句話的意思。

若知道了新插入數據都是保存在葉子節點上的,那么插入操作的思路也就簡單了。

思路:

  1. 先獲取一個節點,和插入數據比較哪個要大?
  2. 若要插入的數據比節點的值大,并且右子樹為空,即直接將數據插入到該節點的右子節點中。若不為空,繼續遍歷右子樹
  3. 若要插入的數據比節點的值小,并且左子樹為空,即直接將數據插入到該節點的左子節點中。若部位空,繼續遍歷左子樹

typedef strcut node{
??? int data;
??? struct node left;
??? struct node right;
}Node;
int BinarySearchInsert(Node * root , int target)
{
??? Node * p = root ;
??? while(p != NULL)
??? {
??????? if(p.data < target)
??????? {
??????????? if(p.right == NULL)
??????????? {
??????????????? p.right = newNode(target);
??????????????? return 0;
??????????? }
??????????? p = p.right;
??????? }
??????? else
??????? {
??????????? if(p.left == NULL)
??????????? {
??????????????? p.left = newNode(target);
??????????????? return 0;
??????????? }
??????????? p = p.left;
??????? }
??? }
??? return -1;
}

搜索二叉樹的刪除操作

搜索二叉樹的刪除操作就比較復雜了,因為刪除之后,你必須要保證剩下的樹結構滿足搜索二叉樹定義。其情況大致可以分為以下三種:

  1. 刪除節點沒有子節點。刪除一個節點,我們需要先找到這個節點,我們可以使用查找操作的邏輯。若刪除節點沒有子節點,也就不會影響樹的結構。直接刪除即可。將其父節點指向該節點的指針,指向NULL;
  2. 刪除的節點有一個子節點。將刪除節點的父節點指向該節點的指針修改為指向子節點即可。
  3. 刪除節點有兩個子節點。將刪除節點的右子樹中最小節點刪除,并替換為該刪除節點即可。這個可能比較抽象,可參數下圖,刪除18這個節點:

typedef strcut node{
??? int data;
??? struct node left;
??? struct node right;
}Node;
int BinarySearchDelete(Node * root , int target)
{
??? Node * p = root;
??? Node * father = NULL

??? Node * minfather = NULL;
??? Node * min = NULL;
??? while(p != NULL && p.data != target)
??? {
??????? father = p; /
*保存父節點*
/
??????? if(p.data < target)
??????????? p = p.left;
??????? else
??????????? p = p.right;
??? }
??? if(p == NULL)
??????? return -1;/
*not find target*
/

??? /
*刪除節點有兩個子節點*
/
??? if(p.left != NULL && p.right != NULL)
??? {
??????? minfather = p;
??????? min = p.right;

??????? while(min.left != NULL )
??????????? minfather = min;
??????????? min = min.left;

??????? /
*至此找到刪除節點右子樹中的最小節點*
/
??????? p.data = min.data; /
*這里我覺得使用memcpy(p,min,sizeof(Node))較好*
/

??????? /
*將刪除的點變為只有一個或沒有子節點*
/
??????? p = min;
??????? father = minfather;
??? }

??? /
*至此,p表示刪除的節點,father表示刪除節點的父節點,并且刪除節點不會有兩個子節點*
/
??? Node * child = NULL;
??? if(p.left != NULL)
??????? child = p.left;
??? else if(p.right != NULL)
??????? child = p.right;

??? if(father == NULL)/
*刪除的是root節點*
/
??????? memcpy(root,child,sizeof(Node));
??? else if(father.left == p)
??????? father.left = child;
??? else
??????? father.right = child;

??? free(p);
}

搜索二叉樹的中序遍歷

????????搜索二叉樹有一個非常強大的特性,那就是通過中序遍歷,即可輸出有序的數據序列,時間復雜度是O(n),非常高效。

如何支持重復數據

上面的操作,我們都是假設沒有重復數據,但是顯示工作中我們總會遇到相同key的節點(key表示作為搜索二叉樹依據)。針對相同鍵值的節點,我們又有哪些方式處理呢?

  1. 利用數組或鏈表進行擴容,將相同鍵值的數據儲存到同一個節點上。
  2. 每個節點只保存一個數據。在插入的過程中,如果遇到一個節點的值,與要插入的值相同。我們就將這個要插入的數據放到這個節點的右子樹。也就是將這個新插入的數據當作大于這個節點的值來處理。后續的刪除都是按照這個邏輯處理

二叉查找樹的時間復雜度

實際上二叉樹的形態各式各樣,即使同一組數據,也可以有不同的形態。如圖:

第一種情況完全退化成了鏈表,時間復雜度為O(n),第三種的效果應該會好一些。其實,我們可以知道時間復雜度其實跟樹的高度成正比。也就是O(height)。根據完全二叉樹可知,L 的范圍是[log2(n+1), log2n +1]。這樣就極大的提高了效率。因此我們能在實際工作中需要無論怎么刪除或增加,都可以保證樹的平衡。紅黑樹就是這樣的一種樹。時間復雜度為O(logn)

搜索二叉樹相較于散列表的優點

雖然散列表在的時間復雜度達到了O(1),而查找二叉樹在理想情況下,也是O(logn)。那么為什么,我們工作中,樹的使用要高于散列表呢?

  1. 散列表是無序存儲,如果要輸出有序的數據,需要先進行排序。而對于二叉樹查找而言,我們僅需中序遍歷就可以得到有序序列。
  2. 散列表擴容是耗時較多(雖然我們有一些優化手段,分時),而且當遇到散列沖突時,性能不穩定。雖然搜索二叉樹也不穩定,但我們在工作中常使用的是平衡二叉查找樹。
  3. 因為散列沖突的存在,散列表的時間復雜度并不一定比logn小。實際查找速度不一定比O(logn)快。另外還有哈希函數的耗時。
  4. 綜上幾點,樹在某些方面還是優于散列表的。

總結

????????本節開始接觸樹的內容,了解到父節點,子節點,兄弟節點,根節點,葉子節點,高度,深度,層等概念

????????樹的種類有很多,工作中,我們常接觸的就是二叉樹。我們也介紹了完全二叉樹和滿二叉樹的概念。以及二叉樹存儲的方式有基于指針的鏈式存儲方式和基于數組的順序存儲方式。其中完全二叉樹使用數組是最節省內存的方式。

????????紹了什么是搜索二叉樹,以及搜索二叉樹的刪除,增加,查找的的邏輯。

????????也簡單介紹了,當有相同的鍵值時,我們該如何處理:利用數組或鏈表擴容。或者默認為大于該節點。

????????最后我們講解了二叉樹的三種遍歷方式:前序遍歷,中序遍歷,后序遍歷。以及代碼的是實現方式。樹結構相對于散列表的優勢。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/164950.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/164950.shtml
英文地址,請注明出處:http://en.pswp.cn/news/164950.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【華為數通HCIP | 網絡工程師】821-IGP高頻題、易錯題之OSPF(2)

個人名片&#xff1a; &#x1f43c;作者簡介&#xff1a;一名大三在校生&#xff0c;喜歡AI編程&#x1f38b; &#x1f43b;???個人主頁&#x1f947;&#xff1a;落798. &#x1f43c;個人WeChat&#xff1a;hmmwx53 &#x1f54a;?系列專欄&#xff1a;&#x1f5bc;?…

計算機中msvcr120.dll丟失怎樣修復?親測有效的5種方法分享

在計算機使用過程中&#xff0c;我們經常會遇到一些錯誤提示&#xff0c;其中之一就是“msvcr120.dll丟失”。這個錯誤通常會導致某些應用程序無法正常運行。那么&#xff0c;當我們遇到這個問題時&#xff0c;應該如何修復呢&#xff1f;本文將詳細介紹msvcr120.dll丟失的解決…

人工智能今天能為你做什么?生成式人工智能如何改變技術文檔領域

▲ 搜索“大龍談智能內容”關注GongZongHao▲ 作者 | Fabrice Lacroix 大型語言模型&#xff08;LLM&#xff09;和生成式人工智能&#xff08;GenAI&#xff09;&#xff0c;尤其是ChatGPT&#xff0c;這些是引領科技革新的新興技術。它們不僅在科技界引起了軒然大波&#x…

Web 自動化神器 TestCafe(三)—用例編寫篇

一、用例編寫基本規范 1、 fixture 測試夾具 使用 TestCafe 編寫測試用例&#xff0c;必須要先使用 fixture 聲明一個測試夾具&#xff0c;然后在這個測試夾具下編寫測試用例&#xff0c;在一個編寫測試用例的 js 或 ts 文件中&#xff0c;可以聲明多個測試夾具 fixture(測試…

【C++11】default、delete與Noncopyable

C11 oop中的default、delete與Noncopyable default 在C11標準中&#xff0c;可以使用default關鍵字來顯式地聲明默認的構造函數和析構函數。 使用default關鍵字可以用來顯式聲明默認的構造函數和析構函數。這樣做可以讓編譯器自動生成默認實現 –>->->關于構造函數…

計數排序+桶排序+基數排序 詳講(思路+圖解+代碼詳解)

文章目錄 計數排序桶排序基數排序一、計數排序概念&#xff1a;寫法一&#xff1a;寫法二&#xff1a; 二、桶排序概念代碼 三、基數排序概念1.LSD排序法&#xff08;最低位優先法&#xff09;2.MSD排序法&#xff08;最高位優先法&#xff09; 基數排序VS基數排序VS桶排序 計數…

內容營銷頻頻出圈,這些品牌號做對了什么?

小紅書擁有大量的年輕用戶&#xff0c;通過運營品牌號既能降低投放成本&#xff0c;又能更好地連接消費者和品牌&#xff0c;在平臺完成一站式閉環營銷。 今天就借助幾個成功案例&#xff0c;來分析下他們是如何搭建官方賬號&#xff0c;通過內容運營吸引更多用戶&#xff0c;實…

一款專為POS機設計的芯片解決方案

一、基本概述 HCM8003設計用于磁條讀卡器系統。它會從F/2F恢復時鐘和數據信號磁產生的數據流頭HCM8003將用于數據速率從200到15000比特每秒。 二、典型電路 內部數據的采集和跟蹤這個范圍是自動的。可以應用于POS機終端設備、磁卡門禁系統、身份識別等場合。 三、引腳定義 四…

redis的主從復制,哨兵模式

1.主從復制 主從復制&#xff1a;主從復制是redis實現高可用的基礎&#xff0c;哨兵模式和集群都是在主從復制的基礎之上實現高可用 主從復制實現數據的多機備份&#xff0c;以及讀寫分離&#xff08;主服務器負責寫&#xff0c;從服務器只能讀&#xff09; 缺陷&#xff1a…

PyTorch基本操作和工作流程

1. PyTorch基礎 張量&#xff08;Tensors&#xff09;&#xff1a; 學習 PyTorch 中表示數據的基本單元。了解如何創建、操作和使用張量。 自動微分&#xff08;Autograd&#xff09;&#xff1a; 了解 PyTorch 的自動微分機制&#xff0c;這是訓練神經網絡的核心。 模型定義…

Android registerForActivityResults使用詳解以及實現原理

registerForActivityResult 使用用途是監聽Activity結果。 以下是使用樣例 //需要傳遞Request用于解析Intent和解析上個Activity返回的結果 val launchdata = registerForActivityResult<PickVisualMediaRequest, Uri?>(ActivityResultContracts.PickVisualMedia()) {…

中國人工智能系列白皮書 AIGC

https://www.caai.cn/index.php?s/home/article/detail/id/3188.html

【git】使用ssh

前言 git之前一直使用https&#xff0c;因為很方便隨時隨地都可以用。最近把代碼托管到GitHub&#xff0c;使用https就使用不了。后面聽同事說GitHub使用ssh是沒問題的&#xff0c;就想著嘗試一下。 git ssh配置 設置用戶名和郵箱 git config --global use.name username g…

OpenCV實現圖像噪聲、去噪基本方法

一、噪聲分類 1、高斯噪聲 指服從高斯分布&#xff08;正態分布&#xff09;的一類噪聲&#xff0c;其產生的主要原因是由于相機在拍攝時視場較暗且亮度不均勻造成的&#xff0c;同時相機長時間工作使得溫度過高也會引起高斯噪聲&#xff0c;另外電路元器件白身噪聲和互相影響…

acwing算法基礎之數學知識--求組合數基礎版

目錄 1 基礎知識2 模板3 工程化 1 基礎知識 &#xff08;一&#xff09; 組合數 C n k C_n^k Cnk?的計算公式&#xff0c; C n k n ? ( n ? 1 ) ? ( n ? k 1 ) 1 ? 2 ? k C_n^k\frac{n\cdot(n-1)\cdots(n-k1)}{1\cdot 2\cdots k} Cnk?1?2?kn?(n?1)?(n?k1)? …

laravel-admin導出excel全部,表中無id列導出失敗

laravel-admin導出excel時&#xff0c;導出全部數據&#xff0c;但是表中沒有id字段&#xff0c;然后就無法導出excel&#xff1b; 就直接顯示 一開始我也很著急&#xff0c;弄了半天還是不行&#xff0c;然后重寫還是有問題 最后發現底層代碼排序是按照id排序的orderBy(id, a…

Unity地面交互效果——6、地形動態頂點置換和曲面細分

回到目錄 Unity置換貼圖局部距離曲面細分 大家好&#xff0c;我是阿趙。 ??這篇文章是我無聊的時候做了一個demo&#xff0c;覺得挺有趣&#xff0c;于是就發上來。這里面包含了4個內容&#xff1a;置換貼圖、頂點偏移、局部曲面細分&#xff0c;曲面細分按距離調整強度。 …

JVS低代碼表單設計:數據聯動詳解(多級數據級、數據回顯等)

在這信息化時代&#xff0c;表單作為數據的收集和展示工具&#xff0c;已經滲透到不同的角落。JVS低代碼對表單的設計和操作進行了不斷的優化和創新。其中&#xff0c;聯動回顯作為一項重要的功能&#xff0c;無論是多級數據級聯控制、組件的聯動控制&#xff0c;還是多表的數據…

【0基礎學Java第三課】-- 運算符

3. 運算符 3.1 什么是運算符3.2 算術運算符3.2.1 **基本四則運算符&#xff1a;加減乘除模( - * / %&#xff09;**3.2.2 增量運算符 - * %3.2.3 自增/自減運算符 -- 3.3 關系運算符3.4邏輯運算符(重點)3.4.1 邏輯與 &&3.4.2 邏輯 ||3.4.3邏輯非 !3.4.4 短路求值 3.5 …