樹
樹
樹是一種重要的數(shù)據(jù)結(jié)構(gòu),通過鏈表建立一棵樹:
//樹節(jié)點的定義
public class TreeNode<T> {
public T s;
private TreeNode<T> parent;
public List<TreeNode<T>> nodeList;
public TreeNode(T str){
s = str;
parent = null;
nodeList = new ArrayList<TreeNode<T>>();
}
public TreeNode<T> getParent(){
return parent;
}
}
用鏈表的結(jié)構(gòu)構(gòu)建樹幕侠,就是使樹的每一條分支都建立一個鏈表來存儲它的子節(jié)點缤底。所以要在節(jié)點的定義中加上List<TreeNode<T>> nodeList
的定義
//樹的定義
public class Tree<T> {
public TreeNode<T> root;
//添加樹節(jié)點
public void addNode(TreeNode<T> node,T newNode){
if(node==null){
if(root==null){
root = new TreeNode<T>(newNode);
}
}else{
TreeNode<T> tn = new TreeNode<T>(newNode);
node.nodeList.add(tn);
}
}
//查找樹節(jié)點
public TreeNode<T> search(TreeNode<T> node,T newNode){
TreeNode<T> temp = null;
if(node.s.equals(newNode)){
return node;
}
for(int i=0;i<node.nodeList.size();i++){
temp = search(node.nodeList.get(i),newNode);
if(temp!=null){
break;
}
}
return temp;
}
public TreeNode<T> getNode(T newNode){
return search(root, newNode);
}
//輸出樹節(jié)點
public void showNode(TreeNode<T> node){
if(null != node){
System.out.println(node.s.toString());
for(int i = 0; i < node.nodeList.size(); i++){
System.out.println(i);
showNode(node.nodeList.get(i));
}
}
}
}
在search方法中,用了for循環(huán)加遞歸的方法來查找節(jié)點暇检。node.nodeList.get(i)
獲得的是該父節(jié)點子樹中的節(jié)點。通過for循環(huán)遍歷這條分支婉称。
輸出節(jié)點也類似块仆,在非二叉樹等的普通樹結(jié)構(gòu)里,想要遍歷整個樹結(jié)構(gòu)王暗,for循環(huán)加遞歸這一組合是很好的方式悔据。
最后可以加上主函數(shù)進行測試:
public class app {
public static void main(String[] args) {
Tree<String> tree = new Tree<String>();
tree.addNode(null, "節(jié)點1");
tree.addNode(tree.getNode("節(jié)點1"), "節(jié)點2");
tree.addNode(tree.getNode("節(jié)點1"), "節(jié)點3");
tree.addNode(tree.getNode("節(jié)點2"), "節(jié)點4");
tree.addNode(tree.getNode("節(jié)點2"), "節(jié)點5");
tree.addNode(tree.getNode("節(jié)點3"), "節(jié)點6");
tree.addNode(tree.getNode("節(jié)點3"), "節(jié)點7");
tree.showNode(tree.root);
}
}
二叉樹
二叉樹是樹的一種特殊形式,每個節(jié)點至多有兩個子樹俗壹。二叉樹有如下一些性質(zhì)(二叉樹的深度和高度的定義不同教材也不完全相同科汗,有從0開始也有從1開始,這里根節(jié)點的深度和葉子結(jié)點的高度都為1開始算):
- 在非空的二叉樹中策肝,第i層至多有2i-1個節(jié)點
- 深度為h的二叉樹最多有2h-1個節(jié)點
- n0=n2+1肛捍,其中n0為葉子結(jié)點的個數(shù),n2為度為2的節(jié)點個數(shù)
- n個節(jié)點的完全二叉樹的深度:log2(n+1)
- 對含有n個節(jié)點的完全二叉樹按照從上到下之众,從左至右的方式編號拙毫,其中第i個節(jié)點有以下性質(zhì):
- 若i>1,其父節(jié)點為i/2
- 若2i<n棺禾,其左孩子為2i缀蹄,否則該節(jié)點無左子樹
- 若2i+1<n,其右孩子為2i+1,否則該節(jié)點無右字樹
完全二叉樹和滿二叉樹缺前,完全二叉樹是除最下面一層外所有層節(jié)點都為滿的蛀醉,最下面一層所有節(jié)點都集中在左邊。滿二叉樹是這顆二叉樹節(jié)點達到最大值衅码。
通過鏈表的形式建立一顆二叉樹拯刁,并對二叉樹進行遍歷等操作:
public class Node {
private int data;
private Node leftChild;
private Node rightChild;
public Node(int data,Node left,Node right){
this.leftChild = left;
this.rightChild = right;
this.data = data;
}
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public Node getLeftChild() {
return leftChild;
}
public void setLeftChild(Node leftChild) {
this.leftChild = leftChild;
}
public Node getRightChild() {
return rightChild;
}
public void setRightChild(Node rightChild) {
this.rightChild = rightChild;
}
}
二叉樹的遍歷有遞歸和非遞歸兩種方法,遞歸的方法很簡單逝段。
//前序遍歷
public static void preOrderRecusion(Node node){
if(node!=null){
show(node);//打印node節(jié)點
preOrderRecusion(node.getLeftChild());
preOrderRecusion(node.getRightChild());
}
}
//中序遍歷
public static void inOrderRecusion(Node node){
if(node!=null){
inOrderRecusion(node.getLeftChild());
show(node);
inOrderRecusion(node.getRightChild());
}
}
//后序遍歷
public static void postOrderRecusion(Node node){
if(node!=null){
postOrderRecusion(node.getLeftChild());
postOrderRecusion(node.getRightChild());
show(node);
}
}
非遞歸遍歷的前序和中序類似垛玻,只是輸出換了個位置,后序稍有不同:
//非遞歸前序遍歷
public static void preOrder(Node p){
//利用棧后進先出的原理來實現(xiàn)
Stack<Node> stack = new Stack<Node>();
Node node = p;
while(node!=null||stack.size()>0){
//先輸出左子樹并將左子樹入棧
while(node!=null){
show(node);
stack.push(node);
node = node.getLeftChild();
}
//從棧中取出左子樹的點奶躯,然后去找他們的右子樹進行循環(huán)
if(stack.size()>0){
node = stack.pop();
node = node.getRightChild();
}
}
}
//非遞歸中序遍歷
public static void inOrder(Node p){
Stack<Node> stack = new Stack<Node>();
Node node = p;
while(node!=null||stack.size()>0){
//循環(huán)將左子樹放入棧中
while(node!=null){
stack.push(node);
node = node.getLeftChild();
}
//先取出最左節(jié)點然后輸出帚桩,然后訪問右子樹
if(stack.size()>0){
node = stack.pop();
show(node);
node = node.getRightChild();
}
}
}
//非遞歸后序遍歷
public static void postOrder(Node p){
Stack<Node> stack = new Stack<Node>();
//建立一個中間棧來按后續(xù)遍歷順序存儲樹節(jié)點
Stack<Node> temp = new Stack<Node>();
Node node = p;
while(node!=null||stack.size()>0){
//把當(dāng)前節(jié)點和右子樹存到兩個棧中
while(node!=null){
temp.push(node);
stack.push(node);
node = node.getRightChild();
}
//出棧取左子樹,再進入上面循環(huán)嘹黔,這樣stack棧不斷pop()和push()账嚎,而temp一直在push()
if(stack.size()>0){
node = stack.pop();
node = node.getLeftChild();
}
}
//最后循環(huán)輸出已經(jīng)按照后序的順序存儲的temp棧
while(temp.size()>0){
node = temp.pop();
show(node);
}
}
關(guān)于二叉樹的其他一些操作:
//返回葉子結(jié)點數(shù)量
public static int leafNum(Node node){
if(node!=null){
if(node.getLeftChild()==null&&node.getRightChild()==null){
return 1;
}
return leafNum(node.getLeftChild())+leafNum(node.getRightChild());
}
return 0;
}
//求二叉樹深度
public static int deep(Node node){
int h1,h2;
if(node==null){
return 0;
}
else{
h1 = deep(node.getLeftChild());
h2 = deep(node.getRightChild());
return h1>h2?h1+1:h2+1;
}
}
//層次遍歷二叉樹
public static void levelOrder(Node node){
if(node==null){
return;
}
Queue<Node> queue = new LinkedList<Node>();
queue.add(node);
while(!queue.isEmpty()){
//從隊列中取出并刪除第一個節(jié)點
Node temp = queue.poll();
show(temp);
if(temp.getLeftChild()!=null){
queue.add(temp.getLeftChild());
}
if(temp.getRightChild()!=null){
queue.add(temp.getRightChild());
}
}
}
二叉排序樹
二叉排序樹又叫二叉搜索樹或二叉查找樹±苈空樹可以是二叉排序樹郭蕉,或者具有以下性質(zhì)的二叉樹稱為二叉排序樹:
1.若左子樹不空,則左子樹上所有節(jié)點的值小于其根節(jié)點
- 若右子樹不空浙值,則右子樹上所有節(jié)點的值大于其根節(jié)點
- 左恳不、右子樹也分別為二叉排序樹
二叉排序樹和二分查找類似,插入开呐、查找的時間復(fù)雜度均為log2n烟勋,但當(dāng)二叉排序樹成為線性的情況下復(fù)雜度會達到n。
若當(dāng)前的二叉排序樹為空筐付,則將插入的節(jié)點作為根節(jié)點卵惦。否則若插入的節(jié)點值小于根節(jié)點值,插入到左子樹中瓦戚。反之沮尿,插入到右子樹中。
public static void insert(Node node){
if(root==null){
root = node;
return;
}
Node current = root;
while(true){
//節(jié)點值小于當(dāng)前根節(jié)點
if(node.getData()<current.getData()){
//若其左子樹為空较解,則添加節(jié)點
if(current.getLeftChild()==null){
current.setLeftChild(node);
return;
}
//否則當(dāng)前結(jié)點變成其左孩子畜疾,然后繼續(xù)循環(huán)
current = current.getLeftChild();
}else if(node.getData()>current.getData()){
if(current.getRightChild()==null){
current.setRightChild(node);
return;
}
current = current.getRightChild();
}else{
return;
}
}
}
查找節(jié)點一樣根據(jù)當(dāng)前結(jié)點與根節(jié)點的大小,利用遞歸實現(xiàn):
//查找節(jié)點
public static Node find(Node node,int data){
if(node==null){
return null;
}else if(node.getData()==data){
return node;
}else if(node.getData()>data){
return find(node.getLeftChild(),data);
}else{
return find(node.getRightChild(),data);
}
}
二叉排序樹的刪除需要分為以下三種情況:
- 若刪除的節(jié)點為葉子結(jié)點印衔,則直接刪除啡捶,再修改其父指針為空。
- 若刪除的節(jié)點度為1奸焙,讓該節(jié)點的父節(jié)點指向該節(jié)點的孩子節(jié)點瞎暑,然后刪除該節(jié)點彤敛。
- 若刪除的節(jié)點度為2,則需要找到該節(jié)點中序遍歷的前驅(qū)或后繼節(jié)點了赌,也就是找到該節(jié)點左子樹中的最大值或右子樹中的最小值來代替該節(jié)點墨榄。
//刪除節(jié)點
public static void delete(Node node){
if(node==null){
return;
}
Node current = root;
Node parent = root;
boolean isLeftNode = true;
//while循環(huán)是為了得到該節(jié)點的父節(jié)點,和該節(jié)點是左還是右節(jié)點
while (current != node) {
parent = current;
if (node.getData() < current.getData()) {
isLeftNode = true;
current = current.getLeftChild();
} else {
isLeftNode = false;
current = current.getRightChild();
}
}
//節(jié)點為葉子節(jié)點
if (current.getLeftChild() == null && current.getRightChild() == null) {
if (current == root) { // 根節(jié)點就刪除整棵樹
root = null;
} else if (isLeftNode) { // 如果是左節(jié)點勿她,做節(jié)點指向空
parent.setLeftChild(null);
} else { // 如果是右節(jié)點袄秩,右節(jié)點指向空
parent.setRightChild(null);
}
}
//節(jié)點只有左節(jié)點
else if (current.getLeftChild() == null) {
if (current == root) {
root = current.getRightChild();
} else if (isLeftNode) {
parent.setLeftChild(current.getRightChild());
} else {
parent.setRightChild(current.getRightChild());
}
}
//節(jié)點只有右節(jié)點
else if (current.getRightChild() == null) {
if (current == root) {
root = current.getLeftChild();
} else if (isLeftNode) {
parent.setLeftChild(current.getLeftChild());
} else {
parent.setLeftChild(current.getLeftChild());
}
}
//有兩個孩子節(jié)點
else {
Node successor = findSuccessor(current);
if(current == root){//如果刪除的是根節(jié)點,就讓新的代替節(jié)點成為根節(jié)點
root = successor;
}else if(isLeftNode){//如果刪除節(jié)點是左孩子就讓其父節(jié)點的做指針指新的代替節(jié)點
parent.setLeftChild(successor);
}else{
parent.setRightChild(successor);
}
//把刪除節(jié)點的左子樹復(fù)給新的代替節(jié)點的左孩子
successor.setLeftChild(current.getLeftChild());
}
}
//找到合適的節(jié)點來代替刪除節(jié)點逢并,這里找的是后繼節(jié)點播揪,即右子樹中的最小節(jié)點
public static Node findSuccessor(Node delNode){
Node parent = delNode;
Node successor = delNode;
//current為刪除節(jié)點右子樹的第一個節(jié)點
Node current = delNode.getRightChild();
//右子樹最小節(jié)點,就是右子樹的最左的節(jié)點筒狠,所以這里循環(huán)獲取最左節(jié)點
while(current != null){
parent = successor;//parent為最左節(jié)點的父節(jié)點
successor = current;//successor為最左節(jié)點
current = current.getLeftChild();
}
//successor == delNode.getRightChild()也就是刪除節(jié)點的右子樹沒有左節(jié)點,那就用其右子樹的根節(jié)點做替換節(jié)點箱沦,所以也就不用if()里面的相關(guān)操作
if(successor != delNode.getRightChild()){//有左節(jié)點的情況下
parent.setLeftChild(successor.getRightChild());//代替節(jié)點的右子樹復(fù)給他父節(jié)點的左子樹辩恼,也就是用他的右子樹代替他本來的位置
successor.setRightChild(delNode.getRightChild());//刪除節(jié)點的右子樹復(fù)給代替節(jié)點的右子樹
}
return successor;
}
平衡二叉樹
平衡二叉樹就是AVL樹,是二叉排序樹的進化谓形。如果二叉排序樹按照最壞的情況插入就會變成一個鏈表灶伊,所以為了防止這種情況,就要使用平衡二叉樹寒跳。
AVL樹的旋轉(zhuǎn)分為四種情況:
LL型:AVL樹的左孩子的左子樹插入結(jié)點導(dǎo)致失衡聘萨,此時需要把樹向右旋轉(zhuǎn)一次
RR型:AVL樹的右孩子的右子樹插入結(jié)點導(dǎo)致失衡,此時需要把樹向左旋轉(zhuǎn)一次
LR型:AVL樹的左孩子的右子樹插入節(jié)點導(dǎo)致失衡童太,此時需要先左子樹向左旋轉(zhuǎn)米辐,再將樹向右旋轉(zhuǎn)
RL型:AVL樹的右孩子的左子樹插入節(jié)點導(dǎo)致失衡,此時需要先右子樹向右旋轉(zhuǎn)书释,再將樹向左旋轉(zhuǎn)
圖示參考:AVL樹調(diào)整平衡
紅黑樹
紅黑樹是一種平衡的二叉樹排序樹翘贮,相比于AVL樹,紅黑樹放寬了對于平衡的要求爆惧。但它與AVL樹類似狸页,都是在進行插入和刪除操作時通過特定操作保持二叉查找樹的平衡,從而獲得較高的查找性能扯再。
紅黑樹性質(zhì):
- 所有節(jié)點都是紅色或黑色
- 根節(jié)點是黑色
- 所有葉子結(jié)點是黑色(葉子節(jié)點指的是NIL節(jié)點芍耘,也就是空節(jié)點)
- 每個紅色節(jié)點的兩個子節(jié)點都是黑色,即不能存在兩個紅色節(jié)點互為父子
- 從任意節(jié)點到其葉子結(jié)點的所有路徑中包含的黑色節(jié)點數(shù)相同
紅黑樹的插入操作熄阻,我們把插入的節(jié)點置為紅色斋竞,然后在根據(jù)情況進行調(diào)整。
- 插入節(jié)點為根節(jié)點饺律,把紅色變成黑色即可窃页。
- 插入節(jié)點的父節(jié)點是黑色跺株,無需做任何改變。
- 插入節(jié)點的父節(jié)點是紅色且其父節(jié)點的兄弟節(jié)點也是紅色脖卖,其祖父節(jié)點必然是黑色乒省。此時,將父節(jié)點及其兄弟節(jié)點置為黑色畦木,然后把祖父節(jié)點置為紅色袖扛。但此時可能會發(fā)生錯誤,因為祖父節(jié)點的父節(jié)點也有可能是紅色十籍,所以我們要把祖父節(jié)點當(dāng)成插入的節(jié)點重新進行這一切蛆封。1)如果祖父節(jié)點就是根節(jié)點,那么把祖父節(jié)點置為黑色勾栗。2)如果祖父節(jié)點的父節(jié)點為黑色惨篱,則可不變。3)如果祖父節(jié)點的父節(jié)點為紅色围俘,則遞歸3這種情況砸讳。
- 插入節(jié)點的父節(jié)點是紅色,其叔父節(jié)點是黑色或空界牡,且插入的節(jié)點為左孩子簿寂。這種情況下,我們先調(diào)換父節(jié)點和祖父節(jié)點顏色宿亡,然后把祖父節(jié)點進行一次右旋轉(zhuǎn)常遂。
- 插入節(jié)點的父節(jié)點是紅色,其叔父節(jié)點是黑色或空挽荠,且插入的節(jié)點為右孩子克胳。這種情況下,我們對父節(jié)點進行一次左旋轉(zhuǎn)圈匆,然后情況就變成了4毯欣。接著按照4的方法進行插入。
紅黑樹的刪除臭脓,如果需要刪除的節(jié)點有兩個兒子酗钞,那么問題可以被轉(zhuǎn)化成刪除一個只有一個兒子的節(jié)點的問題。因為對于二叉查找樹来累,在刪除帶有兩個非葉子兒子的節(jié)點的時候砚作,我們找到要么在它的左子樹中的最大元素、要么在它的右子樹中的最小元素嘹锁,并把它的值轉(zhuǎn)移到要刪除的節(jié)點中葫录。我們接著刪除我們從中復(fù)制出值的那個節(jié)點,它必定有少于兩個非葉子的兒子领猾。因為只是復(fù)制了一個值米同,不違反任何性質(zhì)骇扇,這就把問題簡化為如何刪除最多有一個兒子的節(jié)點的問題。
- 刪除的節(jié)點是紅色且沒有非空左右孩子面粮,直接刪除即可少孝。
- 刪除的節(jié)點是紅色單支節(jié)點且有孩子,這種情況是不可能出現(xiàn)的熬苍,如果孩子是紅色違反了性質(zhì)4稍走,如果是黑色違反了性質(zhì)5。
- 刪除的節(jié)點是黑色單支節(jié)點柴底。其孩子節(jié)點代替其位置婿脸,但這樣影響了平衡性,需要進行調(diào)整柄驻。
- 若孩子節(jié)點為紅色狐树,直接將孩子節(jié)點置為黑色,即可達到平衡鸿脓。
- 若孩子節(jié)點為黑色褪迟,則也分為一下幾種情況:具體參考
B樹
Todo
B+樹
Todo
B*樹
Todo
Trie樹
Todo