數據結構與算法--樹的三種存儲結構
之前學的鏈表、隊列、棧,都是線性表,因為其中每個數據元素只有一個前驅和一個后繼。是一對一的關系。
假如是一對多的關系呢?這種數據結構就是今天要學的樹。
樹的定義
樹是由有限個結點(假設為n)構成的集合。n = 0說明這是棵空樹。一棵樹中,有且只有一個根結點,按照習慣在位于樹的頂端。根結點可以理解為始祖一般的存在,他們有若干個孩子,但是他本身沒有雙親。如圖1中結點A就是根結點。
假設把樹從中截斷(并沒有),可以得到若干個互不相交(沒有交集)的集合,每一個集合本身又是一棵樹,稱為根的子樹,以后就直接叫“子樹”。比如假想我斷開了A-B, A-C的連接,結點B和C沒有雙親成為了根結點,產生了兩棵互不相交的子樹。如下。
什么叫互不相交呢?下面粗線連接部分如左圖D和E,他們到底屬于哪棵樹?可以認為構成子樹的集合之間有交集,造成了原來的子樹T1和子樹T2相交。這樣相交的樹,不叫子樹,因為這不符合樹的定義。
樹的結點與深度
上面的圖中每一個圓圈代表的就是一個結點,結點之間的連線表示了結點之間的關系。結點擁有的子樹數目稱為該結點的度,也可以簡單理解為該結點擁有的孩子個數。如上面的圖中A結點的度為2。度為0的結點稱為葉子結點——也就是沒孩子。度不為0的結點稱為非葉子結點或非終端結點。樹的度為樹中各個結點度的最大值,如圖1中結點D擁有的孩子有3個,最多,所以樹的度就是3。
樹中各個結點之間有什么關系呢?某結點它的子樹的根稱做該節點的孩子(Child),該結點稱為這些孩子的雙親(Parent),或者直接叫父結點。同一個父結點的孩子之間互稱為兄弟(Sibling)。舉例來說,圖1中結點C的孩子結點有E和F,E和F的父結點為C,而E和F之間是兄弟關系。
樹的深度就是指樹的層數,根結點處為第一層,其孩子結點為第二層,以此類推。易知圖1樹的深度為4。
樹的存儲結構
父結點(雙親)表示法
這種結構的思想比較簡單:除了根結點沒有父結點外,其余每個結點都有一個唯一的父結點。將所有結點存到一個數組中。每個結點都有一個數據域data和一個數值parent指示其雙親在數組中存放的位置。根結點由于沒有父結點,parent用-1表示。
package Chap6;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class TreeParent<Item> {
public static class Node<T> {
private T data;
private int parent;
public Node(T data, int parent) {
this.data = data;
this.parent = parent;
}
public T getData() {
return data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", parent=" + parent +
'}';
}
}
// 樹的容量,能容納的最大結點數
private int treeCapacity;
// 樹的結點數目
private int nodesNum;
// 存放樹的所有結點
private Node<Item>[] nodes;
// 以指定樹大小初始化樹
public TreeParent(int treeCapacity) {
this.treeCapacity = treeCapacity;
nodes = new Node[treeCapacity];
}
// 以默認的樹大小初始化樹
public TreeParent() {
treeCapacity = 128;
nodes = new Node[treeCapacity];
}
public void setRoot(Item data) {
// 根結點
nodes[0] = new Node<>(data, -1);
nodesNum++;
}
public void addChild(Item data, Node<Item> parent) {
if (nodesNum < treeCapacity) {
// 新的結點放入數組中第一個空閑位置
nodes[nodesNum] = new Node<>(data, index(parent));
nodesNum++;
} else {
throw new RuntimeException("樹已滿,無法再添加結點!");
}
}
// 用nodeNum是因為其中無null,用treeCapacity里面很多null值根本無需比較
private int index(Node<Item> parent) {
for (int i = 0; i < nodesNum; i++) {
if (nodes[i].equals(parent)) {
return i;
}
}
throw new RuntimeException("無此結點");
}
public void createTree(List<Item> datas, List<Integer> parents) {
if (datas.size() > treeCapacity) {
throw new RuntimeException("數據過多,超出樹的容量!");
}
setRoot(datas.get(0));
for (int i = 1; i < datas.size(); i++) {
addChild(datas.get(i), nodes[parents.get(i - 1)]);
}
}
// 是否為空樹
public boolean isEmpty() {
return nodesNum == 0;
// or return nodes[0] == null
}
public Node<Item> parentTo(Node<Item> node) {
return nodes[node.parent];
}
// 結點的孩子結點
public List<Node<Item>> childrenFromNode(Node<Item> parent) {
List<Node<Item>> children = new ArrayList<>();
for (int i = 0; i < nodesNum; i++) {
if (nodes[i].parent == index(parent)) {
children.add(nodes[i]);
}
}
return children;
}
// 樹的度
public int degreeForTree() {
int max = 0;
for (int i = 0; i < nodesNum; i++) {
if (childrenFromNode(nodes[i]).size() > max) {
max = childrenFromNode(nodes[i]).size();
}
}
return max;
}
public int degreeForNode(Node<Item> node) {
return childrenFromNode(node).size();
}
// 樹的深度
public int depth() {
int max = 0;
for (int i = 0; i < nodesNum; i++) {
int currentDepth = 1;
int parent = nodes[i].parent;
while (parent != -1) {
// 向上繼續查找父結點,知道根結點
parent = nodes[parent].parent;
currentDepth++;
}
if (currentDepth > max) {
max = currentDepth;
}
}
return max;
}
// 樹的結點數
public int nodesNum() {
return nodesNum;
}
// 返回根結點
public Node<Item> root() {
return nodes[0];
}
// 讓樹為空
public void clear() {
for (int i = 0; i < nodesNum; i++) {
nodes[i] = null;
nodesNum = 0;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Tree{\n");
for (int i = 0; i < nodesNum - 1; i++) {
sb.append(nodes[i]).append(", \n");
}
sb.append(nodes[nodesNum - 1]).append("}");
return sb.toString();
}
public static void main(String[] args) {
// 按照以下定義,生成樹
List<String> datas = new ArrayList<>(Arrays.asList("Bob", "Tom", "Jerry", "Rose", "Jack"));
List<Integer> parents = new ArrayList<>(Arrays.asList(0, 0, 1, 2));
TreeParent<String> tree = new TreeParent<>();
tree.createTree(datas, parents);
TreeParent.Node<String> root = tree.root();
// root的第一個孩子
TreeParent.Node<String> aChild = tree.childrenFromNode(root).get(0);
System.out.println(aChild.getData() + "的父結點是" + tree.parentTo(aChild).getData());
System.out.println("根結點的孩子" + tree.childrenFromNode(root));
System.out.println("該樹深度為" + tree.depth());
System.out.println("該樹的度為" + tree.degreeForTree());
System.out.println("該樹的結點數為" + tree.nodesNum());
System.out.println(tree);
}
}
/* Outputs
Tom的父結點是Bob
根結點的孩子[Node{data=Tom, parent=0}, Node{data=Jerry, parent=0}]
該樹深度為3
該樹的度為2
該樹的結點數為5
Tree
{Node{data=Bob, parent=-1},
Node{data=Tom, parent=0},
Node{data=Jerry, parent=0},
Node{data=Rose, parent=1},
Node{data=Jack, parent=2}}
*/
setRoot
方法必須首先被調用,可以看到根結點始終被放置在數組中第一個位置(下標為0),之后才能調用addChild
方法。createTree
將創建樹的過程簡化了,我們只需輸入一組數據datas,和這組數據對應的parents傳給createTree
就行,注意datas的第一個數據是根結點信息,在代碼中默認使用-1表示其parent,所以它在parents中沒有對應的parent值,也就是說datas的第二個值才和parents的第一個值對應,以此類推。樹創建完成后,若想再添加結點到樹,調用addChild
就行。
childrenFromNode
方法獲取某個結點的所有孩子結點,由代碼看出它需要遍歷所有結點,復雜度為O(n)。parentTo
方法獲取某結點的父結點,復雜度O(1)。
另外求樹的度的時候,也是遍歷了所有結點,從中選出最大的度作為樹的度,復雜度為O(n)。求樹的深度也類似,遍歷了所有結點,從下往上,一直追溯到根結點,用currentDepth
記錄了當前結點的深度,從所有結點中選擇最大深度值作為樹的深度。
孩子表示法
換種思路,既然雙親表示法獲取某結點的所有孩子有點麻煩,我們索性讓每個結點記住他所有的孩子。但是由于一個結點擁有的孩子個數是一個不確定的值,雖然最多只有樹的度那么多,但是大多數結點的孩子個數并沒有那么多,如果用數組來存放所有孩子,對于大多數結點來說太浪費空間了。自然我們容易想到用一個可變容量的表來存,選用Java內置的LinkedList是個不錯的選擇。先用一個數組存放所有的結點信息,該鏈表只需存儲結點在數組中的下標就行了。
package Chap6;
import java.util.*;
public class TreeChildren<Item> {
public static class Node<T> {
private T data;
private List<Integer> children;
public Node(T data) {
this.data = data;
this.children = new LinkedList<>();
}
public Node(T data, int[] children) {
this.data = data;
this.children = new LinkedList<>();
for (int child : children) {
this.children.add(child);
}
}
public T getData() {
return data;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", children=" + children +
'}';
}
}
// 樹的容量,能容納的最大結點數
private int treeCapacity;
// 樹的結點數目
private int nodesNum;
// 存放樹的所有結點
private Node<Item>[] nodes;
public TreeChildren(int treeCapacity) {
this.treeCapacity = treeCapacity;
nodes = new Node[treeCapacity];
}
public TreeChildren() {
treeCapacity = 128;
nodes = new Node[treeCapacity];
}
public void setRoot(Item data) {
nodes[0].data = data;
nodesNum++;
}
public void addChild(Item data, Node<Item> parent) {
if (nodesNum < treeCapacity) {
// 新的結點放入數組中第一個空閑位置
nodes[nodesNum] = new Node<>(data);
// 父結點添加其孩子
parent.children.add(nodesNum);
nodesNum++;
} else {
throw new RuntimeException("樹已滿,無法再添加結點!");
}
}
public void createTree(Item[] datas, int[][] children) {
if (datas.length > treeCapacity) {
throw new RuntimeException("數據過多,超出樹的容量!");
}
for (int i = 0; i < datas.length; i++) {
nodes[i] = new Node<>(datas[i], children[i]);
}
nodesNum = datas.length;
}
// 根據給定的結點查找再數組中的位置
private int index(Node<Item> node) {
for (int i = 0; i < nodesNum; i++) {
if (nodes[i].equals(node)) {
return i;
}
}
throw new RuntimeException("無此結點");
}
public List<Node<Item>> childrenFromNode(Node<Item> node) {
List<Node<Item>> children = new ArrayList<>();
for (Integer i : node.children) {
children.add(nodes[i]);
}
return children;
}
public Node<Item> parentTo(Node<Item> node) {
for (int i = 0; i < nodesNum; i++) {
if (nodes[i].children.contains(index(node))) {
return nodes[i];
}
}
return null;
}
// 是否為空樹
public boolean isEmpty() {
return nodesNum == 0;
// or return nodes[0] == null
}
// 樹的深度
public int depth() {
return nodeDepth(root());
}
// 求以node為根結點的子樹的深度
public int nodeDepth(Node<Item> node) {
if (node == null) {
return 0;
}
// max是某個結點所有孩子中的最大深度
int max = 0;
// 即使沒有孩子,返回1也是正確的
if (node.children.size() > 0) {
for (int i : node.children) {
int depth = nodeDepth(nodes[i]);
if (depth > max) {
max = depth;
}
}
}
// 這里需要+1因為depth -> max是當前結點的孩子的深度, +1才是當前結點的深度
return max + 1;
}
public int degree() {
int max = 0;
for (int i = 0; i < nodesNum; i++) {
if (nodes[i].children.size() > max) {
max = nodes[i].children.size();
}
}
return max;
}
public int degreeForNode(Node<Item> node) {
return childrenFromNode(node).size();
}
public Node<Item> root() {
return nodes[0];
}
// 樹的結點數
public int nodesNum() {
return nodesNum;
}
// 讓樹為空
public void clear() {
for (int i = 0; i < nodesNum; i++) {
nodes[i] = null;
nodesNum = 0;
}
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Tree{\n");
for (int i = 0; i < nodesNum - 1; i++) {
sb.append(nodes[i]).append(", \n");
}
sb.append(nodes[nodesNum - 1]).append("}");
return sb.toString();
}
public static void main(String[] args) {
String[] datas = {"Bob", "Tom", "Jerry", "Rose", "Jack"};
int[][] children = {{1, 2}, {3}, {4}, {}, {}};
TreeChildren<String> tree = new TreeChildren<>();
tree.createTree(datas, children);
TreeChildren.Node<String> root = tree.root();
TreeChildren.Node<String> rightChild = tree.childrenFromNode(root).get(1);
System.out.println(rightChild.getData() + "的度為" + tree.degreeForNode(rightChild));
System.out.println("該樹的結點數為" + tree.nodesNum());
System.out.println("該樹根結點" + tree.root());
System.out.println("該樹的深度為" + tree.depth());
System.out.println("該樹的度為" + tree.degree());
System.out.println(tree.parentTo(rightChild));
tree.addChild("Joe", root);
System.out.println("該樹的度為" + tree.degree());
System.out.println(tree);
}
}
/* Outputs
Jerry的度為1
該樹的結點數為5
該樹根結點Node{data=Bob, children=[1, 2]}
該樹的深度為3
該樹的度為2
Node{data=Bob, children=[1, 2]}
該樹的度為3
Tree{
Node{data=Bob, children=[1, 2, 5]},
Node{data=Tom, children=[3]},
Node{data=Jerry, children=[4]},
Node{data=Rose, children=[]},
Node{data=Jack, children=[]},
Node{data=Joe, children=[]}}
*/
有些方法的實現和雙親表示法一樣,有些方法的實現改變了。
createTree
方法中可以接受一個二維int數組,存放著每個結點的children,傳參的時候注意一點,如果某個結點沒有孩子,那么也應該填入空值。就像下面這樣,否則會引發空指針。
int[][] children = {{1, 2}, {3}, {4}, {}, {}};
addChild
參數列表沒變,實現變為新添加的結點在數組中的下標(其實就是數組的第一個空閑位置)add進父結點的孩子鏈表中。createTree
可以按照定義一次性生成樹,只需傳入結點信息的表和對應的孩子鏈表就行,復雜度也是O(n)。childrenFromNode
獲得某個結點的所有孩子,這就比雙親表示法好點了,它沒有遍歷所有結點也無需進行if判斷,而僅僅將該結點的孩子鏈表中的內容(整型值)轉換成Node對象返回而已。不過該實現要獲取某個結點的父結點就沒有雙親法好了,孩子表示法必須遍歷所有結點,復雜度為O(n)。獲取父結點方法中,遍歷所有結點,如果有某個結點的孩子鏈表中包含了所求結點,則返回該結點。
求樹的深度方法也變成了遞歸實現,在雙親法實現中由于存在parent域,所以從下至上查找比較方便;而在孩子表示法中,獲取孩子結點比較方便,所以從根結點開始從上至下查找,這里使用到了遞歸的思想。由于返回的是max + 1
(為什么是這個值后面會解釋),所以需要對樹空的情況進行正確的處理。若是葉子結點,循環不會執行,應該返回max + 1 = 1
,正確;其他情況,該結點有孩子,進入循環開始遞歸,遞歸直到遇到葉子結點停止,開始返回,葉子結點返回1。回到父結點的nodeDepth
函數,max被賦值為1?,F在說說這個max到底是什么意思,代碼中for (int i: node.children)
,遍歷當前結點的所有孩子,它們共享同一個max,所以max的意義就是某結點所有孩子結點的深度的最大值。于是max + 1
就是當前結點的深度。接著說,函數一直返回,每次返回實際就是往上一層,到所求結點的孩子結點處,其孩子結點中的最大深度賦值給max,那么最后返回的max + 1
就是所求結點作為根結點時的子樹深度。
孩子表示法的優化
再說獲取某結點父結點的方法,從代碼看出它遍歷了所有結點。如果要改進,可以將雙親表示法融合進去,增加一個parent域就行。也就是說,Node類改成如下就行,這種實現可以稱為雙親孩子表示法。
public static class Node<T> {
private int parent;
private T data;
private List<Integer> children;
}
這樣獲取父結點的復雜度就變成了O(1),就懶得實現了,稍微改改代碼就好了。
孩子兄弟表示法
還有一種表示法,關注某結點的孩子結點之間的關系,他們互為兄弟。一個結點可能有孩子,也有可能有兄弟,也可能兩者都有,或者兩者都沒?;谶@種思想,可以用具有兩個指針域(一個指向當前結點的孩子,一個指向其兄弟)的鏈表實現,這種鏈表又稱為二叉鏈表。特別注意的是,雙親表示法和孩子結點表示法,都使用了數組存放每一個結點的信息,若稍加分析,使用數組是有必要的。但在這種結構中,我們摒棄了數組,根結點可以作為頭指針,以此開始可以遍歷到樹的全部結點——根結點肯定是沒有兄弟的(根結點如果有兄弟這棵樹就有兩個根結點了),如果它沒有孩子,則這棵樹只有根結點;若有孩子,就如下圖,它的nextChild
的指針域就不為空,現在看這個左孩子,有兄弟(實際就是根結點的第二個孩子)還有孩子,則左孩子的兩個指針域都不為空,再看這個左孩子的nextSib
,他有個孩子...一直這樣下去,對吧,能夠訪問到樹的全部結點的。
整個結構就是一條有兩個走向的錯綜復雜的鏈表,垂直走向是深入到結點的子子孫孫;水平走向就是查找它的兄弟姐妹。這種結構也能直觀反映樹的結構的,上圖其實就是下面這棵樹。
說了這么多,反正把它當鏈表就行了,就是多了一個指針域而已。(和雙向鏈表區別開,雙向鏈表是a.next =b
,必然有b.prev = a
;但是這里二叉鏈表卻沒有這個限制,它指向任意一個結點都可以)。
好了現在來實現吧!
package Chap6;
import java.util.ArrayList;
import java.util.List;
public class TreeChildSib<Item> {
public static class Node<T> {
private T data;
private Node<T> nextChild;
private Node<T> nextSib;
public T getData() {
return data;
}
public Node(T data) {
this.data = data;
}
public Node<T> getNextChild() {
return nextChild;
}
public Node<T> getNextSib() {
return nextSib;
}
@Override
public String toString() {
String child = nextChild == null ? null : nextChild.getData().toString();
String sib = nextSib == null ? null : nextSib.getData().toString();
return "Node{" +
"data=" + data +
", nextChild=" + child +
", nextSib=" + sib +
'}';
}
}
private Node<Item> root;
// 存放所有結點,每次新增一個結點就add進來
private List<Node<Item>> nodes = new ArrayList<>();
// 以指定的根結點初始化樹
public TreeChildSib(Item data) {
setRoot(data);
}
// 空參數構造器
public TreeChildSib() {
}
public void setRoot(Item data) {
root = new Node<>(data);
nodes.add(root);
}
public void addChild(Item data, Node<Item> parent) {
Node<Item> node = new Node<>(data);
// 如果該parent是葉子結點,沒有孩子
if (parent.nextChild == null) {
parent.nextChild = node;
// parent有孩子了,只能放在n其第一個孩子的最后一個兄弟之后
} else {
// 從parent的第一個孩子開始,追溯到最后一個兄弟
Node<Item> current = parent.nextChild;
while (current.nextSib != null) {
current = current.nextSib;
}
current.nextSib = node;
}
nodes.add(node);
}
public List<Node<Item>> childrenFromNode(Node<Item> node) {
List<Node<Item>> children = new ArrayList<>();
for (Node<Item> cur = node.nextChild; cur != null; cur = cur.nextSib) {
{
children.add(cur);
}
}
return children;
}
public Node<Item> parentTo(Node<Item> node) {
for (Node<Item> eachNode : nodes) {
if (childrenFromNode(eachNode).contains(node)) {
return eachNode;
}
}
return null;
}
public boolean isEmpty() {
return nodes.size() == 0;
}
public Node<Item> root() {
return root;
}
public int nodesNum() {
return nodes.size();
}
public int depth() {
return nodeDepth(root);
}
public int nodeDepth(Node<Item> node) {
if (node == null) {
return 0;
}
int max = 0;
if (childrenFromNode(node).size() > 0) {
for (Node<Item> child : childrenFromNode(node)) {
int depth = nodeDepth(child);
if (depth > max) {
max = depth;
}
}
}
return max + 1;
}
public int degree() {
int max = 0;
for (Node<Item> node : nodes) {
if (childrenFromNode(node).size() > max) {
max = childrenFromNode(node).size();
}
}
return max;
}
public int degreeForNode(Node<Item> node) {
return childrenFromNode(node).size();
}
public void deleteNode(Node<Item> node) {
if (node == null) {
return;
}
deleteNode(node.nextChild);
deleteNode(node.nextSib);
node.nextChild = null;
node.nextSib = null;
node.data = null;
nodes.remove(node);
}
public void clear() {
deleteNode(root);
root = null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("Tree{\n");
for (int i = 0; i < nodesNum() - 1; i++) {
sb.append(nodes.get(i)).append(", \n");
}
sb.append(nodes.get(nodesNum() - 1)).append("}");
return sb.toString();
}
public static void main(String[] args) {
TreeChildSib<String> tree = new TreeChildSib<>("A");
TreeChildSib.Node<String> root = tree.root();
tree.addChild("B", root);
tree.addChild("C", root);
tree.addChild("D", root);
TreeChildSib.Node<String> child1 = tree.childrenFromNode(root).get(0);
TreeChildSib.Node<String> child2 = tree.childrenFromNode(root).get(1);
TreeChildSib.Node<String> child3 = tree.childrenFromNode(root).get(2);
tree.addChild("E", child1);
tree.addChild("F", child2);
tree.addChild("G", child1);
tree.addChild("H", child3);
System.out.println(tree);
System.out.println("該樹結點數為" + tree.nodesNum());
System.out.println("該樹深度為" + tree.depth());
System.out.println("該樹的度為" + tree.degree());
System.out.println(child1.getData() + "的度為" + tree.degreeForNode(child1));
System.out.println(child2.getData() + "的父結點為" + tree.parentTo(child2).getData());
tree.clear();
System.out.println(child1);
System.out.println(tree.isEmpty());
}
}
/* Outputs
Tree{
Node{data=A, nextChild=B, nextSib=null},
Node{data=B, nextChild=E, nextSib=C},
Node{data=C, nextChild=F, nextSib=D},
Node{data=D, nextChild=H, nextSib=null},
Node{data=E, nextChild=null, nextSib=G},
Node{data=F, nextChild=null, nextSib=null},
Node{data=G, nextChild=null, nextSib=null},
Node{data=H, nextChild=null, nextSib=null}}
該樹結點數為8
該樹深度為3
該樹的度為3
B的度為2
C的父結點為A
Node{data=null, nextChild=null, nextSib=null}
true
*/
由于有的方法需要遍歷樹的所有結點,所以自建一個表List<Node<Item>> nodes
來存放,具體來說就是每次添加結點的同時將這個結點加入到該表中。
addChild
方法很重要,如果需要依附的父結點還沒有孩子(if分支),那需要添加的結點成為它的第一個孩子;如果父結點有孩子了(else分支),那么就從父結點的第一個孩子開始,一直到它最后一個兄弟之后,新添加的結點位于此處。
獲取某結點的所有孩子childrenFromNode
方法,就是從所求結點的第一個孩子開始,不斷找到其兄弟,第一個孩子與其所有兄弟全部就是所求結點的所有孩子。
求度,求深度的算法和孩子表示法差不多,就不再贅述。來看clear
清空樹的方法,需要把每個結點得信息都置為空,就必須有個方法能遍歷這棵樹,這里用了后序遍歷的方法,這之后還沒完,存放結點的表也應該清空才是。
若是想讓獲取父結點變得方便些,也可以多設置一個parent域,見孩子表示法的優化。
孩子兄弟表示法有一個優點,可以將一棵普通樹轉化成二叉樹,由于二叉樹有諸多特征,使得處理起來變得簡單。孩子兄弟表示法上面的那個鏈表,稍微拉伸下改變下結構,就能變成一棵二叉樹,如下。
by @sunhaiyu
2017.9.8