B+ tree implemented in Java

来源:https://www.cnblogs.com/ylyzty/archive/2023/06/25/17502517.html
-Advertisement-
Play Games

## B+樹相關介紹 > B+樹是一棵**多叉排序樹**,即每個非葉子節點可以包含多個子節點,其整體結構呈扁平化,所以其非常適配於資料庫和操作系統的文件系統中。且B+樹能夠保持數據的穩定有序,插入和刪除都擁有較穩定的**對數時間複雜度**。 **B+樹的特性**:以 m 階為例,m 表示內部節點即非 ...


B+樹相關介紹

B+樹是一棵多叉排序樹,即每個非葉子節點可以包含多個子節點,其整體結構呈扁平化,所以其非常適配於資料庫和操作系統的文件系統中。且B+樹能夠保持數據的穩定有序,插入和刪除都擁有較穩定的對數時間複雜度

B+樹的特性:以 m 階為例,m 表示內部節點即非葉子節點可以包含的最大子節點個數 maximumNum

  • 若一個內部節點有 \(n(n <= m)\) 個子節點,則該內部節點應包含 \(n - 1\) 個關鍵字,也就是索引值
  • 除根節點和葉子節點外,其他節點至少包含 Math.ceil(m / 2) 個子節點,這是因為B+樹的生成順序導致的
    • 最開始,B+樹只有一個根節點和若幹不超過m個的葉子節點;
    • 逐漸添加,導致葉子節點超過m時,此時根節點的子節點個數大於m,不符合要求,需要分裂
    • 分裂則導致增加2個內部節點,其中一個內部節點個數為(m+1)/2,另一個為(m+2)/2
    • 其他內部節點也是如此規律形成,所以所有內部節點的子節點個數均大於Math.ceil(m / 2)
  • 內部節點即對應索引部分,節點中僅包含子樹中最大/最小的索引值
  • 葉子節點即對應數據部分,節點中不僅包含索引值,也包含其他的值信息
  • 最底層所有葉子節點通過雙向鏈表串聯,優化範圍查詢

B+樹實現

目前實現的B+樹的簡易版,葉子節點是存儲的Entry<Integer, String>鍵值對,內部節點存儲的是Integer索引,後續有時間再進行泛型的通用擴展。

節點定義

抽象公共父類 Node

package bplustree;

public abstract class Node {
    InternalNode parent;    // 父節點
	
    public Node() {}
    
    public abstract boolean isValid();          // 判斷刪除節點後各B+樹節點是否滿足要求
    public abstract boolean isAvailable();      // 判斷B+樹節點是否可以分裂節點給其他節點
    public abstract boolean isMergeable();      // 判斷B+樹節點是否可以和其他節點合併
}

內部節點定義

public class InternalNode extends Node{
    int maxChildNodes;                   // 子節點個數最大值 m,m為階數
    int minChildNodes;                   // 除根節點及葉子節點外,子節點個數最小值 ceil(m / 2)
    
    int curNodesNum;                     // 內部節點當前的子節點個數
    InternalNode leftSibling;            // 左兄弟節點
    InternalNode rightSibling;           // 右兄弟節點
    Integer[] keys;                      // 內部節點當前的索引值,最多有 m - 1 個
    Node[] childPointers;                // 內部節點當前的子節點,最多有 m 個
}

葉子節點定義

public class LeafNode extends Node {
    int maximumNum;                      // 葉子節點最多元素個數 m - 1
    int minimumNum;                      
    int curNum;                          // 葉子節點當前的元素個數
    LeafNode leftSibling;                // 左兄弟和右兄弟形成雙向鏈表         
    LeafNode rightSibling;
    Entry[] entries;                     // 葉子節點鍵值對,不僅存儲索引值,也存儲其他值信息
}


class Entry implements Comparable<Entry> {
    Integer key;
    String value;

    public Entry(Integer key, String value) {
        this.key = key;
        this.value = value;
    }

    @Override
    public int compareTo(Entry o) {
        return key.compareTo(o.key);
    }
}

B+樹定義

public class BPlusTree {
    int m;
    InternalNode root;         // B+樹根節點
    LeafNode head;             // 葉子節點的首元素
}

查詢操作

單值查詢

B+樹的查找過程:根據查找的索引值k,從根節點向葉子節點搜索,對數時間複雜度

public String search(int key) {
    if (isEmpty()) {
        return null;
    }

    // 樹查找
    LeafNode leafNode = (this.root == null) ? this.head : findLeafNode(key);
    Entry[] entries = leafNode.entries;
    
    // 葉子節點內進行二分查找
    int index = binarySearch(entries, leafNode.curNum, key, null);

    if (index == -1) {
        return null;
    }
    else {
        return entries[index]
    }
}

// 從根節點開始查找
private LeafNode findLeafNode(Integer key) {
    return findLeafNode(root, key);
}

// 找到索引值所在的葉子節點
private LeafNode findLeafNode(InternalNode internalNode, Integer key) {
    Integer[] keys = internalNode.keys;

    int i;
    for (i = 0; i < internalNode.curNodesNum - 1; i++) {
        if (key.compareTo(keys[i]) < 0) {
            break;
        }
    }

    Object child = internalNode.childPointers[i];
    if (child instanceof LeafNode) {
        return (LeafNode) child;
    }
    else {
        return findLeafNode((InternalNode) child, key);
    }
}
區間查詢

B+樹區間查詢左值可能在的葉子節點位置,然後通過雙向鏈表向後遍歷。

// 閉區間 [left, right]
public ArrayList<String> searchRange(int left, int right) {
    List<String> values = new ArrayList<>();
    LeafNode leafNode = findLeafNode(left);    // 查找左值可能存在的位置,並從該位置向後遍歷

    boolean flag = true;
    while (leafNode != null && flag) {
        Entry[] entries = leafNode.entries;
        for (Entry entry : entries) {
            if (entry == null) {
                break;
            }
            
            if ( entry.key > right) {
                flag = false;
                break;
            }

            if (left <= entry.key && right >= entry.key) {
                values.add(entry.value);
            }
        }

        leafNode = leafNode.rightSibling;
    }

    return values;
}

插入操作

B+樹的插入操作僅在葉子節點進行:

  1. 若為空樹,則創建一個葉子節點,該葉子節點同時也是根節點,插入操作結束;
  2. 根據插入的 key 值,找到應該在的葉子節點插入;
    1. 若插入後葉子節點個數符合要求即小於m,則插入結束
    2. 若插入後葉子節點個數不符合要求即大於等於m,將該節點分裂成兩半,則判斷當前葉子節點是否為根節點
      1. 若當前葉子節點為根節點,則構建一個新的root節點,指向分裂後的兩個子節點
      2. 若當前葉子節點不為根節點,則在父節點處添加一個新的子節點,新子節點則存儲原節點一半的值,並迴圈向上判斷中間節點是否滿足要求
public void insert(int key, String value) {
    if (isEmpty()) {
        this.head = new LeafNode(this.m, new Entry(key, value));
    }
    else {
        LeafNode leafNode = (this.root == null) ? this.head : findLeafNode(key);

        // 插入葉子節點失敗,即葉子節點中存儲已到達上限
        if (!leafNode.insert(new Entry(key, value))) {
            leafNode.entries[leafNode.curNum++] = new Entry(key, value);
            sortEntries(leafNode.entries);

            // 葉子節點分裂的位置
            int mid = getIndexOfMidPointer();
            Entry[] halfEntry = splitEntries(leafNode, mid);

            // 若葉子節點為根節點,即parent為null
            if (leafNode.parent == null) {
                Integer[] parent_keys = new Integer[m];    
                parent_keys[0] = halfEntry[0].key;

                // 創建新的 root
                InternalNode parent = new InternalNode(m, parent_keys);
                leafNode.parent = parent;
                parent.appendChildPointer(leafNode);
            }

            // 若葉子節點不為根節點
            else {
                int newParentKey = halfEntry[0].key;
                leafNode.parent.keys[leafNode.parent.curNodesNum - 1] = newParentKey;
                Arrays.sort(leafNode.parent.keys, 0, leafNode.parent.curNodesNum);
            }

            // 分裂後的另一半葉子節點,添加到父節點
            LeafNode newLeafNode = new LeafNode(this.m, halfEntry, leafNode.parent);

            // 分裂後的另一半葉子節點對應的下標
            int index = leafNode.parent.getIndexOfPointer(leafNode) + 1;
            for (int i = index; i < leafNode.parent.childPointers.length - 1; i++) {
                leafNode.parent.childPointers[i + 1] = leafNode.parent.childPointers[i];
            }
            leafNode.parent.childPointers[index] = newLeafNode;


            // 關聯兄弟節點
            newLeafNode.rightSibling = leafNode.rightSibling;
            if (newLeafNode.rightSibling != null) {
                newLeafNode.rightSibling.leftSibling = newLeafNode;
            }
            leafNode.rightSibling = newLeafNode;
            newLeafNode.leftSibling = leafNode;

            if (this.root == null) {
                this.root = leafNode.parent;
            }
            else {
                // 逐漸上浮,判斷插入是否會導致B+樹內部節點不符合要求
                InternalNode internalNode = leafNode.parent;
                while (internalNode != null) {
                    if (internalNode.isOverfull()) {
                        splitInternalNode(internalNode);
                    }
                    else {
                        break;
                    }

                    internalNode = internalNode.parent;
                }
            }
        }
    }
}


/**
 * 葉子節點插入,導致的上層內部節點分裂
 */
private void splitInternalNode(InternalNode internalNode) {
    InternalNode parent = internalNode.parent;

    int mid = getIndexOfMidPointer();
    Integer newParentKey = internalNode.keys[mid];

    // 內部節點的 node 分裂
    Node[] halfPointers = splitChildPointers(internalNode, mid);
    // 內部節點的 key 分裂
    Integer[] halfKeys = splitKeys(internalNode.keys, mid);

    // 分裂後內部節點的子節點個數
    internalNode.curNodesNum = linearNullSearch(internalNode.childPointers);
    InternalNode sibling = new InternalNode(this.m, halfKeys, halfPointers);
    for (Node pointer : halfPointers) {
        if (pointer != null) {
            pointer.parent = sibling;
        }
    }

    sibling.rightSibling = internalNode.rightSibling;
    internalNode.rightSibling = sibling;
    sibling.leftSibling = internalNode;
    if (sibling.rightSibling != null) {
        sibling.rightSibling.leftSibling = sibling;
    }

    // root node
    if (parent == null) {
        Integer[] keys = new Integer[this.m];
        keys[0] = newParentKey;

        InternalNode newRoot = new InternalNode(this.m, keys);
        newRoot.appendChildPointer(internalNode);
        newRoot.appendChildPointer(sibling);
        this.root = newRoot;

        internalNode.parent = newRoot;
        sibling.parent = newRoot;
    }
    else {
        parent.keys[parent.curNodesNum - 1] = newParentKey;
        Arrays.sort(parent.keys, 0, parent.curNodesNum);

        int index = parent.getIndexOfPointer(internalNode) + 1;
        parent.insertChildPointer(sibling, index);
        sibling.parent = parent;
    }
}


private Node[] splitChildPointers(InternalNode node, int split) {
    Node[] pointers = node.childPointers;
    Node[] newPointers = new Node[this.m + 1];

    for (int i = split + 1; i < pointers.length; i++) {
        newPointers[i - split - 1] = pointers[i];
        node.removePointer(i);
    }

    return newPointers;
}


private Integer[] splitKeys(Integer[] keys, int split) {
    Integer[] newKeys = new Integer[m];
    keys[split] = null;

    for (int i = split + 1; i < keys.length; i++) {
        newKeys[i - split] = keys[i];
        keys[i] = null;
    }

    return newKeys;
}

刪除操作

B+樹的刪除操作僅在葉子節點進行:

  1. 若刪除後,葉子節點中的索引個數仍然滿足要求即大於等於Math.ceil(m / 2)時,將該葉子節點的其他索引左移一位,刪除結束;
  2. 若刪除後,葉子節點中的索引個數不滿足最低要求,則查詢左右兄弟節點:
    1. 若左/右兄弟節點中索引個數大於Math.ceil(m / 2),則從左/右兄弟節點中移動一個索引項到當前葉子節點中,並修改父節點的索引值,刪除結束
    2. 若左/右兄弟節點中索引個數等於Math.ceil(m / 2),則將左/右節點與當前節點合併,修改父節點的索引記錄,並向上逐級判斷內部節點是否因為頁合併導致索引項不滿足最低要求,刪除結束
public void delete(int key) {
    if (isEmpty()) {
        System.out.println("Invalid: The B+ tree is empty!");
    }
    else {
        LeafNode leafNode = this.root == null ? this.head : findLeafNode(key);
        int index = binarySearch(leafNode.entries, leafNode.curNum, key, null);

        if (index < 0) {
            System.out.println("Invalid: The key does not exist in the B+ tree!");
        }
        else {
            leafNode.deleteAtIndex(index);

            // 刪除後,葉子節點仍然滿足要求,刪除結束
            if (leafNode.isValid()) {
                LeafNode sibling;
                InternalNode parent = leafNode.parent;

                // 刪除後,葉子節點不滿足要求,左兄弟節點可以移動一個索引項到當前葉子節點
                if (leafNode.leftSibling != null && leafNode.leftSibling.parent == parent && leafNode.leftSibling.isAvailable()) {
                    sibling = leafNode.leftSibling;
                    Entry entry = sibling.entries[sibling.curNum - 1];
                    leafNode.insert(entry);
                    sortEntries(leafNode.entries);

                    sibling.deleteAtIndex(sibling.curNum - 1);

                    // 更新 parent 的 key
                    int pointIndex = getIndexOfLeafNode(parent.childPointers, leafNode);
                    if (entry.key < parent.keys[pointIndex - 1]) {
                        parent.keys[pointIndex - 1] = entry.key;
                    }
                }
                // 刪除後,葉子節點不滿足要求,右兄弟節點可以移動一個索引項到當前葉子節點
                else if (leafNode.rightSibling != null && leafNode.rightSibling.parent == parent && leafNode.rightSibling.isAvailable()) {
                    sibling = leafNode.rightSibling;
                    Entry entry = sibling.entries[0];
                    leafNode.insert(entry);
                    sortEntries(leafNode.entries);

                    sibling.deleteAtIndex(0);

                    // 更新 parent 的 key
                    int pointIndex = getIndexOfLeafNode(parent.childPointers, leafNode);
                    if (entry.key > parent.keys[pointIndex]) {
                        parent.keys[pointIndex] = entry.key;
                    }
                }
                // 刪除後,葉子節點不滿足要求,左兄弟節點可以與當前葉子節點合併
                else if (leafNode.leftSibling != null && leafNode.leftSibling.parent == parent && leafNode.leftSibling.isMergeable()) {
                    sibling = leafNode.leftSibling;
                    int pointIndex = getIndexOfLeafNode(parent.childPointers, leafNode);

                    parent.removeKey(pointIndex - 1);
                    parent.removePointer(leafNode);

                    sibling.rightSibling = leafNode.rightSibling;
                    if (parent.isValid()) {
                        handleDeficiency(parent);
                    }
                }
                // 刪除後,葉子節點不滿足要求,右兄弟節點可以與當前葉子節點合併
                else if (leafNode.rightSibling != null && leafNode.rightSibling.parent == parent && leafNode.rightSibling.isMergeable()) {
                    sibling = leafNode.rightSibling;
                    int pointIndex = getIndexOfLeafNode(parent.childPointers, leafNode);

                    parent.removeKey(pointIndex);
                    parent.removePointer(leafNode);

                    sibling.leftSibling = leafNode.leftSibling;
                    if (sibling.leftSibling == null) {
                        this.head = sibling;
                    }

                    // 逐級向上層判斷是否滿足要求
                    if (parent.isValid()) {
                        handleDeficiency(parent);
                    }
                }
                // 刪除後,B+樹為空
                else if (this.root == null && this.head.curNum == 0) {
                    this.head = null;
                }
            }
        }
    }
}



/**
 * 處理不滿足要求的內部節點
 * @param internalNode
 */
private void handleInvalidInternalNode(InternalNode internalNode) {
    InternalNode sibling;
    InternalNode parent = internalNode.parent;

    // 當前內部節點為根節點
    if (root == internalNode) {
        for (int i = 0; i < internalNode.childPointers.length; i++) {
            if (internalNode.childPointers[i] != null) {
                if (internalNode.childPointers[i] instanceof InternalNode) {
                    root = (InternalNode) internalNode.childPointers[i];
                    root.parent = null;
                }
                else if (internalNode.childPointers[i] instanceof LeafNode) {
                    root = null;
                }
            }
        }
    }

    // 左兄弟節點可以移動索引項
    else if (internalNode.leftSibling != null && internalNode.leftSibling.isAvailable()) {
        sibling = internalNode.leftSibling;
        Integer key = sibling.keys[internalNode.curNodesNum - 2];
        Node pointer = sibling.childPointers[internalNode.curNodesNum - 1];

        shiftKeys(internalNode.keys, 1);
        shiftPointers(internalNode.childPointers, 1);
        internalNode.keys[0] = key;
        internalNode.childPointers[0] = pointer;

        sibling.removePointer(pointer);
    }
    // 右兄弟節點可以移動索引項
    else if (internalNode.rightSibling != null && internalNode.rightSibling.isAvailable()) {
        sibling = internalNode.rightSibling;

        Integer key = sibling.keys[0];
        Node pointer = sibling.childPointers[0];

        internalNode.keys[internalNode.curNodesNum - 1] = parent.keys[0];
        internalNode.childPointers[internalNode.curNodesNum] = pointer;

        parent.keys[0] = key;

        sibling.removePointer(0);
        shiftPointers(sibling.childPointers, -1);
    }
    // 左兄弟節點可以合併
    else if (internalNode.leftSibling != null && internalNode.leftSibling.isMergeable()) {
        sibling = internalNode.leftSibling;
        int index = -1;
        for (int i = 0; i < parent.childPointers.length; i++) {
            if (parent.childPointers[i] == internalNode) {
                index = i;
                break;
            }
        }
        parent.keys[index - 1] = parent.keys[index];
        for (int i = index; i < parent.keys.length - 1; i++) {
            parent.keys[i] = parent.keys[i + 1];
        }

        shiftPointers(internalNode.childPointers, (int) Math.ceil(m / 2.0));
        for (int i = 0; i < (int) Math.ceil(m / 2.0); i++) {
            internalNode.childPointers[i] = sibling.childPointers[i];
        }

        internalNode.leftSibling = sibling.leftSibling;
        if (internalNode.leftSibling != null) {
            internalNode.leftSibling.rightSibling = internalNode;
        }

        if (parent != null && parent.isValid()) {
            handleInvalidInternalNode(parent);
        }
    }
    // 右兄弟節點可以合併
    else if (internalNode.rightSibling != null && internalNode.rightSibling.isMergeable()) {
        sibling = internalNode.rightSibling;
        int index = -1;
        for (int i = 0; i < parent.childPointers.length; i++) {
            if (internalNode == parent.childPointers[i]) {
                index = i;
                break;
            }
        }

        parent.keys[index] = parent.keys[index + 1];
        for (int i = index + 2; i < parent.keys.length; i++) {
            parent.keys[i - 1] = parent.keys[i];
        }
        for (int i = 0; i < (int) Math.ceil(m / 2.0); i++) {
            internalNode.childPointers[internalNode.curNodesNum++] = sibling.childPointers[i];
        }

        internalNode.rightSibling = sibling.rightSibling;
        if (internalNode.rightSibling != null) {
            internalNode.rightSibling.leftSibling = internalNode;
        }

        if (parent != null && parent.isValid()) {
            handleInvalidInternalNode(parent);
        }
    }
}

參考文章:

1. B+樹


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 狀態機,包括了狀態和動作,某個**狀態**下,只能執行某些**動作**,如果**動作**不匹配,狀態是不會進行變更了,這樣就保護了我們狀態欄位的準備性,不能隨意改變,必須按著我們**設計的規則**進行狀態的輪轉。 # Stateless實現的狀態機 1. **Stateless**:Stateles ...
  • 來源:blog.csdn.net/qq_35387940/article/details/129167329 ## **前言** 平時做一些統計數據,經常從資料庫或者是從介面獲取出來的數據,單位是跟業務需求不一致的。 - 比如, 我們拿出來的 分, 實際上要是元 - 又比如,我們拿到的數據需要 乘以 ...
  • # 1.文件路徑 我們發現不管是寫入還是寫出操作,我們提供的都是文件名,其實這裡準確說應該是文件路徑。當我們簡單把文件名傳遞給open函數時,Python將在當前執行程式的文件所在的目錄中查找文件名所代表的文件。 根據組織文件的方式,可能需要打開不在當前執行程式文件所屬目錄中的文件。如果此時我們把該 ...
  • ## 初級指針 本篇主要介紹:`指針和變數的關係`、指針類型、`指針的運算符`、空指針和野指針、`指針和數組`、`指針和字元串`、const 和指針、以及`gdb 調試段錯誤`。 ### 基礎概念 > 指針是一種特殊的變數。存放地址的變數就是指針。 `int num = 1;` 會申請4個位元組的記憶體 ...
  • 原文在[這裡](https://go.dev/blog/pgo-preview)。 > 原文發佈於2023年2月8日 在構建Go二進位文件時,Go編譯器會進行優化,以儘可能生成性能最佳的二進位文件。例如,常量傳播可以在編譯時對常量表達式進行求值,避免了運行時的計算開銷;逃逸分析可以避免對局部作用域對 ...
  • # 記錄一個在寫項目中遇到的Maven依賴無法導入的問題 項目是一個父項目做依賴管理,三個子項目,今天遇到一個問題: 子項目中導入的依賴,怎麼都導入不進去,maven倉庫中已經有了,idea提示也沒有問題,如圖: ![](https://img2023.cnblogs.com/blog/295824 ...
  • ## 教程簡介 Bootstrap入門教程 - 從基本到高級概念的簡單簡單步驟學習Bootstrap,其中包括概述,環境設置,基本結構,全局樣式,網格系統,流體網格系統,佈局,響應式設計,排版,表格,按鈕,圖像,圖標,下拉,按鈕組,導航元素,導航欄,麵包屑,分頁,標簽,徽章,版式,縮略圖,警報,進度 ...
  • ## 教程簡介 CouchDB 是一個開源的面向文檔的資料庫管理系統,可以通過 RESTful JavaScript Object Notation (JSON) API 訪問。術語 “Couch” 是 “Cluster Of Unreliable Commodity Hardware” 的首字母縮 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...