因爲一些原因,前段時間被迫好好研究了一下B+樹,做了PTA的Self-printable B+ Tree(好叭其實是上學期的數據結構沒好好學
今天突發奇想,爲什麼不用Java再來實現一下呢,還能 水一篇博客 提高Java的代碼水平。B+樹的原理就不詳細解釋了,網上的博客很多,但是實現的代碼很冗雜,質量不是很高。(我個人認爲我的代碼還是比較高效易懂的)
所以下面我們直接上題目和代碼:https://pintia.cn/problem-sets/16/problems/691
7-29 Self-printable B+ Tree
題意大概就是根據輸入的數據,輸出一個3階B+樹的結構。
In this project, you are supposed to implement a B+ tree of order 3, with the following operations: initialize, insert (with splitting) and search. The B+ tree should be able to print out itself.
Input Specification:
Each input file contains one test case. For each case, the first line contains a positive number N (≤10^4 ), the number of integer keys to be inserted. Then a line of the N positive integer keys follows. All the numbers in a line are separated by spaces.
Output Specification:
For each test case, insert the keys into an initially empty B+ tree of order 3 according to the given order. Print in a line Key X is duplicated where X already exists when being inserted. After all the insertions, print out the B+ tree in a top-down lever-order format as shown by the samples.
Sample Input 1:
6
7 8 9 10 7 4
Sample Output 1:
Key 7 is duplicated
[9]
[4,7,8][9,10]
Sample Input 2:
10
3 1 4 5 9 2 6 8 7 0
Sample Output 2:
[6]
[2,4][8]
[0,1][2,3][4,5][6,7][8,9]
Sample Input 3:
3
1 2 3
Sample Output 3:
[1,2,3]
下面是我根據之前AC的C++代碼“翻譯”出來的Java代碼:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Scanner;
public class Main {
static Node tree_root = null; // 樹根,初始爲null
static int[] input = new int[10009]; // 輸入的數組
// 節點類
static class Node{
Node left, mid, right, fa; // 分別對應該節點的左子樹,中子數,右子樹和父節點
List<Integer> num = new ArrayList<>(); // 記錄節點元素的數組
boolean isLeave; // 是否爲葉子節點
public Node() {
left = null; mid = null; right = null; fa = null;
isLeave = true;
}
}
// 查找x的放置位置
static Node find(Node root, int x) {
// 如果該節點是葉子節點
if (root.isLeave == true) {
for (int n: root.num) {
if (n == x) return null; // n == x說明已經存在了相同數值的節點,不能加入,所以返回null
}
return root;
}
int k = root.num.size(), len = k;
for (int i = 0; i < len; ++i) {
if (root.num.get(i) == x) return null;
if (root.num.get(i) < x) continue;
k = i; break;
}
if (k == 0) return find(root.left, x);
else if (k == 1) return find(root.mid, x);
else return find(root.right, x);
}
// 將x插入root中
static void insert(Node root, int x) {
// 插入第一個節點時需要用到,初始化tree_root
if (root == null) {
tree_root = new Node();
tree_root.num.add(x);
return;
}
root.num.add(x);
Collections.sort(root.num);
if (root.num.size() <= 3) return; // 如果加入root後,該節點的元素個數符合要求的話,即可直接返回
// 否則需要更新樹結構,將root的後兩個元素分給一個新的節點new_root
Node new_root = new Node();
new_root.num.add(root.num.get(2)); new_root.num.add(root.num.get(3)); root.num.remove(3);
int up_num = root.num.remove(2); // 需要上調到上一層的數字
// 需要持續進行樹結構的更新,直到符合要求爲止
while (true) {
// 如果到了樹根,需要單獨處理
if (root == tree_root) {
tree_root = new Node(); tree_root.isLeave = false;
tree_root.left = root; tree_root.mid = new_root;
tree_root.num.add(up_num);
root.fa = tree_root; new_root.fa = tree_root;
break;
}
root.fa.num.add(up_num);
Collections.sort(root.fa.num);
// 更新後如果符合條件,則調整完成後跳出
if (root.fa.num.size() == 2) {
if (root.fa.left == root) {
root.fa.right = root.fa.mid;
root.fa.mid = new_root;
new_root.fa = root.fa;
}
else {
root.fa.right = new_root;
new_root.fa = root.fa;
}
break;
}
// 否則需要繼續調整
// 思想其實就是把root分爲兩個節點,從而root.fa就分爲了Tmp1, Tmp2, Tmp3, Tmp4這四個節點
up_num = root.fa.num.get(1);
int left_num = root.fa.num.get(0), right_num = root.fa.num.get(2);
Node Tmp1, Tmp2, Tmp3, Tmp4;
if (root.fa.left == root) {
Tmp1 = root; Tmp2 = new_root; Tmp3 = root.fa.mid; Tmp4 = root.fa.right;
}
else if (root.fa.mid == root){
Tmp1 = root.fa.left; Tmp2 = root; Tmp3 = new_root; Tmp4 = root.fa.right;
}
else{
Tmp1 = root.fa.left; Tmp2 = root.fa.mid; Tmp3 = root; Tmp4 = new_root;
}
root = root.fa; root.left = Tmp1; root.mid = Tmp2; root.right = null;
root.num.clear(); root.num.add(left_num);
Tmp1.fa = root; Tmp2.fa = root;
new_root = new Node();
new_root.left = Tmp3; new_root.mid = Tmp4; new_root.isLeave = false;
new_root.num.add(right_num);
Tmp3.fa = new_root; Tmp4.fa = new_root;
}
return;
}
static class inqueue{
Node tree;
int height;
public inqueue(Node t, int h) {
tree = t;
height = h;
}
}
// 層序遍歷,很簡單的隊列實現
static void levelorder() {
Queue<inqueue> q = new LinkedList<inqueue>();
q.add(new inqueue(tree_root, 0));
int cur_height = 0;
while (!q.isEmpty()) {
inqueue a = q.remove();
if (a.height != cur_height) {
System.out.println();
cur_height = a.height;
}
System.out.print("[");
Iterator<Integer> it = a.tree.num.iterator();
while(true) {
System.out.printf("%d", it.next());
if (it.hasNext()) System.out.print(",");
else {
System.out.print("]");
break;
}
}
if (a.tree.left != null) {
q.add(new inqueue(a.tree.left, a.height+1));
}
if (a.tree.mid != null) {
q.add(new inqueue(a.tree.mid, a.height+1));
}
if (a.tree.right != null) {
q.add(new inqueue(a.tree.right, a.height+1));
}
}
System.out.println();
}
// 主函數
public static void main(String[] args) {
Scanner sn = new Scanner(System.in);
int n = sn.nextInt();
for (int i = 0; i < n; ++i) input[i] = sn.nextInt();
for (int i = 0; i < n; ++i) {
int x = input[i];
if (i == 0) insert(tree_root, x); // 第一次插入數字時,單獨處理
else {
Node root = find(tree_root, x); // 先找到插入的位置
if (root == null) System.out.printf("Key %d is duplicated\n", x); // 如果返回null說明無法插入
else insert(root, x); // 否則正常insert
}
}
levelorder(); //輸出層序遍歷的結果
sn.close();
}
}
大體的思路就是先找數字插入的位置,插入後再調整樹型。唯一需要解釋一下的就是調整樹型的時候用到的Tmp1, Tmp2, Tmp3和Tmp4;我們來看下面這張圖,以root爲其父節點的左子樹爲例解釋一下。
左圖爲初始狀態,如果root中的元素個數超過了3,就要從root中分裂出右圖的節點new_root,並將root和new_root傳遞到上層
所以,我們就可以去PTA提交代碼了,第一次在OJ上提交Java代碼,還有點小激動呢:)
然鵝,超時了???:(
分明我以前用C++提交的代碼輕鬆過百萬來着(不對,什麼百萬,又不是n^2
於是,我第一次嘗試用Java寫數據結構就翻車了,翻在了TLE上。但是這一次嘗試還是很有意義的,不論是編寫代碼的方式還是最後呈現的結果,都能直觀感受到Java和C++之間的差別。在解決具體問題的時候,C++的時間效率還是要高很多的;但是如果在一個大型項目中,時間效率顯得不如開發效率重要時,Java的優勢就展現出來了。所以說這一次翻車還是很有意義的嘗試。
(另外,C++的代碼我也有,但是因爲涉及到代碼查重的問題不能貼在博客裏,需要的可以私信問我要