0. 數據結構圖文解析系列
數據結構系列文章 |
---|
數據結構圖文解析之:數組、單鏈表、雙鏈表介紹及C++模板實現 |
數據結構圖文解析之:棧的簡介及C++模板實現 |
數據結構圖文解析之:隊列詳解與C++模板實現 |
數據結構圖文解析之:樹的簡介及二叉排序樹C++模板實現. |
數據結構圖文解析之:AVL樹詳解及C++模板實現 |
數據結構圖文解析之:二叉堆詳解及C++模板實現 |
數據結構圖文解析之:哈夫曼樹與哈夫曼編碼詳解及C++模板實現 |
1. 樹的簡介
1.1 樹的特征
樹是一種數據結構,它是n(n>=0)個節點的有限集。n=0時稱為空樹。n>0時,有限集的元素構成一個具有層次感的數據結構。
區別於線性表一對一的元素關系,樹中的節點是一對多的關系。樹具有以下特點:
- n>0時,根節點是唯一的,不可能存在多個根節點。
- 每個節點有零個至多個子節點;除了根節點外,每個節點有且僅有一個父節點。根節點沒有父節點。
1.2 樹的相關概念
樹有許多相關的術語與概念,在學習樹的結構之前,我們要熟悉這些概念。
- 子樹:除了根節點外,每個子節點都可以分為多個不相交的子樹。(圖二)
- 孩子與雙親:若一個結點有子樹,那么該結點稱為子樹根的"雙親",子樹的根是該結點的"孩子"。在圖一中,B、H是A的孩子,A是B、H的雙親。
- 兄弟:具有相同雙親的節點互為兄弟,例如B與H互為兄弟。
- 節點的度:一個節點擁有子樹的數目。例如A的度為2,B的度為1,C的度為3.
- 葉子:沒有子樹,也即是度為0的節點。
- 分支節點:除了葉子節點之外的節點,也即是度不為0的節點。
- 內部節點:除了根節點之外的分支節點。
- 層次:根節點為第一層,其余節點的層次等於其雙親節點的層次加1.
- 樹的高度:也稱為樹的深度,樹中節點的最大層次。
- 有序樹:樹中節點各子樹之間的次序是重要的,不可以隨意交換位置。
- 無序樹:樹種節點各子樹之間的次序是不重要的。可以隨意交換位置。
- 森林:0或多棵互不相交的樹的集合。例如圖二中的兩棵樹為森林。
2. 二叉樹簡介
2.1 二叉樹的定義
二叉樹或者為空集,或者由一個根節點和兩棵互不相交的、分別稱為左子樹和右子樹的二叉樹組成。從定義可以看出一棵二叉樹:
- 二叉樹是有序樹,區分左子樹與右子樹,不可以隨意交換子樹位置。
- 一個節點的子樹數量取值范圍為0,1,2。0代表該節點是葉子節點,1代表該節點只有左子樹或只有右子樹,2代表該節點有左右子樹。
根據定義,一棵二叉樹有5中基本形態:
2.2 斜樹、滿二叉樹、完全二叉樹、二叉查找樹
斜樹
所有節點都只有左子樹的二叉樹叫做左斜樹,所有節點都只有右子樹的二叉樹叫做右斜樹。左斜樹和右子樹統稱為斜樹。
斜樹已經退化成線性結構,二叉樹在查找上表現出來優異性能在斜樹得不到體現。
注意:為了只關注重點,我們所有的節點都采用統一淺綠色着色,若有特殊節點將在圖中備注
滿二叉樹
滿二叉樹要滿足兩個條件:
- 所有的節點都同時具有左子樹和右子樹。
- 所有的葉子節點都在同一層上。
在同樣深度的二叉樹中,滿二叉樹的節點數目是最多的,葉子數也是最多的。
完全二叉樹
在一棵二叉樹中,只有最下兩層的度可以小於2,並且最下一層的葉子節點集中出現在靠左的若干位置上。
或者這樣定義:對一棵具有n個節點的二叉樹按層序從左到右編序,二叉樹樹某個節點的編序與同樣位置的滿二叉樹節點的編序相同如果所有節點都滿足這個條件,則二叉樹為完全二叉樹。
從定義可以看出: 滿二叉樹一定是完全二叉樹;完全二叉樹不一定是滿二叉樹。
二叉查找樹
二叉排序樹也稱為二叉搜索樹或二叉排序樹。二叉排序樹的節點包含鍵值key。二叉排序樹或者是一棵空樹,否則要求:
- 若它的左子樹不為空,那么左子樹上所有節點的key都小於根節點的key
- 若它的右子樹不為空,那么右子樹上所有節點的key都大於根節點的key
- 它的左右子樹也分別為二叉排序樹
根據定義,二叉查找樹中沒有重復key的節點。
在實際的應用中,二叉排序樹的應用比較多,我們后面要講的AVL樹本身也是一種二叉排序樹。
2.3 二叉樹的性質
性質一:在二叉樹的第i層上至多有2^(i-1)個節點(i>=1)
證明:利用數學歸納法進行證明
- 當i==1時,第1層節點數目為2^(i-1) = 2^(1-1) = 2^0 = 1。顯然成立,此時二叉樹只有根節點。
- 假設i>1時,第i層的節點數目為2^(i-1)。
根據假設,只需證明第i+1層節點數為2^i 即可。
由於二叉樹每個節點最多有兩個孩子,故第(i+1)層上的節點數最多是第i層的兩倍。
即:第i+1層上節點數最多為: 2* 2^(i-1) = 2 ^ i
故假設成立,命題得證。
性質二:深度為k的二叉樹至多有2^k-1個節點
證明:二叉樹節點數最多時,每層的節點樹都必須最多。
根據性質一,深度為k的二叉樹的節點數最多為: 2^0 + 2^1 +....+2^(k-1) = 2 ^ k -1
性質三:對任何一棵二叉樹T,如果終端節點數為n0,度為2的節點數為n2 ,那么 n0 = n2 +1
證明:二叉樹節點度數最大為2,則 : n = n0 + n1 + n2 (等式一)
從孩子個數角度出發: 度為0的節點沒有孩子, 度為1的節點沒有1個孩子,度為2的節點有2個孩子,孩子總數為 n00 + n11 +n2 2 = n1+2n2;樹的所有節點中,只有根不是任何節點的孩 子,因此有 n -1 = n1 + 2* n2 ,即 n = n1 + 2* n2 + 1. (等式二)
由等式一等式而可以推出 n0 = n2 +1
性質四: 具有n個節點的完全二叉樹的高度為至少為log2(n+1)
證明:高度為h的二叉樹最多有2{h}–1個結點。反之,對於包含n個節點的二叉樹的高度至少為log2(n+1)。
性質五:如果對一棵有n個節點的完全二叉樹的節點按層序編號(從第一層開始到最下一層,每一層從左到右編號),對任一節點i有:
- 如果i=1 ,則節點為根節點,沒有雙親。
- 如果2 * i > n ,則節點i沒有左孩子 ;否則其左孩子節點為2*i . (n為節點總數)
- 如果2 * i+1>n ,則節點i沒有右孩子;否則其右孩子節點為2*1+1
3. 二叉查找樹
二叉查找樹的定義我們已經知道。要維護二叉查找樹的特性,比較復雜的是刪除節點操作,我們將進行重點的解析。不過我們先來看看二叉查找樹的節點結構定義與類定義。
3.1 節點結構
//二叉查找樹的節點結構
template <typename T>
struct BSNode
{
BSNode(T t)
: value(t), lchild(nullptr), rchild(nullptr){}
BSNode() = default;
T value;
BSNode<T>* lchild;
BSNode<T>* rchild;
BSNode<T>* parent;
};
- value:節點的值,也即是上文的key,類型由模板參數決定
- lchild :指向節點的左孩子
- rchild:指向節點的右孩子
- parent: 指向節點的雙親
3.2 二叉查找樹的抽象數據結構
//二叉查找樹類
template <typename T>
class BSTree
{
public:
BSTree();
~BSTree();
void preOrder(); //前序遍歷二叉樹
void inOrder(); //中序遍歷二叉樹
void postOrder(); //后序遍歷二叉樹
void layerOrder(); //層次遍歷二叉樹
BSNode<T>* search_recursion(T key); //遞歸地進行查找
BSNode<T>* search_Iterator(T key); //迭代地進行查找
T search_minimun(); //查找最小元素
T search_maximum(); //查找最大元素
BSNode<T>* successor (BSNode<T>* x); //查找指定節點的后繼節點
BSNode<T>* predecessor(BSNode<T>* x); //查找指定節點的前驅節點
void insert(T key); //插入指定值節點
void remove(T key); //刪除指定值節點
void destory(); //銷毀二叉樹
void print(); //打印二叉樹
private:
BSNode<T>* root; //根節點
private:
BSNode<T>* search(BSNode<T>* & p, T key);
void remove(BSNode<T>* p, T key);
void preOrder(BSNode<T>* p);
void inOrder(BSNode<T>* p);
void postOrder(BSNode<T>* p);
T search_minimun(BSNode<T>* p);
T search_maximum(BSNode<T>* p);
void destory(BSNode<T>* &p);
};
這里我們定義了二叉排序樹的類型BSTree。它包含了:
- BSTree的根節點root,這是唯一的數據成員
- 操作的外部接口與內部實現接口。例如 preOrder()為提供給用戶使用的接口,接口聲明為public;而preOrder(AVLTreeNode
* pnode)是類內部為了遞歸操作所使用的接口,接口聲明為private。
提供的其他接口都有相應的備注說明。
3.3 插入新節點
假設我們要為數組 a[] = {10 , 5 , 15 , 6 , 4 , 16 }構建一個二叉排序樹,我們按順序逐個插入元素。
插入過程是這樣的:
- 如果是空樹,則創建一個新節點,新節點作為根,因此以元素10構建的節點為該二叉查找樹的根。
- 插入5,5比10小,與10的左孩子節點進行比較,10的左孩子節點為空,進行插入。
- 插入15,15比10大,與10的右孩子節點進行比較,10的右孩子節點為空,進行插入。
- 插入6,6比10小,與10的左孩子節點5比較;6比5大,與5的右孩子節點進行比較,5的右孩子為空,進行插入。
- 插入4,4比10小,與10的左孩子節點5比較;4比5小,與5的左孩子節點進行比較,5的左孩子為空,進行插入。
- 插入16,16比10大,與10的右孩子節點15比較;16比15大,與15的右孩子節點進行比較,15的右孩子為空,進行插入。
從這個過程我們可以總結出插入新元素的步驟:
- 尋找元素合適的插入位置:新元素與當前結點進行比較,若值大於當前結點,則從右子樹進行尋找;否則從左子樹進行尋找.
- 找到插入位置之后,以元素的值構建新節點,插入二叉排序樹中
該過程的實現代碼:
/*插入函數*/
template <typename T>
void BSTree<T>::insert(T key)
{
BSNode<T>* pparent = nullptr;
BSNode<T>* pnode = root;
while (pnode != nullptr) //尋找合適的插入位置
{
pparent = pnode;
if (key > pnode->value)
pnode = pnode->rchild;
else if (key < pnode->value)
pnode = pnode->lchild;
else
break;
}
pnode = new BSNode<T>(key); //以元素的值構建新節點
if (pparent == nullptr) //如果是空樹
{
root = pnode; //則新節點為根
}
else
{
if (key > pparent->value)
{
pparent->rchild = pnode;//否則新節點為其父節點的左孩
}
else
pparent->lchild = pnode; //或右孩
}
pnode->parent = pparent; //指明新節點的父節點
};
將構建出來的新節點插入二叉排序樹時,需要修改鏈接指針的指向。
3.2 遍歷平衡二叉樹
遍歷平衡二叉樹,就是以某種方式逐個“訪問”二叉樹的每一個節點。“訪問”是指對節點的進行某種操作,例如輸出節點的值。
平衡二叉樹是有序樹,嚴格區分左子樹與右子樹,如果規定左子樹先於右子樹的次序,我們有三種方式遍歷二叉樹:
- 前序遍歷
- 中序遍歷
- 后序遍歷
我們以如圖的兩棵二叉排序樹進行遍歷的算法演示。
前序遍歷
若二叉樹為空,則空操作返回,否則先訪問根節點,然后前序遍歷左子樹,再前序遍歷右子樹。(簡記為:VLR)
/*前序遍歷算法*/
template <typename T>
void BSTree<T>::preOrder()
{
preOrder(root);
};
template <typename T>
void BSTree<T>::preOrder(BSNode<T> *p)
{
if (p != nullptr)
{
cout << p->value << endl;
preOrder(p->lchild);
preOrder(p->rchild);
}
};
前序遍歷樹a:10 5 4 3 6 15 16
前序遍歷樹b:5 3 2 4 8 7 9
中序遍歷
若二叉樹為空,則空操作返回,否則從根節點開始,中序遍歷根節點的左子樹,然后訪問根節點,最后中序遍歷右子樹。(簡記為:LVR)
/*中序遍歷算法*/
template <typename T>
void BSTree<T>::inOrder()
{
inOrder(root);
};
template<typename T>
void BSTree<T>::inOrder(BSNode<T>* p)
{
if (p != nullptr)
{
inOrder(p->lchild);
cout << p->value << endl;
inOrder(p->rchild);
}
};
前序遍歷樹a:3 4 5 6 10 15 16
前序遍歷樹b:2 3 4 5 7 8 9
二叉排序樹的中序遍歷剛好輸出一個非遞減的有序序列。
后序遍歷
若樹為空,則返回空操作,否則從左到右先葉子后節點的方式遍歷訪問左右子樹,左右子樹都訪問結束,才訪問根節點。(簡稱LRV)
/*后序遍歷算法*/
template <typename T>
void BSTree<T>::postOrder()
{
postOrder(root);
};
template <typename T>
void BSTree<T>::postOrder(BSNode<T>* p)
{
if (p != nullptr)
{
postOrder(p->lchild);
postOrder(p->rchild);
cout << p->value<<endl;
}
};
后序遍歷樹a:3 4 6 5 16 15 10
后序遍歷樹b:2 4 3 7 9 8 5
3.2 前驅與后繼
對於一棵二叉排序樹,中序遍歷時剛好可以輸出一個非遞減的序列。例如前序遍歷圖九樹a:3 4 5 6 10 15 16,則可稱:
- 4是5 前驅節點,6是5的后繼節點
- 6是10的前驅節點,15是10的后繼節點
一個節點的前驅節點有3種情況:
- 它有左子樹,則左子樹根節點為其前驅節點
- 它沒有左子樹,且它本身為右子樹,則其父節點為其前驅節點
- 它沒有左子樹,且它本身為左子樹,則它的前驅節點為“第一個擁有右子樹的父節點”
/*尋找其前驅節點*/
template <typename T>
BSNode<T>* BSTree<T>::predecessor(BSNode<T>* pnode)
{
if (pnode->lchild != nullptr)
{
pnode = pnode->lchild;
while (pnode->rchild != nullptr)
{
pnode = pnode->rchild;
}
return pnode;
}
BSNode<T>* pparent = pnode->parent;
while (pparent != nullptr && pparent->lchild == pnode)//如果進入循環,則是第三種情況;否則為第二種情況
{
pnode = pparent;
pparent = pparent->parent;
}
return pparent;
};
同樣的,一個節點的后繼節點也有三種情況:
- 它有右子樹;則其后繼節點為其右子樹的最左節點
- 它沒有右子樹,但它本身是一個左孩子,則后繼節點為它的雙親
- 它沒有右子樹,但它本身是一個右孩子,則其后繼節點為“具有左孩子的最近父節點”
/*尋找其后繼節點*/
template <typename T>
BSNode<T>* BSTree<T>::successor(BSNode<T>* pnode)
{
if (pnode->rchild != nullptr)
{
pnode = pnode->rchild;
while (pnode->lchild != nullptr)
{
pnode = pnode->lchild;
}
return pnode;
}
BSNode<T>* pparent = pnode->parent;
while (pparent!=nullptr&& pparent->rchild == pnode)
{
pnode = pparent;
pparent = pparent->parent;
}
return pparent;
};
3.3 刪除節點
刪除二叉排序樹的某個節點有三種情況:
- 被刪除節點同時有左子樹與右子樹。
- 被刪除節點只有左子樹或只有右子樹。
- 被刪除節點沒有子樹。
對於第一種情況,我們的處理方式是將前驅節點的值保存在當前結點,繼而刪除前驅節點。
對於第二種情況,我們直接用子樹替換被刪節點。
對於第三種情況,我們可以直接刪除節點。
刪除節點的代碼:
/*刪除指定節點*/
template <typename T>
void BSTree<T>::remove(T key)
{
remove(root, key);
};
/*刪除指定節點*/
/*內部使用函數*/
template <typename T>
void BSTree<T>::remove(BSNode<T>* pnode, T key)
{
if (pnode != nullptr)
{
if (pnode->value == key)
{
BSNode<T>* pdel=nullptr;
if (pnode->lchild == nullptr || pnode->rchild == nullptr)
pdel = pnode; //情況二、三:被刪節點只有左子樹或右子樹,或沒有孩子
else
pdel = predecessor(pnode); //情況一:被刪節點同時有左右子樹,則刪除該節點的前驅
//此時,被刪節點只有一個孩子(或沒有孩子).保存該孩子指針
BSNode<T>* pchild=nullptr;
if (pdel->lchild != nullptr)
pchild = pdel->lchild;
else
pchild = pdel->rchild;
//讓孩子指向被刪除節點的父節點
if (pchild != nullptr)
pchild->parent = pdel->parent;
//如果要刪除的節點是頭節點,注意更改root的值
if (pdel->parent == nullptr)
root = pchild;
//如果要刪除的節點不是頭節點,要注意更改它的雙親節點指向新的孩子節點
else if (pdel->parent->lchild==pdel)
{
pdel->parent->lchild = pchild;
}
else
{
pdel->parent->rchild = pchild;
}
if (pnode->value != pdel->value)
pnode->value = pdel->value;
delete pdel;
}
//進行遞歸刪除
else if (key > pnode->value)
{
remove(pnode->rchild, key);
}
else remove(pnode->lchild, key);
}
};
3.4 查找元素
我們可以遞歸或非遞歸地進行元素的查找。元素的查找過程與元素的插入過程一致,也是在不斷地與當前結點進行比較,若值比當前節點的值大,則在右子樹進行查找,若值比當前節點的值小,則在左子樹進行查找,可以看到這是一個很適合遞歸操作的過程。而由於二叉排序樹這種左小右大的節點特征,也很容易進行非遞歸查找。
/*查找指定元素的節點(非遞歸)*/
template <typename T>
BSNode<T>* BSTree<T>::search_Iterator(T key)
{
BSNode<T> * pnode = root;
while (pnode != nullptr)
{
if (key == pnode->value) //找到
return pnode;
if (key > pnode->value) //關鍵字比節點值大,在節點右子樹查找
pnode = pnode->rchild;
else
pnode = pnode->lchild; //關鍵字比節點值小,在節點左子樹查找
}
return nullptr;
};
/*查找指定元素的節點(遞歸)*/
template <typename T>
BSNode<T>* BSTree<T>::search_recursion(T key)
{
return search(root, key);
};
/*private:search()*/
/*遞歸查找的類內部實現*/
template <typename T>
BSNode<T>* BSTree<T>::search(BSNode<T>* & pnode, T key)
{
if (pnode == nullptr)
return nullptr;
if (pnode->value == key)
return pnode;
//cout << "-->" << pnode->value << endl; //可以輸出查找路徑
if (key > pnode->value)
return search(pnode->rchild, key);
return search(pnode->lchild, key);
};
3.5 查找最值元素
二叉排序樹的最小值位於其最左節點上;最大值位於其最右節點上:
/*尋找最小元素*/
template <typename T>
T BSTree<T>::search_minimun()
{
return search_minimun(root);
};
template <typename T>
T BSTree<T>::search_minimun(BSNode<T>* p)
{
if (p->lchild != nullptr)
return search_minimun(p->lchild);
return p->value;
};
/*尋找最大元素*/
template <typename T>
T BSTree<T>::search_maximum()
{
return search_maximum(root);
};
template <typename T>
T BSTree<T>::search_maximum(BSNode<T>*p)
{
if (p->rchild != nullptr)
return search_maximum(p->rchild);
return p->value;
};
3.6 銷毀二叉樹
使用后序遍歷遞歸銷毀二叉樹
/*銷毀二叉樹*/
template<typename T>
void BSTree<T>::destory()
{
destory(root);
};
template <typename T>
void BSTree<T>::destory(BSNode<T>* &p)
{
if (p != nullptr)
{
if (p->lchild != nullptr)
destory(p->lchild);
if (p->rchild != nullptr)
destory(p->rchild);
delete p;
p = nullptr;
}
};
3.7測試代碼
int main()
{
BSTree<int> t;
t.insert(62);
t.insert(58);
t.insert(47);
t.insert(51);
t.insert(35);
t.insert(37);
t.insert(88);
t.insert(73);
t.insert(99);
t.insert(93);
t.insert(95);
cout << endl << "中序遍歷:" << endl;
t.inOrder();
cout << "最大元素:" << t.search_maximum() << endl;
cout << "最小元素:" << t.search_minimun() << endl;
cout << "刪除元素99" << endl;
t.remove(99);
cout << "最大元素:" << t.search_maximum() << endl;
t.destory();
getchar();
return 0;
}
運行結果:
中序遍歷:
35
37
47
51
58
62
73
88
93
95
99
最大元素:99
最小元素:35
刪除元素99
最大元素:95
4. 二叉查找樹完整代碼
在github上存放了二叉排序樹的vs項目工程。這是二叉排序樹的源代碼:
https://github.com/huanzheWu/Data-Structure/blob/master/BSTree/BSTree/BSTree.h
原創文章,轉載請注明出處:http://www.cnblogs.com/QG-whz/p/5168620.html#_label0