《算法导论》第 12 章 - 二叉搜索树

大家好,今天我们来深入学习《算法导论》第 12 章的内容 ------ 二叉搜索树(Binary Search Tree, BST)。二叉搜索树是一种经典的数据结构,它结合了链表的动态性和有序数组的高效查询能力,在实际开发中有着广泛的应用。

本文将按照《算法导论》的章节结构,详细讲解二叉搜索树的定义、查询、插入、删除等操作,并提供完整可运行的 C++ 代码实现,帮助大家更好地理解和实践。

12.1 什么是二叉搜索树

12.1.1 二叉搜索树的定义

二叉搜索树是一种二叉树,它可以为空,或者满足以下性质:

  • x是二叉搜索树中的一个节点。如果yx左子树中的一个节点,那么y.key <= x.key
  • 如果yx右子树中的一个节点,那么y.key >= x.key

简单来说,就是左子树所有节点的值不大于根节点的值,右子树所有节点的值不小于根节点的值,并且这个性质对树中的每个节点都成立。

12.1.2 二叉搜索树的示例

下面是一个二叉搜索树的示例:

复制代码
      15
     /  \
    6   18
   / \  /
  3  7 17
 / \  \
2  4  13
      /
     9

在这个树中,每个节点的左子树都满足所有节点值小于该节点值,右子树所有节点值大于该节点值。

12.1.3 二叉搜索树的特性

二叉搜索树的最重要特性是中序遍历(左 - 根 - 右)可以得到一个按升序排列的节点序列

例如,对上面的示例树进行中序遍历,得到的序列是:2, 3, 4, 6, 7, 9, 13, 15, 17, 18,这是一个升序序列。

12.1.4 二叉搜索树的思维导图

12.2 查询二叉搜索树

二叉搜索树的查询操作是其最基本也最重要的操作之一,主要包括:查找特定值、查找最小值、查找最大值、查找前驱和后继。

12.2.1 查找(Search)

查找操作用于在二叉搜索树中查找一个具有特定关键字的节点。

算法思想

  1. 从根节点开始
  2. 如果当前节点为空,说明查找失败
  3. 如果当前节点的关键字等于目标值,查找成功
  4. 如果当前节点的关键字大于目标值,在左子树中查找
  5. 如果当前节点的关键字小于目标值,在右子树中查找

流程图

12.2.2 查找最小值(Minimum)

最小值节点是二叉搜索树中最左侧的节点(即左子树为空的节点)。

算法思想

  1. 从根节点开始
  2. 沿着左子树指针一直向下
  3. 直到找到一个左子树为空的节点,该节点即为最小值节点

12.2.3 查找最大值(Maximum)

最大值节点是二叉搜索树中最右侧的节点(即右子树为空的节点)。

算法思想

  1. 从根节点开始
  2. 沿着右子树指针一直向下
  3. 直到找到一个右子树为空的节点,该节点即为最大值节点

12.2.4 查找前驱(Predecessor)

前驱是指在中序遍历中,位于当前节点之前的节点(即比当前节点小的最大节点)。

算法思想

  1. 如果当前节点有左子树,则前驱是左子树中的最大值节点
  2. 否则,向上查找祖先节点,直到找到一个节点是其父节点的右孩子,该父节点即为前驱

12.2.5 查找后继(Successor)

后继是指在中序遍历中,位于当前节点之后的节点(即比当前节点大的最小节点)。

算法思想

  1. 如果当前节点有右子树,则后继是右子树中的最小值节点
  2. 否则,向上查找祖先节点,直到找到一个节点是其父节点的左孩子,该父节点即为后继

12.2.6 查询操作的代码实现

下面是二叉搜索树查询操作的完整 C++ 实现:

cpp 复制代码
#include <iostream>
using namespace std;

// 二叉搜索树节点结构
template <typename T>
struct Node {
    T key;          // 节点关键字
    Node* left;     // 左孩子指针
    Node* right;    // 右孩子指针
    Node* parent;   // 父节点指针
    
    // 构造函数
    Node(T val) : key(val), left(nullptr), right(nullptr), parent(nullptr) {}
};

// 二叉搜索树类
template <typename T>
class BST {
private:
    Node<T>* root;  // 根节点指针
    
    // 递归查找
    Node<T>* search(Node<T>* x, T val) {
        // 如果节点为空或找到目标值,返回该节点
        if (x == nullptr || x->key == val) {
            return x;
        }
        // 如果目标值小于当前节点值,在左子树中查找
        if (val < x->key) {
            return search(x->left, val);
        }
        // 否则在右子树中查找
        else {
            return search(x->right, val);
        }
    }
    
    // 查找最小值
    Node<T>* minimum(Node<T>* x) {
        // 一直向左查找,直到左子树为空
        while (x != nullptr && x->left != nullptr) {
            x = x->left;
        }
        return x;
    }
    
    // 查找最大值
    Node<T>* maximum(Node<T>* x) {
        // 一直向右查找,直到右子树为空
        while (x != nullptr && x->right != nullptr) {
            x = x->right;
        }
        return x;
    }
    
    // 私有版本的查找前驱
    Node<T>* predecessorHelper(Node<T>* x) {
        // 如果有左子树,前驱是左子树的最大值
        if (x->left != nullptr) {
            return maximum(x->left);
        }
        
        // 否则向上查找
        Node<T>* y = x->parent;
        while (y != nullptr && x == y->left) {
            x = y;
            y = y->parent;
        }
        return y;
    }
    
    // 私有版本的查找后继
    Node<T>* successorHelper(Node<T>* x) {
        // 如果有右子树,后继是右子树的最小值
        if (x->right != nullptr) {
            return minimum(x->right);
        }
        
        // 否则向上查找
        Node<T>* y = x->parent;
        while (y != nullptr && x == y->right) {
            x = y;
            y = y->parent;
        }
        return y;
    }
    
    // 中序遍历
    void inorderTraversal(Node<T>* x) {
        if (x != nullptr) {
            inorderTraversal(x->left);  // 遍历左子树
            cout << x->key << " ";      // 访问当前节点
            inorderTraversal(x->right); // 遍历右子树
        }
    }
    
public:
    // 构造函数
    BST() : root(nullptr) {}
    
    // 查找操作
    Node<T>* search(T val) {
        return search(root, val);
    }
    
    // 查找最小值
    Node<T>* minimum() {
        return minimum(root);
    }
    
    // 查找最大值
    Node<T>* maximum() {
        return maximum(root);
    }
    
    // 查找前驱
    Node<T>* predecessor(Node<T>* x) {
        if (x == nullptr) return nullptr;
        return predecessorHelper(x);
    }
    
    // 查找后继
    Node<T>* successor(Node<T>* x) {
        if (x == nullptr) return nullptr;
        return successorHelper(x);
    }
    
    // 中序遍历
    void inorderTraversal() {
        inorderTraversal(root);
        cout << endl;
    }
    
    // 设置根节点(用于测试)
    void setRoot(Node<T>* r) {
        root = r;
    }
};

// 测试代码
int main() {
    BST<int> bst;
    
    // 手动构建一个示例树
    //        15
    //       /  \
    //      6   18
    //     / \  /
    //    3  7 17
    //   / \  \
    //  2  4  13
    //        /
    //       9
    
    Node<int>* node15 = new Node<int>(15);
    Node<int>* node6 = new Node<int>(6);
    Node<int>* node18 = new Node<int>(18);
    Node<int>* node3 = new Node<int>(3);
    Node<int>* node7 = new Node<int>(7);
    Node<int>* node17 = new Node<int>(17);
    Node<int>* node2 = new Node<int>(2);
    Node<int>* node4 = new Node<int>(4);
    Node<int>* node13 = new Node<int>(13);
    Node<int>* node9 = new Node<int>(9);
    
    // 设置根节点
    bst.setRoot(node15);
    
    // 构建树结构
    node15->left = node6;
    node15->right = node18;
    node6->parent = node15;
    node18->parent = node15;
    
    node6->left = node3;
    node6->right = node7;
    node3->parent = node6;
    node7->parent = node6;
    
    node18->left = node17;
    node17->parent = node18;
    
    node3->left = node2;
    node3->right = node4;
    node2->parent = node3;
    node4->parent = node3;
    
    node7->right = node13;
    node13->parent = node7;
    
    node13->left = node9;
    node9->parent = node13;
    
    // 演示中序遍历(应输出升序序列)
    cout << "中序遍历结果: ";
    bst.inorderTraversal();
    
    // 演示查找操作
    int target = 7;
    Node<int>* found = bst.search(target);
    if (found != nullptr) {
        cout << "找到节点: " << found->key << endl;
    } else {
        cout << "未找到节点: " << target << endl;
    }
    
    // 演示查找最小值和最大值
    Node<int>* minNode = bst.minimum();
    Node<int>* maxNode = bst.maximum();
    cout << "最小值: " << minNode->key << endl;
    cout << "最大值: " << maxNode->key << endl;
    
    // 演示查找前驱和后继
    Node<int>* x = bst.search(13);
    if (x != nullptr) {
        Node<int>* pred = bst.predecessor(x);
        Node<int>* succ = bst.successor(x);
        cout << x->key << "的前驱是: " << (pred ? pred->key : -1) << endl;
        cout << x->key << "的后继是: " << (succ ? succ->key : -1) << endl;
    }
    
    // 释放内存
    delete node2;
    delete node3;
    delete node4;
    delete node6;
    delete node7;
    delete node9;
    delete node13;
    delete node15;
    delete node17;
    delete node18;
    
    return 0;
}

12.2.7 查询操作的综合案例

下面是一个演示二叉搜索树查询操作的综合案例:

cpp 复制代码
#include <iostream>
using namespace std;

// 二叉搜索树节点结构
template <typename T>
struct Node {
    T key;          // 节点关键字
    Node* left;     // 左孩子指针
    Node* right;    // 右孩子指针
    Node* parent;   // 父节点指针
    
    // 构造函数
    Node(T val) : key(val), left(nullptr), right(nullptr), parent(nullptr) {}
};

// 二叉搜索树类
template <typename T>
class BST {
private:
    Node<T>* root;  // 根节点指针
    
    // 递归查找
    Node<T>* search(Node<T>* x, T val) {
        // 如果节点为空或找到目标值,返回该节点
        if (x == nullptr || x->key == val) {
            return x;
        }
        // 如果目标值小于当前节点值,在左子树中查找
        if (val < x->key) {
            return search(x->left, val);
        }
        // 否则在右子树中查找
        else {
            return search(x->right, val);
        }
    }
    
    // 查找最小值
    Node<T>* minimum(Node<T>* x) {
        // 一直向左查找,直到左子树为空
        while (x != nullptr && x->left != nullptr) {
            x = x->left;
        }
        return x;
    }
    
    // 查找最大值
    Node<T>* maximum(Node<T>* x) {
        // 一直向右查找,直到右子树为空
        while (x != nullptr && x->right != nullptr) {
            x = x->right;
        }
        return x;
    }
    
    // 私有版本的查找前驱
    Node<T>* predecessorHelper(Node<T>* x) {
        // 如果有左子树,前驱是左子树的最大值
        if (x->left != nullptr) {
            return maximum(x->left);
        }
        
        // 否则向上查找
        Node<T>* y = x->parent;
        while (y != nullptr && x == y->left) {
            x = y;
            y = y->parent;
        }
        return y;
    }
    
    // 私有版本的查找后继
    Node<T>* successorHelper(Node<T>* x) {
        // 如果有右子树,后继是右子树的最小值
        if (x->right != nullptr) {
            return minimum(x->right);
        }
        
        // 否则向上查找
        Node<T>* y = x->parent;
        while (y != nullptr && x == y->right) {
            x = y;
            y = y->parent;
        }
        return y;
    }
    
    // 中序遍历
    void inorderTraversal(Node<T>* x) {
        if (x != nullptr) {
            inorderTraversal(x->left);  // 遍历左子树
            cout << x->key << " ";      // 访问当前节点
            inorderTraversal(x->right); // 遍历右子树
        }
    }
    
public:
    // 构造函数
    BST() : root(nullptr) {}
    
    // 查找操作
    Node<T>* search(T val) {
        return search(root, val);
    }
    
    // 查找最小值
    Node<T>* minimum() {
        return minimum(root);
    }
    
    // 查找最大值
    Node<T>* maximum() {
        return maximum(root);
    }
    
    // 查找前驱
    Node<T>* predecessor(Node<T>* x) {
        if (x == nullptr) return nullptr;
        return predecessorHelper(x);
    }
    
    // 查找后继
    Node<T>* successor(Node<T>* x) {
        if (x == nullptr) return nullptr;
        return successorHelper(x);
    }
    
    // 中序遍历
    void inorderTraversal() {
        inorderTraversal(root);
        cout << endl;
    }
    
    // 设置根节点(用于测试)
    void setRoot(Node<T>* r) {
        root = r;
    }
};

// 查询操作综合案例
int main() {
    // 创建一个二叉搜索树并手动插入节点
    BST<int> bst;
    
    // 手动构建一个示例树
    //        15
    //       /  \
    //      6   18
    //     / \  /
    //    3  7 17
    //   / \  \
    //  2  4  13
    //        /
    //       9
    
    Node<int>* node15 = new Node<int>(15);
    Node<int>* node6 = new Node<int>(6);
    Node<int>* node18 = new Node<int>(18);
    Node<int>* node3 = new Node<int>(3);
    Node<int>* node7 = new Node<int>(7);
    Node<int>* node17 = new Node<int>(17);
    Node<int>* node2 = new Node<int>(2);
    Node<int>* node4 = new Node<int>(4);
    Node<int>* node13 = new Node<int>(13);
    Node<int>* node9 = new Node<int>(9);
    
    // 设置根节点
    bst.setRoot(node15);
    
    // 构建树结构
    node15->left = node6;
    node15->right = node18;
    node6->parent = node15;
    node18->parent = node15;
    
    node6->left = node3;
    node6->right = node7;
    node3->parent = node6;
    node7->parent = node6;
    
    node18->left = node17;
    node17->parent = node18;
    
    node3->left = node2;
    node3->right = node4;
    node2->parent = node3;
    node4->parent = node3;
    
    node7->right = node13;
    node13->parent = node7;
    
    node13->left = node9;
    node9->parent = node13;
    
    // 演示中序遍历(应输出升序序列)
    cout << "中序遍历结果: ";
    bst.inorderTraversal();
    
    // 演示查找操作
    int target = 7;
    Node<int>* found = bst.search(target);
    if (found != nullptr) {
        cout << "找到节点: " << found->key << endl;
    } else {
        cout << "未找到节点: " << target << endl;
    }
    
    // 演示查找最小值和最大值
    Node<int>* minNode = bst.minimum();
    Node<int>* maxNode = bst.maximum();
    cout << "最小值: " << minNode->key << endl;
    cout << "最大值: " << maxNode->key << endl;
    
    // 演示查找前驱和后继
    Node<int>* x = bst.search(13);
    if (x != nullptr) {
        Node<int>* pred = bst.predecessor(x);
        Node<int>* succ = bst.successor(x);
        cout << x->key << "的前驱是: " << (pred ? pred->key : -1) << endl;
        cout << x->key << "的后继是: " << (succ ? succ->key : -1) << endl;
    }
    
    // 释放内存
    delete node2;
    delete node3;
    delete node4;
    delete node6;
    delete node7;
    delete node9;
    delete node13;
    delete node15;
    delete node17;
    delete node18;
    
    return 0;
}

注意:为了使上述代码能够运行,需要在 BST 类中添加一个setRoot方法用于设置根节点,代码如下:

复制代码
// 在BST类的public部分添加
void setRoot(Node<T>* r) {
    root = r;
}

12.3 插入和删除

除了查询操作,插入和删除也是二叉搜索树的基本操作。

12.3.1 插入(Insert)

插入操作用于在二叉搜索树中插入一个新节点,同时保持二叉搜索树的性质。

算法思想

  1. 从根节点开始,找到新节点的合适插入位置
  2. 新节点总是作为叶子节点插入
  3. 插入过程与查找过程类似,只是需要记录插入位置的父节点

流程图

12.3.2 删除(Delete)

删除操作相对复杂,需要考虑多种情况,同时保持二叉搜索树的性质。

算法思想

  1. 如果被删除节点没有孩子,直接删除
  2. 如果被删除节点只有一个孩子,用孩子替换该节点
  3. 如果被删除节点有两个孩子,找到该节点的后继(或前驱),用后继替换该节点,然后删除后继

为了简化删除操作,《算法导论》中引入了一个辅助函数transplant,用于将一棵子树替换为另一棵子树。

流程图

12.3.3 插入和删除操作的代码实现

下面是在 BST 类中添加插入和删除操作的代码:

复制代码
// 在BST类的public部分添加
// 插入操作
void insert(T val) {
    Node<T>* z = new Node<T>(val);
    insert(z);
}

// 删除操作
void remove(Node<T>* z) {
    if (z == nullptr) return;
    
    // 情况1:没有左孩子
    if (z->left == nullptr) {
        transplant(z, z->right);
    }
    // 情况2:有左孩子但没有右孩子
    else if (z->right == nullptr) {
        transplant(z, z->left);
    }
    // 情况3:有两个孩子
    else {
        Node<T>* y = minimum(z->right);  // 找到后继
        // 如果后继不是直接右孩子
        if (y->parent != z) {
            transplant(y, y->right);     // 用后继的右孩子替换后继
            y->right = z->right;         // 后继的右指针指向z的右孩子
            y->right->parent = y;
        }
        transplant(z, y);                // 用后继替换z
        y->left = z->left;               // 后继的左指针指向z的左孩子
        y->left->parent = y;
    }
    
    delete z;  // 释放被删除节点的内存
}

// 在BST类的private部分添加
// 插入操作
void insert(Node<T>* z) {
    Node<T>* y = nullptr;      // 记录x的父节点
    Node<T>* x = root;         // 从根节点开始
    
    // 找到插入位置
    while (x != nullptr) {
        y = x;                 // 更新父节点
        if (z->key < x->key) { // 比较关键字,决定向左还是向右
            x = x->left;
        } else {
            x = x->right;
        }
    }
    
    z->parent = y; // 设置z的父节点
    
    // 如果树为空,z成为根节点
    if (y == nullptr) {
        root = z;
    }
    // 否则根据关键字大小插入到左或右
    else if (z->key < y->key) {
        y->left = z;
    } else {
        y->right = z;
    }
}

// 替换子树辅助函数
void transplant(Node<T>* u, Node<T>* v) {
    // 如果u是根节点,v成为新的根节点
    if (u->parent == nullptr) {
        root = v;
    }
    // 如果u是左孩子,v成为其父节点的新左孩子
    else if (u == u->parent->left) {
        u->parent->left = v;
    }
    // 否则v成为其父节点的新右孩子
    else {
        u->parent->right = v;
    }
    
    // 如果v不为空,更新其父节点指针
    if (v != nullptr) {
        v->parent = u->parent;
    }
}

12.3.4 插入和删除操作的综合案例

下面是一个演示二叉搜索树插入和删除操作的综合案例:

复制代码
// 插入和删除操作综合案例
int main() {
    BST<int> bst;
    
    // 插入节点
    int values[] = {15, 6, 18, 3, 7, 17, 2, 4, 13, 9};
    int n = sizeof(values) / sizeof(values[0]);
    
    cout << "插入顺序: ";
    for (int i = 0; i < n; i++) {
        cout << values[i] << " ";
        bst.insert(values[i]);
    }
    cout << endl;
    
    cout << "插入后的中序遍历: ";
    bst.inorderTraversal();
    
    // 测试删除操作
    // 删除叶子节点(4)
    Node<int>* node4 = bst.search(4);
    if (node4 != nullptr) {
        bst.remove(node4);
        cout << "删除4后的中序遍历: ";
        bst.inorderTraversal();
    }
    
    // 删除只有一个孩子的节点(7)
    Node<int>* node7 = bst.search(7);
    if (node7 != nullptr) {
        bst.remove(node7);
        cout << "删除7后的中序遍历: ";
        bst.inorderTraversal();
    }
    
    // 删除有两个孩子的节点(6)
    Node<int>* node6 = bst.search(6);
    if (node6 != nullptr) {
        bst.remove(node6);
        cout << "删除6后的中序遍历: ";
        bst.inorderTraversal();
    }
    
    // 测试插入新节点
    bst.insert(8);
    cout << "插入8后的中序遍历: ";
    bst.inorderTraversal();
    
    return 0;
}

12.4 随机构建二叉搜索树

到目前为止,我们讨论的二叉搜索树的性能很大程度上取决于输入数据的顺序。如果输入数据已经排序,那么构建的二叉搜索树将退化为链表,此时各种操作的时间复杂度将变为 O (n)。

为了避免这种情况,我们可以采用随机化方法构建二叉搜索树,即随机排列输入序列,然后再插入到二叉搜索树中

12.4.1 随机构建二叉搜索树的性质

随机构建的二叉搜索树具有以下重要性质:

  • 对于一个包含 n 个不同元素的集合,随机构建的二叉搜索树的期望高度为 O (log n)
  • 因此,所有基本操作(查找、插入、删除)的期望时间复杂度均为 O (log n)

12.4.2 随机构建二叉搜索树的代码实现

下面是随机构建二叉搜索树的代码实现:

复制代码
#include <cstdlib>
#include <ctime>
#include <algorithm>

// 在BST类中添加随机构建函数
public:
    // 从数组随机构建二叉搜索树
    void buildRandomBST(T arr[], int n) {
        // 随机打乱数组
        srand(time(nullptr));
        random_shuffle(arr, arr + n);
        
        // 清空现有树(简化实现,实际应用中需要完整的删除操作)
        root = nullptr;
        
        // 依次插入打乱后的元素
        for (int i = 0; i < n; i++) {
            insert(arr[i]);
        }
    }

12.4.3 随机构建二叉搜索树的综合案例

下面是一个演示随机构建二叉搜索树的综合案例:

复制代码
// 随机构建二叉搜索树综合案例
int main() {
    BST<int> bst;
    
    // 创建一个有序数组
    const int n = 10;
    int arr[n];
    for (int i = 0; i < n; i++) {
        arr[i] = i + 1;
    }
    
    cout << "原始有序数组: ";
    for (int i = 0; i < n; i++) {
        cout << arr[i] << " ";
    }
    cout << endl;
    
    // 随机构建二叉搜索树
    bst.buildRandomBST(arr, n);
    
    cout << "随机构建的BST中序遍历: ";
    bst.inorderTraversal();
    
    // 查找最大值和最小值
    Node<int>* minNode = bst.minimum();
    Node<int>* maxNode = bst.maximum();
    cout << "最小值: " << minNode->key << endl;
    cout << "最大值: " << maxNode->key << endl;
    
    // 查找特定值
    int target = 5;
    Node<int>* found = bst.search(target);
    if (found != nullptr) {
        cout << "找到节点: " << found->key << endl;
        Node<int>* pred = bst.predecessor(found);
        Node<int>* succ = bst.successor(found);
        cout << target << "的前驱是: " << (pred ? pred->key : -1) << endl;
        cout << target << "的后继是: " << (succ ? succ->key : -1) << endl;
    } else {
        cout << "未找到节点: " << target << endl;
    }
    
    return 0;
}

思考题

  1. 证明:在二叉搜索树中,一个节点的前驱节点的关键字一定小于该节点的关键字,后继节点的关键字一定大于该节点的关键字。

  2. 设计一个算法,判断一棵二叉树是否为二叉搜索树。

  3. 对于 n 个关键字,有多少种不同形态的二叉搜索树?

  4. 设计一个算法,在 O (n) 时间内将一棵二叉搜索树转换为一个有序链表。

  5. 假设我们有一个包含 n 个元素的二叉搜索树,设计一个算法,找到第 k 小的元素,要求时间复杂度为 O (h),其中 h 是树的高度。

本章注记

二叉搜索树是计算机科学中最基础也最重要的数据结构之一,它首次由 Perliss 在 1960 年提出。二叉搜索树结合了链表的动态性和有序数组的高效查询能力,是许多复杂数据结构的基础,如红黑树、B 树等。

虽然二叉搜索树的最坏情况时间复杂度为 O (n),但在实际应用中表现良好,尤其是当输入数据具有随机性时。对于需要保证最坏情况性能的场景,可以使用平衡二叉搜索树,如 AVL 树、红黑树等,这些数据结构通过各种旋转操作来维持树的平衡性,确保所有操作的时间复杂度为 O (log n)。

二叉搜索树在数据库索引、编译器设计、操作系统等领域有着广泛的应用。理解二叉搜索树的原理和操作是深入学习更复杂数据结构的基础。

相关推荐
2301_7850381814 分钟前
c++初学day1(类比C语言进行举例,具体原理等到学到更深层的东西再进行解析)
c语言·c++·算法
Dream it possible!1 小时前
LeetCode 面试经典 150_数组/字符串_加油站(14_134_C++_中等)(贪心算法)
c++·leetcode·面试
EnzoRay1 小时前
C++(一)
c++
星期天要睡觉1 小时前
机器学习——支持向量机(SVM)
算法·机器学习·支持向量机·svm
已读不回1431 小时前
LRU算法在前端性能优化中的实践艺术(缓存请求函数为例)
javascript·算法
ZLRRLZ2 小时前
【数据结构】哈希扩展学习
数据结构·学习·哈希算法
大熊背2 小时前
基于人眼视觉特性的相关图像增强基础知识介绍
人工智能·算法·计算机视觉
范特西_3 小时前
不同的子序列-二维动态规划
算法·动态规划
aluluka3 小时前
Emacs 折腾日记(二十九)—— 打造C++ IDE
c++·ide·emacs