遞迴(recursion)演算法與二叉樹(1)
筆者按:曾經剛開始學習資料結構和演算法時,總會為簡潔雋永的遞迴程式碼而驚歎,也想寫出如此優雅的程式碼,但是思考過程真的實屬不易!!!那時候遞迴都會盡量用顯式棧來規避。
生活中的遞迴!
首先,對遞迴要有一個類似盜夢空間或者平行世界的認識,就像現實世界和你的夢境,當你開始進入夢境,這兩個世界完全隔離(平行世界和平行萬物)除了你的(思想、記憶),思想記憶可以看成從現實世界傳到夢境裡的(傳入引數),而夢中驚醒又可以看成噩夢驚醒,將噩夢裡的記憶經歷帶回現實世界(返回引數),如果夢裡的自己又開始做夢進入第三個空間,就這樣逐層遞進,然後逐層歸還,這大概就是遞迴的“奧義”!!!這就是我們生活中的遞迴
總的原則:
在樹的各種演算法中,我們要確立這樣的思想,一個節點既代表節點本身,又代表以該節點為節點的一棵樹,適用於父節點的演算法流程同樣適用於它,哪怕它是空節點!!!對於空節點,it's time to return!!!
1.WarmUp:
先看一個簡單的:樹的高度求法——葉子結點高度為1,然後逐層加一;某個節點的高度是其左子節點高度和右子節點高度的較大值+1,對於葉子結點下的空節點返回0。(還記得extended binary tree擴充套件二叉樹的定義嗎,這個定義使得我們的演算法可以處理空節點的情況,也就是空節點和普通節點都是同等的)
public int height() { // TODO Auto-generated method stub return height(this.root); } //遞迴呼叫height求當前節點的高度 public int height(BinaryNode<T> p) { if(p==null) return 0; int lcount=height(p.left); int rcount=height(p.right); return (lcount>=rcount)?lcount+1:rcount+1; }
2再來看看二叉樹的遍歷
中序(根)遍歷、先序(根)遍歷、後根(序)遍歷、層次遍歷。
舉個栗子瞧瞧看:
2.1先序(根)遍歷
遍歷的先後如圖:
需要注意的是藍線描述的是訪問節點的先後過程,左子樹遍歷完後直接回到當前遞迴層訪問的根節點,然後再去遍歷該根節點的右子樹。
遍歷過程就是一個深度優先搜尋的過程,借用棧的非遞迴寫法:
/*用非遞迴先根遍歷以node為根節點的(子)樹
*在二叉樹先序遍歷非遞迴演算法中,先將根結點壓棧,在棧不為空的時候執行迴圈:讓棧頂元素p出棧,訪問棧頂元素p,
*如果p的右孩子不為空,則讓其右孩子先進棧,如果p的左孩子不為空,則再讓其左孩子進棧
*(注意:進棧順序一定是先右孩子,再左孩子)。
*/
public void preOrderNoRecursive(BinaryNode<T> node)
{
LinkedStack<BinaryNode<T>> stack=new LinkedStack<BinaryNode<T>>();
BinaryNode<T> p=node;
//停止while迴圈的條件是棧為空並且p指向null
while(!stack.isEmpty() || p!=null)
{
while(p!=null)
{
System.out.print(p.data);
stack.push(p);//入棧操作表示訪問該節點
p=p.left;
}
if(!stack.isEmpty())
{
p=stack.getTop();
stack.pop();
p=p.right;
}
}
}
遞迴思路:訪問到某個節點時就直接列印或者儲存記錄,然後以左子節點作為遞迴函式的引數,再以右子節點作為遞迴函式的引數,當然遇到空節點就return。
public void preOrder() {
//先根次序遍歷:訪問根節點,遍歷左子樹,遍歷右子樹 TODO Auto-generated method stub
System.out.print("先根次序遍歷二叉樹");
preOrder(root);
System.out.println();
}
public void preOrder(BinaryNode<T> p)
{
if(p!=null)
{
System.out.print(p.data.toString()+" ");
preOrder(p.left);
preOrder(p.right);
}
else
return;
}
2.2中序(根)遍歷
遞迴思路:以左子節點作為遞迴函式的引數,然後訪問到某個節點時就直接列印或者儲存記錄,再以右子節點作為遞迴函式的引數,當然遇到空節點就return。
非遞迴中序遍歷:
//用非遞迴中根遍歷以node為根節點的(子)樹
public void inOrderNoRecursive(BinaryNode<T> node)
{
System.out.println("非遞迴中根遍歷: ");
LinkedStack<BinaryNode<T>> stack=new LinkedStack<BinaryNode<T>>();
BinaryNode<T> p=node;
while(!stack.isEmpty() || p!=null )
{
if(p!=null)
{
stack.push(p);
p=p.left;
}
else
{
p=stack.pop();
System.out.print(p.data+" ");
p=p.right;
}
}
}
public void inOrder() {
//中根次序遍歷:遍歷左子樹,訪問根節點,遍歷右子樹 TODO Auto-generated method stub
System.out.print("中根次序遍歷二叉樹");
inOrder(root);
System.out.println();
}
public void inOrder(BinaryNode<T> p)
{
if(p!=null)
{
preOrder(p.left);
System.out.print(p.data.toString()+" ");
preOrder(p.right);
}
}
2.3後序(根)遍歷
遞迴思路:以左子節點作為遞迴函式的引數,再以右子節點作為遞迴函式的引數,然後訪問到某個節點時就直接列印或者儲存記錄。當然遇到空節點就return。
@Override
public void postOrder() {
// 後根次序遍歷:遍歷左子樹,遍歷右子樹,訪問根節點TODO Auto-generated method stub
System.out.print("後根次序遍歷二叉樹");
postOrder(root);
System.out.println();
}
public void postOrder(BinaryNode<T> p)
{
if(p!=null)
{
postOrder(p.left);
postOrder(p.right);
System.out.print(p.data.toString()+" ");
}
}
非遞迴版本這裡提一下不詳細寫了:
//對於非遞迴後根遍歷,使用順序棧比
public void postOrderNoRecursive(BinaryNode<T> node)
{
System.out.println("非遞迴後根遍歷: ");
SeqStack<BinaryNode<T>> stack=new SeqStack<BinaryNode<T>>();
BinaryNode<T> p=node;
int[] tag=new int[this.count()];
//停止while迴圈的條件是棧為空並且p指向null
while(!stack.isEmpty() || p!=null)
{
while(p!=null)
{
stack.push(p);
tag[stack.getTopIndex()]=0;
p=p.left;
}
while(!stack.isEmpty() && tag[stack.getTopIndex()]==1)
{
System.out.print(stack.pop());
}
if(!stack.isEmpty())
{
p=stack.getTop();
tag[stack.getTopIndex()]=1;
p=p.right;
}
}
}
2.4層次遍歷
public void levelOrder(BinaryNode<T> node) {
// TODO Auto-generated method stub
LinkedQueue<BinaryNode<T>> q=new LinkedQueue<BinaryNode<T>>();
BinaryNode<T> p=node;
q.enqueue(p);
while(!q.isEmpty())
{
p=q.dequeu();
System.out.print(p.data.toString());
if(p.left!=null)
q.enqueue(p.left);
if(p.right!=null)
q.enqueue(p.right);
}
}
@Override
public void levelOrder()
{
levelOrder(this.root);
}
3.樹節點的增刪查改
3.1查詢——如果當前節點裡的值不是目標值則先查詢左子節點,再查詢右子節點。
public BinaryNode<T> search(T key) {
// TODO Auto-generated method stub
return search(this.root,key);
}
public BinaryNode<T> search(BinaryNode<T> p,T key)
{
if(p==null||key==null)
return null;
if(p.data.equals(key))
return p;
BinaryNode<T> find=search(p.left,key);
if(find==null)
find=search(p.right,key);
return find;
}
3.2在以p為根節點的樹中查詢node節點的位置——如果當前節點裡的值不是目標值則先查詢以左子節點為根節點的樹裡是否有目標值,再查詢以右子節點為根節點的樹裡是否有目標值。
public BinaryNode<T> getParent(BinaryNode<T> node) {
// TODO Auto-generated method stub
return getParent(this.root,node);
}
//查詢以p為根的子樹中節點node的父節點
public BinaryNode<T> getParent(BinaryNode<T> p,BinaryNode<T> node)
{
if(p==null)
return null;
if(p.left==node||p.right==node)
return p;
BinaryNode<T> find=getParent(p.left,node);
if(find==null)
find=getParent(p.right,node);
return find;
}
3.3刪除
//插入x作為節點p的左(右)子節點,如果p節點已經存在對應的左(右)節點,則原左(右)節點作為插入節點的左(右)子節點
@Override
public BinaryNode<T> insertChild(BinaryNode<T> p, T x, boolean leftChild) {
// TODO Auto-generated method stub
if(p==null || x==null)
return null;
BinaryNode<T> new_node=new BinaryNode<T>(x);
if(leftChild)
{
if(p.left==null)
p.left=new_node;
else
{
new_node.left=p.left;
p.left=new_node;
}
}
else
{
if(p.right==null)
p.right=new_node;
else
{
new_node.right=p.right;
p.right=new_node;
}
}
return new_node;
}
//刪除節點p的左(右)子節點,這裡規定將以p的左(右)子節點為根節點的樹全部刪去
@Override
public void removeChild(BinaryNode<T> p, boolean leftChild) {
// TODO Auto-generated method stub
if(p!=null)
{
if(leftChild)
p.left=null;
else
p.right=null;
}
}
@Override
public void removeAll() {
// TODO Auto-generated method stub
this.root=null;
}