【資料結構】二叉樹(順序儲存、鏈式儲存)的JAVA程式碼實現
二叉樹是一種非線性的資料結構。它是由n個有限元素的集合,該集合或者為空、或者由一個稱為根(root)的元素及兩顆不相交的、被分別稱為左子樹、右子樹的二叉樹組成。當集合為空時,稱該二叉樹為空二叉樹。在二叉樹中,一個元素也可以稱做一個結點。
二叉樹是有序的,即若將其左右兩個子樹顛倒,就成為另一棵不同的二叉樹。這也就意味著,即使某棵二叉樹的樹中結點只有一棵子樹,也同樣要區分是左子樹還是右子樹。
二叉樹的基本概念
- 結點的度:結點所擁有的子樹的個數;
- 葉結點:度為0的結點,或稱為終端結點;
- 分枝結點:度不為0的結點,或稱為非終端結點;
- 左孩子、右孩子、雙親
- 路徑、路徑長度
- 結點的層數:規定樹的根結點的層數為1,其他結點的層數等於它雙親結點的層數加1;
- 樹的深度:樹中所有結點的最大層數;
- 樹的度:樹中各結點度的最大值;
- 滿二叉樹:一棵二叉樹中,如果所有的分枝結點都存在左子樹和右子樹,且所有的葉子結點都在同一層上;
- 完全二叉樹:一棵深度為k的有n個結點的二叉樹,對樹中的結點按從上至下、從左至右的順序進行編號,如果編號為i的結點與滿二叉樹中編號為i的結點在二叉樹中的位置相同。
完全二叉樹的特點:葉子結點只能出現在最下層和次下層,且最下層的葉子結點集中在樹的左部。
二叉樹的主要性質
- 一棵非空二叉樹的第i層最多有2^(i-1)個結點。
- 一顆深度為k的二叉樹中,最多具有2^k-1個結點。
- 對於一棵非空的二叉樹,如果葉子結點的數目是n0,度數為2的結點樹為n2,則有n0=n2+1。
- 具有n個結點的完全二叉樹的深度為[lbn]+1。
- 對於具有n個結點的完全二叉樹,如果對樹中的結點按從上至下、從左至右的順序從1開始順序進行編號,則對於任何的序號為i的結點,有如下的情況:
- 如果i>1,則序號為i的結點的雙親節點的序號為n/2(“/”代表整除);如果i=1,則它是根結點,無雙親節點。
- 如果2i<n,則序號為i的結點的左孩子結點的序號為2i;如果2i>n,則序號為i的結點無左孩子。
- 如果2i+1<n,則序號為i的結點的右孩子結點的序號為2i+1;如果2i+1>n,則序號為i的結點無右孩子。
關於二叉樹的第三點性質的證明:
從節點上來講:n=n0+n1+n2;
從路徑上來講:n-1=n1+2*n2(等式左邊是進入分支,等式右邊為發出分支);
二叉樹的抽象資料型別
- 資料元素:具有相同元素(結點)的資料集合;
- 資料結構:結點之間通過左右引用維護之間的關係;
- 資料操作:對二叉樹的基本操作定義在IBiTree中,程式碼如下:
public interface IBiTree<E> {
void create(E val, Node<E>l, Node<E> r); //以val為根節點元素,l和r為左右子樹構造二叉樹
void insertL(E val, Node<E> p); //將元素插入p的左子樹
void insertR(E val, Node<E> p); //將元素插入p的右子樹
Node<E> deleteL(Node<E> p) ; //刪除p的左子樹
Node<E> deleteR(Node<E> p); //刪除p的右子樹
Node<E> search(Node<E> root, E value) ; //在root樹中查詢結點元素為value的結點
void traverse(Node<E> root, int i); //按某種方式i遍歷root二權樹
}
二叉樹的實現
二叉樹的順序儲存
二叉樹的順序儲存,是用一組連續的儲存單元存放二叉樹的結點,通常按照二叉樹結點從上至下、從左至右的順序儲存。這樣結點在儲存位置上的前趨後繼關係並不一定就是他們在邏輯上的鄰接關係。然而只有通過一些方法確定某結點在邏輯上的前趨結點和後繼結點,這樣的儲存方式才有意義。
因此根據二叉樹的性質,完全二叉樹和滿二叉樹採用順序儲存的方式比較合適,樹中結點的序號可以唯一地反映結點之間的邏輯關係,這樣既能最大可能的節省儲存空間,又可以利用陣列元素的下標值確定結點在二叉樹中的位置以及結點之間的關係。
但如果是一般的二叉樹,如果還按照結點從上至下、從左至右的順序儲存將結點儲存在一個一維陣列中,則陣列元素下標之間不一定能反映結點之間的邏輯關係,需要新增一些不存在的空節點,使之成為一棵安全二叉樹或者滿二叉樹。這會造成空間的大量浪費,最壞的情況是右單支樹。
二叉樹的鏈式儲存
二叉樹的鏈式儲存結構就是用連結串列來表示一棵二叉樹,即用連結串列來指示元素之間的邏輯關係。通常有兩種儲存形式:
- 連結串列中每個結點由三個域組成,除了資料域之外,還有兩個指標域,分別用來給出該結點的左孩子和右孩子所在的儲存地址。
- 連結串列中每個結點由四個域組成,除了資料域之外,還有三個指標域,分別用來給出該結點的左孩子、右孩子和雙親結點所在的儲存地址。
兩種方式的區別和單鏈表和雙向連結串列的區別有些類似,下面以第一種方式為例來實現二叉樹的鏈式儲存:
public class Node<E>{
private E data; //資料域
private Node<E> lchild; //左孩子
private Node<E> rchild; //右孩子
//建構函式
public Node(E val, Node<E> lp, Node<E> rp){
data = val;
lchild = lp;
rchild = rp;
}
//建構函式
public Node(Node<E> lp, Node<E> rp){
this(null,lp,rp);
}
//建構函式
public Node(E val){
this(val,null,null);
}
//建構函式
public Node() {
this(null);
}
//資料屬性
public E getData() {
return data;
}
public void setData(E data) {
this.data = data;
}
//左孩子
public Node<E> getLchild() {
return lchild;
}
public void setLchild(Node<E> lchild) {
this.lchild = lchild;
}
//右孩子
public Node<E> getRchild() {
return rchild;
}
public void setRchild(Node<E> rchild) {
this.rchild = rchild;
}
}
public class LinkBiTree<E> implements IBiTree<E> {
private Node<E> head; // 連結串列頭引用指標
public Node<E> getHead() {
return head;
}
// 建構函式,生成一棵以val為根結點資料域資訊,以二叉樹lp和rp為左子樹和右子樹的二叉樹。
public LinkBiTree(E val, Node<E> lp, Node<E> rp) {
Node<E> p = new Node<E>(val, lp, rp);
head = p;
}
// 建構函式,生成一棵以val為根結點資料域資訊的二叉樹
public LinkBiTree(E val) {
this(val, null, null);
}
// 建構函式,生成一棵空的二叉樹
public LinkBiTree() {
head = null;
}
// 判斷是否是空二叉樹
public boolean isEmpty() {
return head == null;
}
// 獲取根結點
public Node<E> Root() {
return head;
}
// 獲取結點的左孩子結點
public Node<E> getLchild(Node<E> p) {
return p.getLchild();
}
// 獲取結點的右孩子結點
public Node<E> getRchild(Node<E> p) {
return p.getRchild();
}
// 建立二叉樹
public void create(E val, Node<E> l, Node<E> r) {
Node<E> p = new Node<E>(val, l, r);
head = p;
}
// 將結點p的左子樹插入值為val的新結點,
// 原來的左子樹成為新結點的左子樹
public void insertL(E val, Node<E> p) {
Node<E> tmp = new Node<E>(val);
tmp.setLchild(p.getLchild());
p.setLchild(tmp);
}
// 將結點p的右子樹插入值為val的新結點,
// 原來的右子樹成為新結點的右子樹
public void insertR(E val, Node<E> p) {
Node<E> tmp = new Node<E>(val);
tmp.setRchild(p.getRchild());
p.setRchild(tmp);
}
// 若p非空,刪除p的左子樹
public Node<E> deleteL(Node<E> p) {
if ((p == null) || (p.getLchild() == null)) {
return null;
}
Node<E> tmp = p.getLchild();
p.setLchild(null);
return tmp;
}
// 若p非空,刪除p的右子樹
public Node<E> deleteR(Node<E> p) {
if ((p == null) || (p.getRchild() == null)) {
return null;
}
Node<E> tmp = p.getRchild();
p.setRchild(null);
return tmp;
}
// 編寫演算法,在二叉樹中查詢值為value的結點
public Node<E> search(Node<E> root, E value) {
Node<E> p = root;
if (p == null) {
return null;
}
if (!p.getData().equals(value)) {
return p;
}
if (p.getLchild() != null) {
return search(p.getLchild(), value);
}
if (p.getRchild() != null) {
return search(p.getRchild(), value);
}
return null;
}
// 判斷是否是葉子結點
public boolean isLeaf(Node<E> p) {
return ((p != null) && (p.getLchild() == null) && (p.getRchild() == null));
}
// 中序遍歷
public void inorder(Node<E> p) {
}
// 前序遍歷
public void preorder(Node<E> p) {
}
// 後序列遍歷
public void postorder(Node<E> p) {
}
// 層次遍歷
public void levelOrder(Node<E> root) {
}
// 遍歷二叉樹
public void traverse(Node<E> root, int i) {
}
其中關於連結串列的幾種遍歷方式(中序遍歷、前序遍歷、後序遍歷、層次遍歷),在下一篇文章中會具體地講到。