二叉樹的概念
二叉樹猶如我們的族譜一般!
他也像是一顆倒立的大樹!
樹狀圖是一種數據結構,它是由 n(n>=1)個有限結點組成一個具有層次關係的集合。把它叫做“樹”是因爲它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。它具有以下的特點:
每個結點有零個或多個子結點;沒有父結點的結點稱爲根結點;每一個非根結點有且只有一個父結點;除了根結點外,每個子結點可以分爲多個不相交的子樹;
專業術語 | 中文 | 描述 |
---|---|---|
Root | 根節點 | 一棵樹的頂點 |
Child | 孩子節點 | 一個結點含有的子樹的根結點稱爲該結點的子結點 |
Leaf | 葉子節點 | 沒有孩子的節點 |
Degree | 度 | 一個節點包含的子樹的數量 |
Edge | 邊 | 一個節點與另外一個節點的連接 |
Depth | 深度 | 根節點到這個節點經過的邊的數量 |
Height | 節點高度 | 從當前節點到葉子節點形成路徑中邊的數量 |
Level | 層級 | 節點到根節點最長路徑的邊的總和 |
Path | 路徑 | 一個節點和另一個節點之間經過的邊和 Node 的序列 |
如下圖就是一顆二叉樹:
二叉樹的原理
二叉樹是一個每個結點最多只能有兩個分支的樹,左邊的分支稱之爲左子樹,右邊的分支稱之爲右子樹。
二叉樹的三條限定公式:
公式 |
---|
(1) 在非空二叉樹中,第 i - 1 層的結點總數不超過2^i-1(2的i-1次方), i >= 1; |
(2) 深度爲 h - 1 的二叉樹最多有2^h - 1(2的h次方 - 1) 個結點(h>=1),最少有 h 個結點; |
(3) 對於任意一棵二叉樹,如果其葉結點數爲 N0,而度數爲 2 的結點總數爲 N2,則 N0 = N2 + 1; |
二叉樹的分類
一、完全二叉樹
若設二叉樹的高度爲 h,除第 h 層外,其它各層 (1~h-1) 的結點數都達到最大個數,第 h 層有葉子節點,並且葉子結點都是從左到右依次排布,這就是完全二叉樹(堆就是完全二叉樹)。
二、滿二叉樹
除了葉結點外每一個結點都有左右子節點且葉子結點都處在最底層的二叉樹。
三、平衡二叉樹
又被稱爲 AVL 樹,它是一顆空樹或左右兩個子樹的高度差的絕對值不超過 1,並且左右兩個子樹都是一棵平衡二叉樹。
四、二叉搜索樹
這也是二叉樹中最常用的,也是我們重點講的對象!
二叉搜索樹又稱二叉查找樹、二叉排序樹(Binary Sort Tree)。它是一顆空樹或是滿足下列性質的二叉樹:
性質 |
---|
(1)若左子樹不空,則左子樹上所有節點的值均小於或等於它的根節點的值; |
(2)若右子樹不空,則右子樹上所有節點的值均大於或等於它的根節點的值; |
(3)左、右子樹也分別爲二叉排序樹。 |
五、紅黑樹
是每個節點都帶有顏色屬性(顏色爲紅色或黑色)的自平衡二叉查找樹,滿足下列性質:
性質 |
---|
(1)節點是紅色或黑色; |
(2)根節點是黑色; |
(3)所有葉子節點都是黑色; |
(4)每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點; |
(5)從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。 |
二叉搜索樹的算法實現
二叉搜索樹的原理:
一堆數據中,以任意一個數作爲樹根,之後的所有數,都與樹根作比較,大於樹根,存儲在樹根的右子節點上,如果右子節點已有節點,那麼再與其進行比較,直到找到一個父節點的子節點爲NULL時,將該數插入在當前位置。小於樹根,則存儲在樹根的左子節點上…
例如,下圖就是一顆二叉搜索樹(二叉排序樹):
二叉樹一般採用鏈式存儲方式:每個結點包含兩個指針域,指向兩個孩子結8點,還包含一個數據,存儲結點信息。
節點結構體的定義:
#define NODE_MAX 1024
#define IsLess(a, b) (a < b) // 用於兩數判斷
#define IsEqual(a, b) (a == b)
typedef int ElemType;
typedef struct _Bnode {
ElemType data;
struct _Bnode* lchild; // 左子節點
struct _Bnode* rchild; // 右子節點
}Bnode, * Btree;
二叉搜索樹插入節點
將要插入的結點 e,與節點 root 節點進行比較,若小於則去到左子樹進行比較,若大於則去到右子樹進行比較,重複以上操作直到找到一個空位置用於放置該新節點。
bool insertBtree(Bnode **root, Bnode *node) {
Bnode *tem = NULL; // 用於遍歷
Bnode *parent = NULL; // 用於保存待插入位置的父節點
bool isLchild = true; // true:插入左子節點;false:插入右子節點
if (!node) { // 如果子節點爲空,直接返回
return false;
} else { // 它的左右子節點置爲NULL
node->lchild = NULL;
node->rchild = NULL;
}
if (*root) { // 如果存在根節點
tem = *root; // 使用tem保存根節點,以便後續遍歷
} else {
*root = node; // 將子樹節點賦值根節點
return true;
}
while (tem) {
parent = tem; // 保存父節點
if (IsLess(node->data, tem->data)) { // 如果子節點小於父節點
tem = tem->lchild; // 子節點將插在父節點的左子節點上
isLchild = true;
} else {
tem = tem->rchild; // 子節點將插在父節點的右子節點上
isLchild = false;
}
}
//找到空位置後,進行插入
if (isLchild) {
parent->lchild = node; // 插入左子節點
} else {
parent->rchild = node; // 插入右子節點
}
return true;
}
二叉搜索樹刪除節點
將要刪除的節點的值,與節點 root 節點進行比較,若小於則去到左子樹進行比較,若大於則去到右子樹進行比較,重複以上操作直到找到一個節點的值等於刪除的值,則將此節點刪除。刪除時有 4 中情況須分別處理:
1. 刪除節點不存在左右子節點,即爲葉子節點,直接刪除;
2. 刪除節點存在左子節點,不存在右子節點,直接把左子節點替代刪除節點;
3. 刪除節點存在右子節點,不存在左子節點,直接把右子節點替代刪除節點;
4. 刪除節點存在左右子節點,則取左子樹上的最大節點或右子樹上的最小節點替換刪除節點。
可以使用兩種方式實現刪除:遞歸實現 和 非遞歸實現!
遞歸實現刪除:
// 遞歸方式尋找左子節點最大值
int recursionFindMax(Bnode* root) {
if (!root) {
exit(-1);
}
if (!root->rchild) {
return root->data;
}
return recursionFindMax(root->rchild);
}
// 遞歸方式刪除值
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree) {
if (!root) { // 遞歸出口:沒有找到刪除的值
return NULL;
}
// 結點的值大於刪除的值
if (IsLess(key, root->data)) {
root->lchild = recursionDeleteBtree(root->lchild, key, deleteBtree);
return root;
}
// 結點的值小於刪除的值
if (IsLess(root->data, key)) {
root->rchild = recursionDeleteBtree(root->rchild, key, deleteBtree);
return root;
}
// 獲取待刪除的節點
deleteBtree = root;
// 刪除節點不存在左右子節點,即爲葉子節點,直接刪除
if (!root->lchild && !root->rchild) return NULL;
// 刪除節點只存在右子節點,直接用右子節點取代刪除節點
if (root->rchild && !root->lchild) return root->rchild;
// 刪除節點只存在左子節點,直接用左子節點取代刪除節點
if (root->lchild && !root->rchild) return root->lchild;
// 刪除節點存在左右子節點,直接用左子節點的最大值取代刪除節點
int var = recursionFindMax(root->lchild);
root->data = var;
root->lchild = recursionDeleteBtree(root->lchild, var, deleteBtree);
return root;
}
非遞歸方式刪除:
// 非遞歸方式
Bnode *findMax(Bnode *root, ElemType &var) {
Bnode *praent = root; // 保存最大左子節點的父節點
if (!root) {
exit(-1);
}
// 找到左子節點的最大值
while (root->rchild) {
praent = root; // 保存父節點
root = root->rchild;
}
if (praent != root) {
praent->rchild = NULL; // 斷開其與右子節點的關係
}
var = root->data;
return root;
}
// 非遞歸方式刪除
Bnode *deleteBtree(Bnode *root, ElemType key) {
Bnode *tem = root;
Bnode *praent = NULL; // 保存待刪除節點的父節點
bool isLchild = true; // true:左子節點 false:右子節點
if (!root) {
return NULL;
}
while (tem) {
if (IsLess(key, tem->data)) {
praent = tem; // 保存父節點
tem = tem->lchild;
isLchild = true; // 左子節點
continue;
}
if (IsLess(tem->data, key)) {
praent = tem;
tem = tem->rchild;
isLchild = false; // 右子節點
continue;
}
// 刪除節點不存在左右子節點,即爲葉子節點,直接刪除
if (!tem->lchild && !tem->rchild) {
if (tem != root) { // 如果刪除節點不是根節點
if (isLchild) {
praent->lchild = NULL; // 斷開其與左子節點關係,方便釋放
} else {
praent->rchild = NULL; // 斷開關係
}
}
return tem;
}
// 刪除節點只存在右子節點,直接用右子節點取代刪除節點
if (tem->rchild && !tem->lchild) {
praent->rchild = tem->rchild;
return tem;
}
// 刪除節點只存在左子節點,直接用左子節點取代刪除節點
if (tem->lchild && !tem->rchild) {
praent->lchild = tem->lchild;
return tem;
}
// 刪除節點存在左右子節點,直接用左子節點的最大值取代刪除節點
ElemType var;
praent = findMax(tem->lchild, var);
if (praent == tem->lchild) tem->lchild = NULL; // 斷開關係
tem->data = var;
return praent;
}
return NULL;
}
建議使用非遞歸方式實現刪除!
二叉搜索樹搜索(查找)
一樣有兩種方式實現查找:遞歸方式 和 非遞歸方式
遞歸方式實現查找:
// 遞歸方式查找
Bnode *queryByRec(Bnode *root, ElemType key) {
if (!root || IsEqual(root->data, key)) {
return root;
} else if (IsLess(key, root->data)) {
return queryByRec(root->lchild, key);
} else {
return queryByRec(root->rchild, key);
}
}
非遞歸方式實現查找:
// 非遞歸方式查找
Bnode *queryByLoop(Bnode *root, ElemType key) {
if (root) {
while (root && !IsEqual(root->data, key)) {
if (IsLess(key, root->data)) {
root = root->lchild;
} else {
root = root->rchild;
}
}
return root;
}
return NULL;
}
二叉樹的遍歷
二叉樹的遍歷是指從根結點出發,按照某種次序依次訪問所有結點,使得每個結點被當且訪問一次。共分爲四種方式:
一、前序遍歷
先訪問根節點,然後前序遍歷左子樹,再前序遍歷右子樹。
上圖遍歷結果爲:19、7、5、11、15、25、21、61
前序遍歷使用遞歸方式實現:
// 遞歸方式正序輸出
void PreOrderRec(Bnode* root){
if (!root) {
return;
}
cout << "-" << root->data << "- ";
PreOrderRec(root->lchild);
PreOrderRec(root->rchild);
}
前序遍歷使用 棧 實現:
具體過程:
-
首先申請一個新的棧,記爲 stack;
-
將頭結點(根節點) head 壓入 stack 中;
-
每次從 stack 中彈出棧頂節點,記爲 cur,然後打印 cur 值,如果 cur 右孩子不爲空,則將右孩子壓入棧中;如果 cur 的左孩子不爲空,將其壓入 stack 中;
重複步驟 3,直到 stack 爲空.
// 非遞歸方式正序輸出
void PreOrder(Bnode *root) {
if (!root) {
return;
}
Bnode cur;
Stack stack;
initStack(stack);
insertValue(stack, *root); // 根節點入棧
// 棧不爲空,繼續執行
while (!isStackEmpty(stack)) {
popValue(stack, cur); // 出棧
cout << cur.data << " ";
if (cur.rchild) { // 右子節點不爲空,入棧
insertValue(stack, *cur.rchild);
}
if (cur.lchild) { // 左子節點不爲空,入棧
insertValue(stack, *cur.lchild);
}
}
clearStack(stack); // 釋放棧的內存
}
=
=
=
二、中序遍歷
先訪問根節點的左子樹,然後訪問根節點,最後遍歷右子樹
上圖遍歷結果爲:5、7、11、15、19、21、25、61
=
=
=
三、後序遍歷
從左到右,先葉子後節點的方式遍歷訪問左右子樹,最後訪問根節點
上圖遍歷結果爲:5、15、11、7、21、61、25、19
=
=
=
四、層序遍歷
從根節點從上往下逐層遍歷,在同一層,按從左到右的順序對節點逐個訪問。
上圖遍歷結果爲:19、7、25、5、11、21、61、15
具體代碼實現有興趣的可以自己試一下!
全部測試代碼:
tree.h
#pragma once
#define NODE_MAX 1024
#define IsLess(a, b) (a < b)
#define IsEqual(a, b) (a == b)
typedef int ElemType;
typedef struct _Bnode {
ElemType data;
struct _Bnode* lchild; // 左子節點
struct _Bnode* rchild; // 右子節點
}Bnode, * Btree;
stack.h
#pragma once
#include <cstddef>
#include "tree.h"
#define StackMax 128
typedef struct TreeStack {
Bnode *top;
Bnode *base;
}Stack;
bool initStack(Stack &stack);
bool isStackEmpty(Stack &stack);
bool isStackfull(Stack &stack);
bool insertValue(Stack &stack, Bnode &val);
bool popValue(Stack &stack, Bnode &val);
void clearStack(Stack &stack);
bool initStack(Stack &stack) {
stack.base = new Bnode[StackMax];
if (!stack.base) return false;
stack.top = stack.base;
return true;
}
bool isStackEmpty(Stack &stack) {
if (stack.top == stack.base) {
return true;
}
return false;
}
bool isStackfull(Stack &stack) {
if ((stack.top - stack.base) == StackMax) {
return true;
}
return false;
}
bool insertValue(Stack &stack, Bnode &val) {
if (isStackfull(stack)) {
return false;
}
*stack.top = val;
stack.top++;
return true;
}
bool popValue(Stack &stack, Bnode &val) {
if (isStackEmpty(stack)) {
return false;
}
stack.top--;
val = *stack.top;
return true;
}
void clearStack(Stack &stack) {
delete stack.base;
stack.base = NULL;
stack.top = NULL;
}
main.cpp
#include <iostream>
#include <Windows.h>
#include "stack.h"
using namespace std;
// 插入子樹節點
bool insertBtree(Bnode **root, Bnode *node); // 參數一:根節點;參數二:子樹節點
/* 遞歸方式:*/
// 找到左子節點的最大值
static int recursionFindMax(Bnode *root);
// 刪除節點
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree); // 參數一:根節點;參數二:刪除的值;參數三:保存刪除節點地址返回釋放
/* 非遞歸方式:*/
// 找到左子節點的最大值
static Bnode *findMax(Bnode *root, ElemType &var);
// 刪除節點
Bnode *deleteBtree(Bnode *root, ElemType key);
/* 遞歸方式:*/
// 查找節點
Bnode *queryByRec(Bnode *root, ElemType key);
/* 非遞歸方式:*/
// 查找結點
Bnode *queryByLoop(Bnode *root, ElemType key);
/* 遞歸方式正序遍歷 */
void PreOrderRec(Bnode *root);
/* 非遞歸方式正序遍歷 */
void PreOrder(Bnode *root);
bool insertBtree(Bnode **root, Bnode *node) {
Bnode *tem = NULL; // 用於遍歷
Bnode *parent = NULL; // 用於保存待插入位置的父節點
bool isLchild = true; // true:插入左子節點;false:插入右子節點
if (!node) { // 如果子節點爲空,直接返回
return false;
} else { // 它的左右子節點置爲NULL
node->lchild = NULL;
node->rchild = NULL;
}
if (*root) { // 如果存在根節點
tem = *root; // 使用tem保存根節點,以便後續遍歷
} else {
*root = node; // 將子樹節點賦值根節點
return true;
}
while (tem) {
parent = tem; // 保存父節點
if (IsLess(node->data, tem->data)) { // 如果子節點小於父節點
tem = tem->lchild; // 子節點將插在父節點的左子節點上
isLchild = true;
} else {
tem = tem->rchild; // 子節點將插在父節點的右子節點上
isLchild = false;
}
}
if (isLchild) {
parent->lchild = node; // 插入左子節點
} else {
parent->rchild = node; // 插入右子節點
}
return true;
}
// 遞歸方式尋找左子節點最大值
int recursionFindMax(Bnode* root) {
if (!root) {
exit(-1);
}
if (!root->rchild) {
return root->data;
}
return recursionFindMax(root->rchild);
}
// 遞歸方式刪除值
Bnode * recursionDeleteBtree(Bnode *root, ElemType key, Bnode *&deleteBtree) {
if (!root) { // 遞歸出口:沒有找到刪除的值
return NULL;
}
// 結點的值大於刪除的值
if (IsLess(key, root->data)) {
root->lchild = recursionDeleteBtree(root->lchild, key, deleteBtree);
return root;
}
// 結點的值小於刪除的值
if (IsLess(root->data, key)) {
root->rchild = recursionDeleteBtree(root->rchild, key, deleteBtree);
return root;
}
// 獲取待刪除的節點
deleteBtree = root;
// 刪除節點不存在左右子節點,即爲葉子節點,直接刪除
if (!root->lchild && !root->rchild) return NULL;
// 刪除節點只存在右子節點,直接用右子節點取代刪除節點
if (root->rchild && !root->lchild) return root->rchild;
// 刪除節點只存在左子節點,直接用左子節點取代刪除節點
if (root->lchild && !root->rchild) return root->lchild;
// 刪除節點存在左右子節點,直接用左子節點的最大值取代刪除節點
int var = recursionFindMax(root->lchild);
root->data = var;
root->lchild = recursionDeleteBtree(root->lchild, var, deleteBtree);
return root;
}
// 非遞歸方式
Bnode *findMax(Bnode *root, ElemType &var) {
Bnode *praent = root; // 保存最大左子節點的父節點
if (!root) {
exit(-1);
}
// 找到左子節點的最大值
while (root->rchild) {
praent = root; // 保存父節點
root = root->rchild;
}
if (praent != root) {
praent->rchild = NULL; // 斷開其與右子節點的關係
}
var = root->data;
return root;
}
// 非遞歸方式刪除
Bnode *deleteBtree(Bnode *root, ElemType key) {
Bnode *tem = root;
Bnode *praent = NULL; // 保存待刪除節點的父節點
bool isLchild = true; // true:左子節點 false:右子節點
if (!root) {
return NULL;
}
while (tem) {
if (IsLess(key, tem->data)) {
praent = tem; // 保存父節點
tem = tem->lchild;
isLchild = true; // 左子節點
continue;
}
if (IsLess(tem->data, key)) {
praent = tem;
tem = tem->rchild;
isLchild = false; // 右子節點
continue;
}
// 刪除節點不存在左右子節點,即爲葉子節點,直接刪除
if (!tem->lchild && !tem->rchild) {
if (tem != root) { // 如果刪除節點不是根節點
if (isLchild) {
praent->lchild = NULL; // 斷開其與左子節點關係,方便釋放
} else {
praent->rchild = NULL; // 斷開關係
}
}
return tem;
}
// 刪除節點只存在右子節點,直接用右子節點取代刪除節點
if (tem->rchild && !tem->lchild) {
praent->rchild = tem->rchild;
return tem;
}
// 刪除節點只存在左子節點,直接用左子節點取代刪除節點
if (tem->lchild && !tem->rchild) {
praent->lchild = tem->lchild;
return tem;
}
// 刪除節點存在左右子節點,直接用左子節點的最大值取代刪除節點
ElemType var;
praent = findMax(tem->lchild, var);
if (praent == tem->lchild) tem->lchild = NULL; // 斷開關係
tem->data = var;
return praent;
}
return NULL;
}
// 遞歸方式查找
Bnode *queryByRec(Bnode *root, ElemType key) {
if (!root || IsEqual(root->data, key)) {
return root;
} else if (IsLess(key, root->data)) {
return queryByRec(root->lchild, key);
} else {
return queryByRec(root->rchild, key);
}
}
// 非遞歸方式查找
Bnode *queryByLoop(Bnode *root, ElemType key) {
if (root) {
while (root && !IsEqual(root->data, key)) {
if (IsLess(key, root->data)) {
root = root->lchild;
} else {
root = root->rchild;
}
}
return root;
}
return NULL;
}
// 遞歸方式正序輸出
void PreOrderRec(Bnode* root){
if (!root) {
return;
}
cout << "-" << root->data << "- ";
PreOrderRec(root->lchild);
PreOrderRec(root->rchild);
}
// 非遞歸方式正序輸出
void PreOrder(Bnode *root) {
if (!root) {
return;
}
Bnode cur;
Stack stack;
initStack(stack);
insertValue(stack, *root); // 根節點入棧
while (!isStackEmpty(stack)) {
popValue(stack, cur); // 出棧
cout << cur.data << " ";
if (cur.rchild) { // 右子節點不爲空,入棧
insertValue(stack, *cur.rchild);
}
if (cur.lchild) { // 左子節點不爲空,入棧
insertValue(stack, *cur.lchild);
}
}
clearStack(stack);
}
int main(void) {
int test[] = { 19, 7, 25, 5, 11, 15, 21, 61 };
Bnode *root = NULL;
Bnode *node = NULL;
node = new Bnode;
node->data = test[0];
insertBtree(&root, node);
for (int i = 1; i < sizeof(test) / sizeof(test[0]); i++) {
node = new Bnode;
node->data = test[i];
insertBtree(&root, node);
}
// 打印二叉搜索樹
cout << "二叉樹生成後打印:" << endl;
PreOrderRec(root);
cout << endl;
// 遞歸方式刪除
node = new Bnode;
node = NULL;
recursionDeleteBtree(root, 15, node);
if (node) {
cout << "刪除" << node->data << "成功" << endl;
delete node;
} else {
cout << "沒有該值,刪除失敗!" << endl;
}
cout << "刪除15後打印:" << endl;
PreOrderRec(root);
cout << endl;
// 非遞歸方式刪除
node = new Bnode;
node = NULL;
node = deleteBtree(root, 19);
if (node) {
cout << "刪除11成功" << endl;
delete node;
} else {
cout << "沒有該值,刪除11失敗!" << endl;
}
cout << "刪除11後打印:" << endl;
PreOrderRec(root);
cout << endl;
// 遞歸方式查找
node = new Bnode;
node = NULL;
node = queryByRec(root, 21);
if (node) {
cout << "查找成功,確實有" << node->data << endl;
} else {
cout << "查找失敗,沒有21該值!" << endl;
}
// 非遞歸方式查找
node = queryByLoop(root, 61);
if (node) {
cout << "查找成功,確實有" << node->data << endl;
} else {
cout << "查找失敗,沒有61該值!" << endl;
}
// 非遞歸方式輸出
PreOrder(root);
system("pause");
return 0;
}
運行截圖:
總結:
重在理解,代碼其實很容易實現的,理解好就行!特別建議,項目開發中,能不用遞歸就不用遞歸!!!
筆記來自騎牛學院高級VIP課程!