物有本末,事有始終,知所先后,則近道矣。-----題記。
BotWong半路入行it做碼農,也就半年時間,竟“不知天高地厚”地來到了深圳闖天下。一口氣投了百個簡歷,一周后終於有公司邀約面試,除了基礎的java語法和開發經驗,大一點的公司都會出幾道題給你做(算法題)。BotWong是一頭霧水,而且心里很生氣!氣!氣!氣!以前自己學的是“人類心理學理論與實踐”專業,唯一的計算機基礎也就是用過word、excel給客戶報過價。自己硬着頭皮把java語法和javaEE框架學了一遍,像我這樣的個例已經算是出類拔萃的了,為什么還要對我這么苛刻,哼!BotWong從那以后就開始討厭那些喜歡考試的公司,並發誓以后再也不去那樣的公司面試!
我和BotWong是好朋友,他提出了自己的困惑:到底要不要學這些看着“沒用”的算法,問我該怎么看待這件事,該如何做。好在BotWang還是個頭腦清醒的人,要不然也不會問這樣的問題了。我看他挺有誠意,就把自己以前寫的一些代碼,其中就包含一些算法,拿出來泛泛地說了一遍,建議他自己都去實現一遍。BotWang消失了三月,再見之時,已能感知其功力大漲,已非三月前的那個BotWong了。我心甚慰,特此將自己以前寫過的集合類都貼出來,方便自己和網上的寶寶們隨時翻看,隨時補充能量。
一、二叉樹類:
package tree; /** * 二叉樹數據載體類 * @author tery * * @param <T> */ public class BinaryTreeNode<T> { private T data;//權值 private BinaryTreeNode<T> left;//左孩子 private BinaryTreeNode<T> right;//右孩子 public BinaryTreeNode(T data){ this.data=data; left=right=null; } public T getData(){ return this.data; } public void setData(T data){ this.data=data; } public BinaryTreeNode<T> getLeft() { return left; } public void setLeft(BinaryTreeNode<T> left) { this.left = left; } public BinaryTreeNode<T> getRight() { return right; } public void setRight(BinaryTreeNode<T> right) { this.right = right; }
/**
*插入權值
*/
@SuppressWarnings("unchecked") public BinaryTreeNode<T> insert(BinaryTreeNode<T> node,Integer data){ if(node==null){ return new BinaryTreeNode<T>((T)data); }
//如果當前節點的權值大於data,那么插入到左孩子節點上 if(Integer.valueOf(node.getData().toString())>data){ node.left=insert(node.getLeft(),data); }
//如果當前節點的權值大於data,那么插入到左孩子節點上
if(Integer.valueOf(node.getData().toString())<data){ node.right=insert(node.getRight(),data); }
//相等拋異常
if(Integer.valueOf(node.getData().toString())==data){
throw new IllegalArgumentException("the data:"+data+"is already exsist in the tree");
}
return node;
}
}
insert方法中涉及到的遞歸:
0: aload_1 1: ifnonnull 13 4: new #1 // class tree/BinaryTreeNode 7: dup 8: aload_2 9: invokespecial #45 // Method "<init>":(Ljava/lang/Object;)V 12: areturn 13: aload_1 14: getfield #20 // Field data:Ljava/lang/Object; 23: aload_2 30: if_icmple 79 36: getfield #24 // Field left:Ltree/BinaryTreeNode; 39: aload_2 40: invokevirtual #53 // Method insert:(Ltree/BinaryTreeNode;Ljava/lang/Object;)Ltree/BinaryTreeNode; 43: putfield #24 // Field left:Ltree/BinaryTreeNode; 79: aload_1 80: areturn
上面是insert方法的jvm執行的指令,我摘抄了部分關鍵點說明一下遞歸是怎么做的:
insert函數棧中的局部變量表中的參數:第一個是當前樹節點對象(暫且稱為ref0),第二個是函數中傳進來的node對象的引用(稱為ref1),第三個是傳進來的常量data(在常量池中#20)
0: aload_1 將第一個引用類型局部變量推送至棧頂,那個引用也就是insert方法中的node對象的引用(ref1)
1 : ifnonnull 13 如果ref1不為空,則跳到13行執行
4-12 : 如果是空,則此節點即是我們要插入的節點,調用它的構造函數<init>方法,將data值賦給它,然后areturn(將這個新生成的對象引用返回)
13 : aload_1 將ref1壓入棧頂
14:getfield 將常量池中當前對象ref1的data的值壓入棧頂
23:aload_2 將傳入的data值壓入棧頂
30:if_icmple 79,如果傳入的data值小於ref1的權值,那么就跳到79執行
36:getfield #24 ,將常量池中#24,即left壓入棧中,即left常量的引用
39:aload_2 將data值壓入棧中。實際上36、39兩步的指令都是在為下面40行做准備,40行調用insert方法,傳遞給它需要的參數
40:invokevirtrual #53,調用insert方法,將36、39行准備的兩個參數帶過去
43:putfield #24 ,給當前實例ref1的left字段賦值
79:aload_1 ,將當前實例ref1壓入棧中
80:areturn ,將棧頂的ref1返回
如果你不太懂,我還給你畫了個圖,幫助你理解:
假設jvm只執行了在左邊樹插入的遞歸步驟:
上面的字節碼指令和圖解是截取了部分關鍵點進行講解的,如果想進一步探討的寶寶們,可以聯系我詳細討論。
三、二叉樹工具類:
1. 前根序遍歷:先遍歷根結點,然后遍歷左子樹,最后遍歷右子樹。(ABDHECFG)
2.中根序遍歷:先遍歷左子樹,然后遍歷根結點,最后遍歷右子樹。(HDBEAFCG)
3.后根序遍歷:先遍歷左子樹,然后遍歷右子樹,最后遍歷根節點(HDEBFGCA)
package tree; import java.util.ArrayList; import java.util.List; import java.util.Stack; public class BinaryTreeUtil { /** * 用遞歸的方式實現二叉樹的前序遍歷 * @param root * @return */ public static<T> List<T> preOrderVisit(BinaryTreeNode<T> root){ List<T> result=new ArrayList<>(); preOrderVisit(root,result); return result; } private static<T> void preOrderVisit(BinaryTreeNode<T> node, List<T> result) { //如果節點為空,返回 if(node==null){ return; } //不為空,則加入節點的值 result.add(node.getData()); //先遞歸左孩子 preOrderVisit(node.getLeft(),result); //再遞歸右孩子 preOrderVisit(node.getRight(),result); } /** * 用遞歸的方式實現二叉樹的中序遍歷 * @param root * @return */ public static<T> List<T> inOrderVisit(BinaryTreeNode<T> root){ List<T> result=new ArrayList<T>(); inOrderVisit(root,result); return result; } private static<T> void inOrderVisit(BinaryTreeNode<T> node, List<T> result) { if(node==null){ return; } inOrderVisit(node.getLeft(),result); result.add(node.getData()); inOrderVisit(node.getRight(),result); } /** * 用遞歸的方式實現二叉樹的后遍歷 * @param root * @return */ public static<T> List<T> postOrderVisit(BinaryTreeNode<T> root){ List<T> result=new ArrayList<T>(); postOrderVisit(root,result); return result; } private static<T> void postOrderVisit(BinaryTreeNode<T> node, List<T> result) { if(node==null){ return; } postOrderVisit(node.getLeft(),result); postOrderVisit(node.getRight(),result); result.add(node.getData()); } /** * 用非遞歸的方式實現前序遍歷 * @param root * @return */ public static<T> List<T> preOrderVisitWithoutRecursion(BinaryTreeNode<T> root){ List<T> result=new ArrayList<T>(); Stack<BinaryTreeNode<T>> stack=new Stack<>(); if(root!=null){ stack.push(root); } while(!stack.isEmpty()){ BinaryTreeNode<T> node=stack.pop(); result.add(node.getData()); if(node.getRight()!=null){ stack.push(node.getRight()); } if(node.getLeft()!=null){ stack.push(node.getLeft()); } } return result; } /** * 用非遞歸的方式實現中序遍歷 * @param root * @return */ public static<T> List<T> inOrderVisitWithoutRecursion(BinaryTreeNode<T> root){ List<T> result=new ArrayList<T>(); Stack<BinaryTreeNode<T>> stack=new Stack<>(); BinaryTreeNode<T> node=root; while(node!=null || !stack.isEmpty()){ while(node!=null){ stack.push(node); node=node.getLeft(); } BinaryTreeNode<T> currentNode=stack.pop(); result.add(currentNode.getData()); node=currentNode.getRight(); } return result; }
二叉樹部分上面總共列舉了15個方法,主要的思想還是用遞歸。關於遞歸,為了讓寶寶們明白是怎么回事,我也是花了點心思去解釋的,請各位再深入思考一下。里面涉及到了jvm函數調用層面的知識,如果不懂,建議大家去看看jvm的書,了解jvm怎么執行代碼,那就可以輕輕松松地理解遞歸到底是怎么執行的了。