回爐篇11—資料結構(10)之二叉樹
前言:
接下來我們將會介紹另外一種資料結構——樹。二叉樹是樹這種資料結構的一員,後面我們還會介紹紅黑樹,2-3-4樹等資料結構。那麼為什麼要使用樹?它有什麼優點?
前面我們介紹陣列的資料結構,我們知道對於有序陣列,查詢很快,並介紹可以通過二分法查詢,但是想要在有序陣列中插入一個數據項,就必須先找到插入資料項的位置,然後將所有插入位置後面的資料項全部向後移動一位,來給新資料騰出空間,平均來講要移動N/2次,這是很費時的。同理,刪除資料也是。
然後我們介紹了另外一種資料結構——連結串列,連結串列的插入和刪除很快,我們只需要改變一些引用值就行了,但是查詢資料卻很慢了,因為不管我們查詢什麼資料,都需要從連結串列的第一個資料項開始,遍歷到找到所需資料項為止,這個查詢也是平均需要比較N/2次。
那麼我們就希望一種資料結構能同時具備陣列查詢快的優點以及連結串列插入和刪除快的優點,於是 樹 誕生了。
1.樹
樹(tree)是一種抽象資料型別(ADT),用來模擬具有樹狀結構性質的資料集合。它是由n(n>0)個有限節點通過連線它們的邊組成一個具有層次關係的集合。把它叫做“樹”是因為它看起來像一棵倒掛的樹,也就是說它是根朝上,而葉朝下的。
①、節點:上圖的圓圈,比如A,B,C等都是表示節點。節點一般代表一些實體,在java面向物件程式設計中,節點一般代表物件。
②、邊:連線節點的線稱為邊,邊表示節點的關聯關係。一般從一個節點到另一個節點的唯一方法
樹有很多種,向上面的一個節點有多餘兩個的子節點的樹,稱為多路樹,後面會講解2-3-4樹和外部儲存都是多路樹的例子。而每個節點最多隻能有兩個子節點的一種形式稱為二叉樹,這也是本篇部落格講解的重點。
樹的常用術語
①、路徑:順著節點的邊從一個節點走到另一個節點,所經過的節點的順序排列就稱為“路徑”。
②、根:樹頂端的節點稱為根。一棵樹只有一個根,如果要把一個節點和邊的集合稱為樹,那麼從根到其他任何一個節點都必須有且只有一條路徑。A是根節點。
③、父節點:若一個節點含有子節點,則這個節點稱為其子節點的父節點;B是D的父節點。
④、子節點:一個節點含有的子樹的根節點稱為該節點的子節點;D是B的子節點。
⑤、兄弟節點:具有相同父節點的節點互稱為兄弟節點;比如上圖的D和E就互稱為兄弟節點。
⑥、葉節點:沒有子節點的節點稱為葉節點,也叫葉子節點,比如上圖的A、E、F、G都是葉子節點。
⑦、子樹:每個節點都可以作為子樹的根,它和它所有的子節點、子節點的子節點等都包含在子樹中。
⑧、節點的層次:從根開始定義,根為第一層,根的子節點為第二層,以此類推。
⑨、深度:對於任意節點n,n的深度為從根到n的唯一路徑長,根的深度為0;
⑩、高度:對於任意節點n,n的高度為從n到一片樹葉的最長路徑長,所有樹葉的高度為0;
2.二叉樹
二叉樹:樹的每個節點最多隻能有兩個子節點
上圖的第一幅圖B節點有DEF三個子節點,就不是二叉樹,稱為多路樹;而第二幅圖每個節點最多隻有兩個節點,是二叉樹,並且二叉樹的子節點稱為“左子節點”和“右子節點”。上圖的D,E分別是B的左子節點和右子節點。
如果我們給二叉樹加一個額外的條件,就可以得到一種被稱作二叉搜尋樹(binary search tree)的特殊二叉樹。
二叉搜尋樹要求:若它的左子樹不空,則左子樹上所有結點的值均小於它的根結點的值; 若它的右子樹不空,則右子樹上所有結點的值均大於它的根結點的值; 它的左、右子樹也分別為二叉排序樹。
二叉搜尋樹作為一種資料結構,那麼它是如何工作的呢?它查詢一個節點,插入一個新節點,以及刪除一個節點,遍歷樹等工作效率如何,下面我們來一一介紹。
二叉樹的節點類:
package com.ys.tree;
public class Node {
private Object data; //節點資料
private Node leftChild; //左子節點的引用
private Node rightChild; //右子節點的引用
//列印節點內容
public void display(){
System.out.println(data);
}
}
二叉樹的具體方法:
package com.ys.tree;
public interface Tree {
//查詢節點
public Node find(Object key);
//插入新節點
public boolean insert(Object key);
//刪除節點
public boolean delete(Object key);
//Other Method......
}
3.查詢節點
查詢某個節點,我們必須從根節點開始遍歷。
①、查詢值比當前節點值大,則搜尋右子樹;
②、查詢值等於當前節點值,停止搜尋(終止條件);
③、查詢值小於當前節點值,則搜尋左子樹;
//查詢節點
public Node find(int key) {
Node current = root;
while(current != null){
if(current.data > key){//當前值比查詢值大,搜尋左子樹
current = current.leftChild;
}else if(current.data < key){//當前值比查詢值小,搜尋右子樹
current = current.rightChild;
}else{
return current;
}
}
return null;//遍歷完整個樹沒找到,返回null
}
用變數current來儲存當前查詢的節點,引數key是要查詢的值,剛開始查詢將根節點賦值到current。接在在while迴圈中,將要查詢的值和current儲存的節點進行對比。如果key小於當前節點,則搜尋當前節點的左子節點,如果大於,則搜尋右子節點,如果等於,則直接返回節點資訊。當整個樹遍歷完全,即current == null,那麼說明沒找到查詢值,返回null。
樹的效率:查詢節點的時間取決於這個節點所在的層數,每一層最多有2n-1個節點,總共N層共有2n-1個節點,那麼時間複雜度為O(logn),底數為2。
4.插入節點
要插入節點,必須先找到插入的位置。與查詢操作相似,由於二叉搜尋樹的特殊性,待插入的節點也需要從根節點開始進行比較,小於根節點則與根節點左子樹比較,反之則與右子樹比較,直到左子樹為空或右子樹為空,則插入到相應為空的位置,在比較的過程中要注意儲存父節點的資訊 及 待插入的位置是父節點的左子樹還是右子樹,才能插入到正確的位置。
//插入節點
public boolean insert(int data) {
Node newNode = new Node(data);
if(root == null){//當前樹為空樹,沒有任何節點
root = newNode;
return true;
}else{
Node current = root;
Node parentNode = null;
while(current != null){
parentNode = current;
if(current.data > data){//當前值比插入值大,搜尋左子節點
current = current.leftChild;
if(current == null){//左子節點為空,直接將新值插入到該節點
parentNode.leftChild = newNode;
return true;
}
}else{
current = current.rightChild;
if(current == null){//右子節點為空,直接將新值插入到該節點
parentNode.rightChild = newNode;
return true;
}
}
}
}
return false;
}
5.遍歷樹
遍歷樹是根據一種特定的順序訪問樹的每一個節點。比較常用的有前序遍歷,中序遍歷和後序遍歷。而二叉搜尋樹最常用的是中序遍歷。
①、中序遍歷:左子樹——》根節點——》右子樹
②、前序遍歷:根節點——》左子樹——》右子樹
③、後序遍歷:左子樹——》右子樹——》根節點
//中序遍歷
public void infixOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.data+" ");
infixOrder(current.rightChild);
}
}
//前序遍歷
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
preOrder(current.leftChild);
preOrder(current.rightChild);
}
}
//後序遍歷
public void postOrder(Node current){
if(current != null){
postOrder(current.leftChild);
postOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
6.查詢最大值和最小值
這沒什麼好說的,要找最小值,先找根的左節點,然後一直找這個左節點的左節點,直到找到沒有左節點的節點,那麼這個節點就是最小值。同理要找最大值,一直找根節點的右節點,直到沒有右節點,則就是最大值。
//找到最大值
public Node findMax(){
Node current = root;
Node maxNode = current;
while(current != null){
maxNode = current;
current = current.rightChild;
}
return maxNode;
}
//找到最小值
public Node findMin(){
Node current = root;
Node minNode = current;
while(current != null){
minNode = current;
current = current.leftChild;
}
return minNode;
}
7.刪除節點
刪除節點是二叉搜尋樹中最複雜的操作,刪除的節點有三種情況,前兩種比較簡單,但是第三種卻很複雜。
1、該節點是葉節點(沒有子節點)
2、該節點有一個子節點
3、該節點有兩個子節點
下面我們分別對這三種情況進行講解。
①、刪除沒有子節點的節點
要刪除葉節點,只需要改變該節點的父節點引用該節點的值,即將其引用改為 null 即可。要刪除的節點依然存在,但是它已經不是樹的一部分了,由於Java語言的垃圾回收機制,我們不需要非得把節點本身刪掉,一旦Java意識到程式不在與該節點有關聯,就會自動把它清理出儲存器。
@Override
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查詢刪除值,找不到直接返回false
while(current.data != key){
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
}else{
isLeftChild = false;
current = current.rightChild;
}
if(current == null){
return false;
}
}
//如果當前節點沒有子節點
if(current.leftChild == null && current.rightChild == null){
if(current == root){
root = null;
}else if(isLeftChild){
parent.leftChild = null;
}else{
parent.rightChild = null;
}
return true;
}
return false;
}
刪除節點,我們要先找到該節點,並記錄該節點的父節點。在檢查該節點是否有子節點。如果沒有子節點,接著檢查其是否是根節點,如果是根節點,只需要將其設定為null即可。如果不是根節點,是葉節點,那麼斷開父節點和其的關係即可。
②、刪除有一個子節點的節點
刪除有一個子節點的節點,我們只需要將其父節點原本指向該節點的引用,改為指向該節點的子節點即可。
//當前節點有一個子節點
if(current.leftChild == null && current.rightChild != null){
if(current == root){
root = current.rightChild;
}else if(isLeftChild){
parent.leftChild = current.rightChild;
}else{
parent.rightChild = current.rightChild;
}
return true;
}else{
//current.leftChild != null && current.rightChild == null
if(current == root){
root = current.leftChild;
}else if(isLeftChild){
parent.leftChild = current.leftChild;
}else{
parent.rightChild = current.leftChild;
}
return true;
}
③、刪除有兩個子節點的節點
當刪除的節點存在兩個子節點,那麼刪除之後,兩個子節點的位置我們就沒辦法處理了。既然處理不了,我們就想到一種辦法,用另一個節點來代替被刪除的節點,那麼用哪一個節點來代替呢?
我們知道二叉搜尋樹中的節點是按照關鍵字來進行排列的,某個節點的關鍵字次高節點是它的中序遍歷後繼節點。用後繼節點來代替刪除的節點,顯然該二叉搜尋樹還是有序的。(這裡用後繼節點代替,如果該後繼節點自己也有子節點,我們後面討論。)
那麼如何找到刪除節點的中序後繼節點呢?其實我們稍微分析,這實際上就是要找比刪除節點關鍵值大的節點集合中最小的一個節點,只有這樣代替刪除節點後才能滿足二叉搜尋樹的特性。
後繼節點也就是:比刪除節點大的最小節點。
演算法:程式找到刪除節點的右節點,(注意這裡前提是刪除節點存在左右兩個子節點,如果不存在則是刪除情況的前面兩種),然後轉到該右節點的左子節點,依次順著左子節點找下去,最後一個左子節點即是後繼節點;如果該右節點沒有左子節點,那麼該右節點便是後繼節點。
需要確定後繼節點沒有子節點,如果後繼節點存在子節點,那麼又要分情況討論了。
①、後繼節點是刪除節點的右子節點
這種情況簡單,只需要將後繼節點表示的子樹移到被刪除節點的位置即可!
②、後繼節點是刪除節點的右子節點的左子節點
public Node getSuccessor(Node delNode){
Node successorParent = delNode;
Node successor = delNode;
Node current = delNode.rightChild;
while(current != null){
successorParent = successor;
successor = current;
current = current.leftChild;
}
//將後繼節點替換刪除節點
if(successor != delNode.rightChild){
successorParent.leftChild = successor.rightChild;
successor.rightChild = delNode.rightChild;
}
return successor;
④、刪除有必要嗎?
通過上面的刪除分類討論,我們發現刪除其實是挺複雜的,那麼其實我們可以不用真正的刪除該節點,只需要在Node類中增加一個標識欄位isDelete,當該欄位為true時,表示該節點已經刪除,反正沒有刪除。那麼我們在做比如find()等操作的時候,要先判斷isDelete欄位是否為true。這樣刪除的節點並不會改變樹的結構。
public class Node {
int data; //節點資料
Node leftChild; //左子節點的引用
Node rightChild; //右子節點的引用
boolean isDelete;//表示節點是否被刪除
}
8.二叉樹的效率
從前面的大部分對樹的操作來看,都需要從根節點到下一層一層的查詢。
一顆滿樹,每層節點數大概為2n-1,那麼最底層的節點個數比樹的其它節點數多1,因此,查詢、插入或刪除節點的操作大約有一半都需要找到底層的節點,另外四分之一的節點在倒數第二層,依次類推。
總共N層共有2n-1個節點,那麼時間複雜度為O(logn),底數為2。
在有1000000 個數據項的無序陣列和連結串列中,查詢資料項平均會比較500000 次,但是在有1000000個節點的二叉樹中,只需要20次或更少的比較即可。
有序陣列可以很快的找到資料項,但是插入資料項的平均需要移動 500000 次資料項,在 1000000 個節點的二叉樹中插入資料項需要20次或更少比較,在加上很短的時間來連線資料項。
同樣,從 1000000 個數據項的陣列中刪除一個數據項平均需要移動 500000 個數據項,而在 1000000 個節點的二叉樹中刪除節點只需要20次或更少的次數來找到他,然後在花一點時間來找到它的後繼節點,一點時間來斷開節點以及連線後繼節點。
所以,樹對所有常用資料結構的操作都有很高的效率。
遍歷可能不如其他操作快,但是在大型資料庫中,遍歷是很少使用的操作,它更常用於程式中的輔助演算法來解析算術或其它表示式。
9、用陣列表示樹
用陣列表示樹,那麼節點是存在陣列中的,節點在陣列中的位置對應於它在樹中的位置。下標為 0 的節點是根,下標為 1 的節點是根的左子節點,以此類推,按照從左到右的順序儲存樹的每一層。
樹中的每個位置,無論是否存在節點,都對應於陣列中的一個位置,樹中沒有節點的在陣列中用0或者null表示。
假設節點的索引值為index,那麼節點的左子節點是 2index+1,節點的右子節點是 2index+2,它的父節點是 (index-1)/2。
在大多數情況下,使用陣列表示樹效率是很低的,不滿的節點和刪除掉的節點都會在陣列中留下洞,浪費儲存空間。更壞的是,刪除節點如果要移動子樹的話,子樹中的每個節點都要移到陣列中新的位置,這是很費時的。
不過如果不允許刪除操作,陣列表示可能會很有用,尤其是因為某種原因要動態的為每個位元組分配空間非常耗時。
10.完整的BinaryTree程式碼
Node.java
package com.ys.tree;
public class Node {
int data; //節點資料
Node leftChild; //左子節點的引用
Node rightChild; //右子節點的引用
boolean isDelete;//表示節點是否被刪除
public Node(int data){
this.data = data;
}
//列印節點內容
public void display(){
System.out.println(data);
}
}
Tree.java
package com.ys.tree;
public interface Tree {
//查詢節點
public Node find(int key);
//插入新節點
public boolean insert(int data);
//中序遍歷
public void infixOrder(Node current);
//前序遍歷
public void preOrder(Node current);
//後序遍歷
public void postOrder(Node current);
//查詢最大值
public Node findMax();
//查詢最小值
public Node findMin();
//刪除節點
public boolean delete(int key);
//Other Method......
}
BinaryTree.java
package com.ys.tree;
public class BinaryTree implements Tree {
//表示根節點
private Node root;
//查詢節點
public Node find(int key) {
Node current = root;
while(current != null){
if(current.data > key){//當前值比查詢值大,搜尋左子樹
current = current.leftChild;
}else if(current.data < key){//當前值比查詢值小,搜尋右子樹
current = current.rightChild;
}else{
return current;
}
}
return null;//遍歷完整個樹沒找到,返回null
}
//插入節點
public boolean insert(int data) {
Node newNode = new Node(data);
if(root == null){//當前樹為空樹,沒有任何節點
root = newNode;
return true;
}else{
Node current = root;
Node parentNode = null;
while(current != null){
parentNode = current;
if(current.data > data){//當前值比插入值大,搜尋左子節點
current = current.leftChild;
if(current == null){//左子節點為空,直接將新值插入到該節點
parentNode.leftChild = newNode;
return true;
}
}else{
current = current.rightChild;
if(current == null){//右子節點為空,直接將新值插入到該節點
parentNode.rightChild = newNode;
return true;
}
}
}
}
return false;
}
//中序遍歷
public void infixOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
System.out.print(current.data+" ");
infixOrder(current.rightChild);
}
}
//前序遍歷
public void preOrder(Node current){
if(current != null){
System.out.print(current.data+" ");
infixOrder(current.leftChild);
infixOrder(current.rightChild);
}
}
//後序遍歷
public void postOrder(Node current){
if(current != null){
infixOrder(current.leftChild);
infixOrder(current.rightChild);
System.out.print(current.data+" ");
}
}
//找到最大值
public Node findMax(){
Node current = root;
Node maxNode = current;
while(current != null){
maxNode = current;
current = current.rightChild;
}
return maxNode;
}
//找到最小值
public Node findMin(){
Node current = root;
Node minNode = current;
while(current != null){
minNode = current;
current = current.leftChild;
}
return minNode;
}
@Override
public boolean delete(int key) {
Node current = root;
Node parent = root;
boolean isLeftChild = false;
//查詢刪除值,找不到直接返回false
while(current.data != key){
parent = current;
if(current.data > key){
isLeftChild = true;
current = current.leftChild;
相關推薦
回爐篇11—資料結構(10)之二叉樹
前言:
接下來我們將會介紹另外一種資料結構——樹。二叉樹是樹這種資料結構的一員,後面我們還會介紹紅黑樹,2-3-4樹等資料結構。那麼為什麼要使用樹?它有什麼優點?
前面我們介紹陣列的資料結構,我們知道對於有序陣列,查詢很快,並介紹可以通過二分法查詢,但是想要在有
java資料結構與之二叉樹相關實現(第一篇:遍歷)
一、基本概念
每個結點最多有兩棵子樹,左子樹和右子樹,次序不可以顛倒。 性質:
非空二叉樹的第n層上至多有2^(n-1)個元素。
深度為h的二叉樹至多有2^h-1個結點。
滿二叉樹:所有終端都在同一層次,且非終端結點的度數為2。 在滿二叉
資料結構實驗之二叉樹六:哈夫曼編碼(SDUT 3345)
題解:離散中的“最小生成樹(最優樹)”。
#include <bits/stdc++.h>
using namespace std;
void qusort(int l, int r, int a[])
{
int x = a[l];
int i = l, j =
資料結構實驗之二叉樹一:樹的同構 (SDUT 3340)
題解:把原本結構體的左右子樹的型別定義成 int 型,用來存放這個結點的左右子樹的編號,分別建造兩棵二叉樹,按個比較,如果在第二棵樹中沒有找到,那麼就不用在判斷了。
#include <bits/stdc++.h>
using namespace std;
struct node
資料結構實驗之二叉樹八:(中序後序)求二叉樹的深度(SDUT 2804)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct node
{
char data ;
struct node *l,*r;
};
struct node *cr
資料結構實驗之二叉樹七:葉子問題(SDUT 3346)
#include <bits/stdc++.h>
using namespace std;
struct node
{
char data;
struct node *lc, *rc;
};
char a[100];
int num = 0;
struct node
資料結構實驗之二叉樹四:(先序中序)還原二叉樹 (SDUT 3343)
#include <bits/stdc++.h>
using namespace std;
struct node
{
char data;
struct node *lc, *rc;
};
char a[100],b[100];
int n;
struct node
資料結構實驗之二叉樹五:層序遍歷 (SDUT 3344)
#include <bits/stdc++.h>
using namespace std;
struct node
{
char data;
struct node *lc, *rc;
};
char s[505];
int num;
struct node *cre
資料結構實驗之二叉樹三:統計葉子數 SDUT 3342
#include <stdio.h>
#include <string.h>
struct node
{
char data;
struct node *l,*r;
};
struct node *root;
char st[51];
int i;
in
資料結構實驗之二叉樹二:遍歷二叉樹 SDUT 3341
#include <bits/stdc++.h>
using namespace std;
struct Tree
{
char data;
struct Tree *right;
struct Tree *left;
};
char str[55];
in
【二叉樹】SDUT 3342 資料結構實驗之二叉樹三:統計葉子數
Problem Description
已知二叉樹的一個按先序遍歷輸入的字元序列,如abc,,de,g,,f,,, (其中,表示空結點)。請建立二叉樹並求二叉樹的葉子結點個數。
Input
連續輸入多組資料,每組資料輸入一個長度小於50個字元的字串。
Output
輸出
SDUTOJ3344資料結構實驗之二叉樹五:層序遍歷
資料結構實驗之二叉樹五:層序遍歷
https://acm.sdut.edu.cn/onlinejudge2/index.php/Home/Contest/contestproblem/cid/2711/pid/3344
Time Limit: 1000 ms Memo
3340--資料結構實驗之二叉樹一:樹的同構
現給定兩棵樹,請你判斷它們是否是同構的。
Input
輸入資料包含多組,每組資料給出2棵二叉樹的資訊。對於每棵樹,首先在一行中給出一個非負整數N (≤10),即該樹的結點數(此時假設結點從0到N−1編號);隨後N行,第i行對應編號第i個結點,給出該結點中儲存的1
資料結構實驗之二叉樹三:統計葉子數
Problem Description
已知二叉樹的一個按先序遍歷輸入的字元序列,如abc,de,g,f, (其中,表示空結點)。請建立二叉樹並求二叉樹的葉子結點個數。
Input
連續輸入多組資料,每
資料結構實驗之二叉樹二:遍歷二叉樹
Problem Description
已知二叉樹的一個按先序遍歷輸入的字元序列,如abc,de,g,f, (其中,表示空結點)。請建立二叉樹並按中序和後序的方式遍歷該二叉樹。
Input
連續輸入多組資料,每組資料輸入一個長度小於50個字元的字串。
Outpu
資料結構實驗之二叉樹三:統計葉子數(有返回值版)
Problem Description
已知二叉樹的一個按先序遍歷輸入的字元序列,如abc,de,g,f, (其中,表示空結點)。請建立二叉樹並求二叉樹的葉子結點個數。
Input
連續輸入多組資料,每組資料輸入一個長度小於50個字元的字串。
Output
輸出
資料結構實驗之二叉樹四:(先序中序)還原二叉樹
Problem Description
給定一棵二叉樹的先序遍歷序列和中序遍歷序列,要求計算該二叉樹的高度。
Input
輸入資料有多組,每組資料第一行輸入1個正整數N(1 <= N <=
資料結構實驗之二叉樹七:葉子問題
Problem Description
已知一個按先序輸入的字元序列,如abd,eg,cf,(其中,表示空結點)。請建立該二叉樹並按從上到下從左到右的順序輸出該二叉樹的所有葉子結點。
Input
輸入資
資料結構實驗之二叉樹三:統計葉子數 SDUT 3342
#include <stdio.h>
#include <string.h>
struct node
{
char data;
struct node *l,*
資料結構實驗之二叉樹八:(中序後序)求二叉樹的深度
Problem Description
已知一顆二叉樹的中序遍歷序列和後序遍歷序列,求二叉樹的深度。
Input
輸入資料有多組,輸入T,代表有T組資料。每組資料包括兩個長度小於50的字串,第一個字串表示二叉樹的中序遍歷,第二個表示二叉樹的後序遍歷。
Output