二叉樹遞迴非遞迴遍歷,層次遍歷,反轉,輸出路徑等常見操作詳細總結
1.序言
在實際工作中,很多業務場景其實也需要一些比較巧妙的演算法來支撐,並不是業務邏輯就全是複製貼上或者說重複的程式碼寫一百遍。越是隨著演算法研究的深入,越是發現數據結構的重要性。或者說,資料結構中就蘊藏著無數精妙演算法的思想,很多演算法的思想在資料結構中體現得非常突出。而作為一種非線性的資料結構,二叉樹是非常重要非常常見也非常牛逼的一種資料結構,裡面包含有遞迴,棧,佇列,dfs等等很多常見的操作。因此,特意寫一篇比較長的文章,記錄一下二叉樹裡面的一些常見操作以及裡面包含的思想。
2.二叉樹節點定義
二叉樹內部是由一個一個的Node組成的。因此我們一般定義一個Node類。這個沒什麼好說的,直接上程式碼。
static class Node<T> {
T data;
Node left = null;
Node right = null;
Node(T data) {
this.data = data;
}
}
3.初始化二叉樹
這個也沒太多好說的,直接上程式碼。
public static Node init() { Node n1 = new Node(1); Node n2 = new Node(2); Node n3 = new Node(3); Node n4 = new Node(4); Node n5 = new Node(5); Node n6 = new Node(6); Node n7 = new Node(7); Node n8 = new Node(8); n1.left = n2; n1.right = n3; n2.left = n4; n2.right = n5; n3.left = n6; n3.right = n7; n4.left = n8; return n1; }
返回的是樹的根節點。
4.前序中序後續遍歷遞迴
這個其實也沒有太多好說的。但還是稍微說兩句。
前序遍歷是按根節點->左子樹->右子樹的順序訪問二叉樹。
中序遍歷是按左子樹->根節點->右子樹的順序訪問二叉樹。
後序遍歷是按左子樹->根節點->右子樹的順序訪問二叉樹。
還是直接上程式碼
public static void preOrder(Node root) { if (root != null) { System.out.print(root.data + " "); preOrder(root.left); preOrder(root.right); } } public static void midOrder(Node root) { if (root != null) { midOrder(root.left); System.out.print(root.data + " "); midOrder(root.right); } } public static void postOrder(Node root) { if (root != null) { postOrder(root.left); postOrder(root.right); System.out.print(root.data + " "); } }
遞迴的方式簡單明瞭,相信也不用過多解釋。
5.前序中序非遞迴
前序中序遍歷非遞迴的方式較為簡單一些。先說說這兩種情況。
public static void preOrder2(Node root) {
Stack<Node> stack = new Stack();
if (root == null) {
return;
}
stack.push(root);
while (stack.size() > 0) {
Node tmp = stack.pop();
System.out.print(tmp.data + " ");
if (tmp.right != null) {
stack.push(tmp.right);
}
if (tmp.left != null) {
stack.push(tmp.left);
}
}
}
public static void midOrder2(Node root) {
Stack<Node> stack = new Stack<>();
while (root != null || ! stack.empty()) {
while(root != null) {
stack.push(root);
root = root.left;
}
if (! stack.empty()) {
Node tmp = stack.pop();
System.out.print(tmp.data + " ");
root = tmp.right;
}
}
}
核心思想是利用棧這種資料結構的特點來模擬遞迴。
在前序遍歷中,因為每一次遇到新的節點就要訪問,所以直接用一個棧模擬即可。每次pop出一個最後一個壓入棧頂端的節點並且訪問,然後將該節點的左右子節點壓入棧。注意因為棧的特點是後進先出,而前序遍歷是先訪問左節點再訪問右節點,所以壓棧的時候應該是先壓右子節點再壓左子節點,這樣保證pop的時候是先pop出的左子節點。
而在中序遍歷中,可以想象是先沿著左子樹一直遍歷直到某個節點的左子樹為空,在這個過程中所有的節點自然都被壓入棧中。當某個節點的左子樹為空時,將該節點pop訪問,並開始遍歷該節點的右子樹。
6.後續遍歷的非遞迴
後續遍歷的非遞迴方式最複雜,先上程式碼。
private void postorder2(Node node) {
if (node == null) return;
Node cur = node;
Node pre = node;
Stack<Node> stack = new Stack<>();
// cur移動到左子樹最下面
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
while (! stack.isEmpty()) {
// pop棧頂
cur = stack.pop();
if (cur.right != null && cur.right != pre) {
// 根節點再入棧
stack.push(cur);
// 處理右子樹
cur = cur.right;
while (cur != null) {
// 到右子樹最下面
stack.push(cur);
cur = cur.left;
}
} else {
System.out.print(cur.data + " ");
pre = cur;
}
}
}
後續遍歷要先將左右子樹都訪問完畢以後再訪問根節點。所以步驟如下:
1.首先肯定是沿著左子樹往下搜尋,並且一直做壓棧操作。
2.當達到左子樹為空以後,此時棧頂元素出棧。如果該元素右子樹不為空,並且該元素的右子樹未被訪問,則先將該元素再壓棧回去(因為該元素此時未被訪問)。
3.將該元素的右子樹也壓棧,並且沿右子樹的的左子樹繼續搜尋。
4.當右子樹為空或者已經被訪問,此時元素可以被訪問,出棧,訪問,並且將當前訪問節點標記。
7.層次遍歷
層次遍歷是從根節點開始,沿著二叉樹的寬度一層一層往下遍歷。由這個特點不難看出,我們可以利用佇列先進先出的特點來模擬層次遍歷。
更具體地說,我們先讓根節點入佇列,並且訪問根節點。然後讓根節點的左節點入隊,再讓右節點入隊。這樣左結點就儲存在隊頭的位置,將首先被訪問。
訪問完根節點以後,左節點出隊,同時訪問左節點。訪問完畢讓左節點的左右節點依次入隊。此時佇列中的頭節點為根節點的右節點。
上述過程迴圈,一直到佇列為空即可。
public static void levelOrder(Node root) {
if (root == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
Node tmp = queue.poll();
System.out.print(tmp.data + " ");
if (tmp.left != null) {
queue.offer(tmp.left);
}
if (tmp.right != null) {
queue.offer(tmp.right);
}
}
}