典型数据结构学习

典型数据结构概括

一、顺序表

以下是对C++中顺序表(顺序存储结构)的详细解析,结合核心实现原理与代码示例:

1、顺序表的基本概念

顺序表是线性表的顺序存储结构,其特点为:

  1. 逻辑相邻性:逻辑上相邻的元素在物理存储地址上也相邻。

  2. 随机访问:通过首地址和元素索引可在O(1)时间内访问任意元素。

  3. 存储密度高:仅存储数据元素,无额外指针开销。

  4. 动态扩展性:动态分配方式支持容量调整(需手动或自动扩容)

2、顺序表的结构定义

1. 静态分配
#define MAX_SIZE 100
typedef struct {
    int data[MAX_SIZE];  // 静态数组
    int length;          // 当前元素个数
} SqList;
  • 缺点:容量固定,无法动态扩展
2. 动态分配
typedef struct {
    int *elems;          // 动态数组基地址
    int length;          // 当前元素个数
    int size;            // 总容量
} SqList;
  • 优点:支持动态扩容(如reallocnew重新分配)

3、顺序表的核心操作

1. 初始化与销毁
// 初始化动态顺序表
bool initList(SqList &L, int initSize) {
    L.elems = new int[initSize];
    if (!L.elems) return false;  // 内存分配失败
    L.length = 0;
    L.size = initSize;
    return true;
}

// 销毁顺序表
void destroyList(SqList &L) {
    if (L.elems) delete[] L.elems;
    L.length = L.size = 0;
}
  • 关键点:需检查内存分配是否成功
2. 插入元素
bool insertList(SqList &L, int index, int value) {
    if (index < 0 || index > L.length) return false;  // 索引越界
    if (L.length == L.size) {                         // 容量不足时扩容
        int newSize = L.size * 2;
        int *newElems = new int[newSize];
        memcpy(newElems, L.elems, L.length * sizeof(int));
        delete[] L.elems;
        L.elems = newElems;
        L.size = newSize;
    }
    for (int i = L.length; i > index; i--) {          // 元素后移
        L.elems[i] = L.elems[i-1];
    }
    L.elems[index] = value;
    L.length++;
    return true;
}
  • 时间复杂度:平均O(n),最坏O(n)(需移动所有元素)
3. 删除元素
bool deleteList(SqList &L, int index) {
    if (index < 0 || index >= L.length) return false;
    for (int i = index; i < L.length - 1; i++) {       // 元素前移
        L.elems[i] = L.elems[i+1];
    }
    L.length--;
    return true;
}
  • 时间复杂度:平均O(n)
4. 查找元素
// 按值查找
int findValue(SqList &L, int value) {
    for (int i = 0; i < L.length; i++) {
        if (L.elems[i] == value) return i;
    }
    return -1;
}

// 按索引访问
int getValue(SqList &L, int index) {
    if (index >= 0 && index < L.length) 
        return L.elems[index];
    else 
        throw out_of_range("Invalid index");
}
  • 按值查找:时间复杂度O(n)

  • 按索引访问:时间复杂度O(1)

4、顺序表的优缺点

优点:
  1. 高效随机访问:适合频繁查询的场景。

  2. 内存紧凑:无额外指针开销,存储密度高

缺点:
  1. 插入/删除效率低:需移动大量元素。

  2. 扩容成本高:动态分配需复制全部元素

5、C++标准库中的顺序表(vector

  1. 自动扩容:容量不足时按1.5或2倍扩容(不同编译器实现不同)

  2. 接口丰富:支持push_backinserterase等操作。

  3. 示例代码

#include <vector>
using namespace std;

vector<int> vec;               // 初始化
vec.push_back(10);             // 尾部插入
vec.insert(vec.begin(), 5);    // 头部插入
vec.erase(vec.begin() + 1);    // 删除第二个元素

6、应用场景

  • 适合场景:数据量稳定、查询频繁(如缓存、静态数据集)。

  • 不适用场景:频繁插入/删除(此时链表更优)

总结

顺序表是基础数据结构,理解其实现原理对优化程序性能至关重要。在实际开发中,优先使用标准库vector以简化内存管理。手写顺序表时需注意索引合法性检查动态扩容策略,避免内存泄漏与越界访问。

二、链表

链表是计算机科学中一种基础且重要的动态数据结构,与顺序表(数组)形成互补,适用于需要频繁插入/删除的场景。以下是链表的核心要点及与顺序表的对比:

1、链表的基本结构与类型

  1. 基本结构 链表由节点(Node)构成,每个节点包含两部分:

    • 数据域:存储数据元素(如整型、对象等)。

    • 指针域:指向其他节点的地址(单链表仅含一个指针,双向链表含两个指针)

  2. 主要类型

    • 单链表:每个节点包含指向后继的指针(next),仅支持单向遍历。

      struct Node { int data; Node* next; };
      
    • 双向链表:节点包含前驱(prev)和后继(next)指针,支持双向遍历。

      struct DoublyNode { int data; DoublyNode *prev, *next; };
      
    • 循环链表:尾节点指针指向头节点,形成闭环。分单向和双向循环链表

    • 带头链表:含一个不存储数据的头节点,简化插入/删除逻辑

2、链表的核心操作

  1. 基本操作

    • 插入:在任意位置插入节点仅需调整相邻节点的指针,时间复杂度O(1)。例如,单链表插入步骤:

      1. 创建新节点。

      2. 新节点的next指向插入位置的后继节点。

      3. 前驱节点的next指向新节点

    • 删除:直接修改指针跳过待删节点,时间复杂度O(1)

    • 遍历:从头节点开始顺序访问,时间复杂度O(n),无法随机访问

  2. 动态内存管理

    • 节点通过malloc(C)或new(C++)动态分配内存,需手动释放避免内存泄漏。例如:

      Node* newNode = new Node{value, nullptr};  // 动态分配
      delete node;  // 释放内存
      
    • 内存泄漏风险:若未释放已删除的节点,会导致内存浪费

3、链表的优缺点

  1. 优势

    • 动态扩展:无需预先分配固定内存,按需申请节点,避免空间浪费。

    • 高效增删:插入/删除仅需调整指针,时间效率高,适合频繁修改的场景(如事件队列、内存管理)

  2. 劣势

    • 访问效率低:查找需遍历,时间复杂度O(n)

    • 空间开销大:每个节点需额外存储指针,存储密度低于顺序表。

    • 缓存命中率低:节点物理地址分散,无法利用CPU缓存预加载机制

4、链表的应用场景

  1. 动态数据集合 适用于元素数量不固定且频繁增删的场景(如实时日志记录、任务调度)

  2. 复杂数据结构的基础

    • 内存管理:操作系统用链表管理空闲内存块。

    • 图结构:邻接表表示图的连接关系。

    • LRU缓存淘汰算法:通过链表维护访问顺序

  3. 特定算法需求

    • 大整数运算:链表可存储超长整数。

    • 深度优先搜索(DFS):递归栈的替代实现

5、链表与顺序表的对比

特性顺序表链表
存储方式连续内存离散内存
随机访问支持(O(1))不支持(O(n))
插入/删除效率需移动元素(O(n))仅调整指针(O(1))
空间利用率高(无额外指针)低(指针占用额外空间)
扩容成本需重新分配内存按需动态分配
缓存命中率高(连续存储)低(地址分散)
适用场景查询频繁、数据量稳定增删频繁、数据动态变化

6、C++标准库中的链表实现

  • std::list:双向链表的实现,支持高效插入/删除,提供迭代器操作。例如:

    #include <list>
    std::list<int> myList = {1, 2, 3};
    myList.push_back(4);  // 尾部插入
    myList.erase(it);      // 删除指定位置元素
    
  • std::forward_list:单链表的轻量级实现,内存开销更小

总结

链表通过动态内存管理和指针操作实现了灵活的数据存储,适合频繁修改数据的场景,但需权衡其访问效率与空间开销。在实际开发中,可根据需求选择顺序表或链表,或结合两者优势(如动态数组std::vector与链表std::list的互补使用)。理解链表的底层原理对优化算法(如反转链表、合并有序链表)和解决复杂问题(如环形链表检测)至关重要

三、跳跃表

1、核心结构:

层级节点元素序列(值, 层级)
0—> {4 , 3} —> {5 , 0} —> {16,0} —> {27,5} —> {34,0} —> {47,2} —> {50,0}
1—> {4 , 3} --------------------------------> {27,5} ------------------> {47,2}
2—> {4 , 3} --------------------------------> {27,5} ------------------> {47,2}
3—> {4 , 3} --------------------------------> {27,5}
4-----------------------------------------------> {27,5}
5-----------------------------------------------> {27,5}

2、处理逻辑:

插入:
从最高层级开始向下遍历层级所对应的下一级数据,如果插入数据比当前数据小,则跳过当前数据,前往下一层;如果当前数据比插入数据小,则将当前数据的next域更新为此数据,直到不再有比插入数据小的数据存在。依次执行此逻辑直到遍历完跳跃表的所有层级。
例:欲插入数据:23
则处理逻辑为:创建一个新的update节点,先查看第六层的下一级,是{27,5},因为27>23,所以不做操作转向下一层。第五层同理,转到第四层。第四层的下一级为{4 , 3},因为23>4,则update[3]更新为当前{4 , 3}节点,而后继续跟下一级比较,是{27,5},故不操作转下一层。第三层和第二层同理,并且是从{4 , 3}节点开始向下一级比较。最后一层处理逻辑相同,最终update[0]将指向{16,0}节点。
然后更新跳跃表,创建一个节点s存放23数据,随机选取一个层级,然后根据temp[0~5]的位置插入23数据。
每一层:

s->forward[i] = update[i]->forward[i];     // 将s的下一级更新为update[i]的下一级                               
update[i]->forward[i] = s;                // 将update[i]的下一级更新为s

此时(设23的层级为4)跳跃表结构变为:

层级节点元素序列(值, 层级)
0—> {4 , 3} —> {5 , 0} —> {16,0} —> {23,4} --> {27,5} —> {34,0} —> {47,2} —> {50,0}
1—> {4 , 3} --------------------------------> {23,4} --> {27,5} ------------------> {47,2}
2—> {4 , 3} --------------------------------> {23,4} --> {27,5} ------------------> {47,2}
3—> {4 , 3} --------------------------------> {23,4} --> {27,5}
4-----------------------------------------------> {23,4} --> {27,5}
5-------------------------------------------------------------> {27,5}

3、核心构建:

插入:

bool InsertNode(SkipList* plist, KeyType key, int value) {//插入
    assert(plist != nullptr);
    bool res = false;
    SkipListNode* update[MAX_LEVEL + 1] = {0};//创建一个包含所有层级的指针数组
    //用第0号位表next域
    //利用这个数组记录新的值在多层级内的位置
    //具体操作参见资源文件InsertNode.png
    SkipListNode* p = plist->head;
    for (int i = plist->level; i >= 0; --i) {//外循环表层级的改变
        while (nullptr != p->forward[i] && p->forward[i]->key < key) {//内循环表位置的变换
            p = p->forward[i];
        }
        update[i] = p;
        //记录新的值在所有层级的位置
        //每个层级记录的都是上一个比新值小的值所在位置
    }
    p = p->forward[0];
    //在最底层退出时p指向了前一个比新值小的值
    //此时向后一位就是新值应该处于的地址
    //当想要插入的值是表内最大值时,指针指向nullptr
    if (nullptr == p || p->key != key) {
        res = true;
        int newlevel = randomlevel();//为新值随机一个新层高
        if (newlevel > plist->level) {//如果新层高是跳跃表中的最高层(但是比MAX_LEVEL小)
            for (int i = plist->level + 1; i <= newlevel; ++i) {
                update[i] = plist->head;//?
            }
            plist->level = newlevel;
        }
        SkipListNode* s = BuyNode(newlevel);
        s->key = key;
        s->value = value;
        s->level = newlevel;
        for (int i = 0; i <= newlevel; ++i) {
            //assert(s->forward[i] != nullptr && update[i] != nullptr);
            s->forward[i] = update[i]->forward[i];
            update[i]->forward[i] = s;
        }
    }
    return res;
}

查询:

SkipListNode* SearchKey(const SkipList* plist, KeyType key) {//搜索
    assert(plist != nullptr);
    SkipListNode* p = plist->head;
    for (int i = plist->level; i >= 0; --i) {
        //i初始化为最高层,从最高层开始查找
        while (nullptr != p->forward[0] && p->forward[i]->key < key) {
            p = p->forward[i];
            /*
            * 如果i没有到最底层(0层)则在高层之间快速跳跃
            * 如果i到达底层则变为顺序查找,直到找到当前值不小于目标值时退出
            * 如果当前值更大,就跳转到下一个多层(或底层,如果i为0)结点
            */
        }
    }
    p = p->forward[0];
    if (nullptr != p && p->key != key) {
        p = nullptr;//如果没有找到
        //注意这个写法,并不是找到后返回p,很俏皮
    }
    return p;
}

移除:

bool RemoveNode(SkipList* plist, KeyType key) {
    assert(plist != nullptr);
    SkipListNode* p = plist->head;
    SkipListNode* update[MAX_LEVEL + 1] = { 0 };
    //用来更新层级指针的中间位
    //0位表示next域
    for (int i = plist->level; i >= 0; --i) {//外循环表示层级的改变
        while (nullptr != p->forward[i] && p->forward[i]->key < key) {//内循环表示位置的变换
            p = p->forward[i];
        }
        update[i] = p;
        //记录查找值在所有层级的位置
        //每个层级记录的都是上一个比查找值小的值所在位置
    }
    p = p->forward[0];
    //在最底层退出时p指向了前一个比查找值小的值
    //此时向后一位就是查找值应该处于的地址
    //然后开始删除↓
    if (nullptr != p && p->key == key) {
        for (int i = 0; i <= plist->level; ++i) {
            if (update[i]->forward[i] != p) {
                break;
            }
            update[i]->forward[i] = p->forward[i];
        }
        //for(int i = 0; i <= p->level; ++i) {
        //    update[i]->forward[i]=p->forward[i];
        //}
        FreeNode(p);
        --plist->cursize;
        while (plist->level > 0 && plist->head->forward[plist->level] == nullptr) {
            --plist->level;
            //如果删除了当前元素导致跳跃表最高层减少
            //则我们把跳跃表的最高层减少
        }
        return true;
    }
    return false;
}

四、栈

1、栈的定义与特性

栈(Stack)是一种先进后出(LIFO)的线性数据结构,仅允许在栈顶(Top)进行插入(Push)和删除(Pop)操作。其核心特性包括:

  1. 受限访问:只能通过栈顶操作元素,无法直接访问中间或底部元素。

  2. 动态或静态实现:可用数组(静态栈)或链表(动态栈)实现,前者固定大小,后者灵活扩容

  3. 操作复杂度:入栈(Push)和出栈(Pop)的时间复杂度均为 O(1),查询中间元素需遍历,复杂度为 O(n)

2、栈的实现方式
1. 静态栈(数组实现)
  • 原理:使用数组存储元素,栈顶指针 top 初始化为 -1,标识空栈。

  • 关键代码示例

    class Stack {
        int maxTop, top;
        double* values;
    public:
        Stack(int size) { 
            maxTop = size - 1; 
            values = new double[size]; 
            top = -1; 
        }
        void Push(double x) {
            if (IsFull()) { /* 处理栈满 */ }
            values[++top] = x;
        }
        double Pop() { return values[top--]; }
    };
    
  • 特点:实现简单,但容量固定,需预分配空间

2. 动态栈(链表实现)
  • 原理:使用单向链表的头插法,头节点作为栈顶,无需预先分配空间。

  • 关键代码示例

    template <class T>
    class LinkedStack {
        struct Node { T data; Node* next; };
        Node* head; int cnt;
    public:
        void Push(T val) {
            Node* newNode = new Node{val, head->next};
            head->next = newNode;
            cnt++;
        }
        T Pop() {
            Node* temp = head->next;
            T val = temp->data;
            head->next = temp->next;
            delete temp;
            return val;
        }
    };
    
  • 特点:动态扩容,适合元素数量不确定的场景

3、栈的核心操作
  1. 入栈(Push):将元素添加到栈顶。

  2. 出栈(Pop):移除栈顶元素并返回其值。

  3. 查看栈顶(Top):获取但不移除栈顶元素。

  4. 判空(Empty):检查栈是否为空。

  5. 容量查询(Size):返回当前元素数量

4、C++标准库中的栈(std::stack)

C++标准库提供 std::stack 容器适配器,默认底层容器为 deque,也可指定 vectorlist

  • 常用方法

    stack<int> s;
    s.push(10);       // 入栈
    s.pop();          // 出栈
    int top = s.top();// 获取栈顶
    bool isEmpty = s.empty();
    
  • 底层容器选择

    • deque(默认):头尾操作高效,适合通用场景。

    • vector:尾部操作快,但需连续内存。

    • list:任意位置插入/删除高效

5、栈的应用场景
  1. 逆序输出:利用LIFO特性反转数据顺序,如字符串逆序

  2. 括号匹配:通过栈检查嵌套括号是否正确闭合

  3. 函数调用栈:记录函数调用顺序及局部变量

  4. 撤销操作(Undo):记录操作历史以便回退

  5. 表达式求值:处理后缀表达式(逆波兰表示法)

6、性能优化与注意事项
  1. 栈溢出:静态栈需处理满栈异常;动态栈需避免内存泄漏。

  2. 模板类设计:通过模板支持泛型数据类型,提升复用性

  3. 标准库优化:使用 emplace 替代 push 可避免拷贝开销(C++11+)

7、代码示例(链式栈模板类)
#include <iostream>
template <typename T>
class Stack {
    struct Node { T data; Node* next; };
    Node* topNode; int size;
public:
    Stack() : topNode(nullptr), size(0) {}
    void push(T val) {
        Node* newNode = new Node{val, topNode};
        topNode = newNode;
        size++;
    }
    T pop() {
        if (topNode == nullptr) throw "Empty Stack";
        T val = topNode->data;
        Node* temp = topNode;
        topNode = topNode->next;
        delete temp;
        size--;
        return val;
    }
    T top() const { return topNode->data; }
    bool empty() const { return topNode == nullptr; }
};
总结

栈作为基础数据结构,在C++中可通过数组或链表灵活实现,标准库 std::stack 提供了高效封装。理解其LIFO特性及适用场景(如逆序处理、状态回溯)是算法设计与系统开发的关键。实际应用中需根据场景选择实现方式,并注意异常处理与性能优化。

五、队列

1、队列的基本概念与特性

队列(Queue)是一种先进先出(FIFO, First In First Out)的线性数据结构,其操作限制为只能在队尾(rear)插入元素(入队),在队头(front)删除元素(出队)。其核心特性包括:

  • 入队(Enqueue):在队尾添加新元素,时间复杂度一般为O(1)

  • 出队(Dequeue):在队头移除元素,时间复杂度一般为O(1)

  • 只允许访问队头和队尾元素,中间元素不可直接访问

2、队列的底层实现方式

队列的底层可通过不同数据结构实现,具体选择影响操作的复杂度:

  1. 数组实现(顺序队列)

    • 使用动态数组存储元素,入队时在数组末尾添加元素,出队时删除数组首元素(需移动后续元素,时间复杂度O(n))

    • 循环队列:通过模运算将数组视为环形结构,避免数据移动。当rearfront指针到达数组末尾时,重置为0,适用于固定大小队列

    • 动态数组优化:入队时若空间不足自动扩容(均摊复杂度O(1)),出队时可能缩容

  2. 链表实现(链式队列)

    • 使用头指针(front)指向队首节点,尾指针(rear)指向队尾节点。入队操作在链表尾部插入节点,出队操作删除头节点,时间复杂度均为O(1)

    • 优点:无扩容限制,内存利用率高。

3、C++标准库中的std::queue

std::queue是C++提供的容器适配器,基于底层容器(如dequelist)封装队列操作,头文件为<queue>

  1. 模板参数

    template <class T, class Container = deque<T>> class queue;
    
    • T:元素类型。

    • Container:底层容器类型,需支持push_back()pop_front()等操作,默认使用deque

  2. 核心成员函数

    • 访问元素

      • front():返回队头元素的引用(若队列为空,行为未定义)

      • back():返回队尾元素的引用

    • 容量操作

      • empty():判断队列是否为空

      • size():返回队列中元素个数

    • 修改操作

      • push(const T& val):在队尾插入元素

      • pop():删除队头元素(若队列为空,行为未定义)

      • emplace(args...):直接在队尾构造元素(避免拷贝,C++11起支持)

  3. 底层容器选择的影响

    • deque(默认):支持随机访问,内存分段管理,扩容效率高,适合频繁的入队和出队

    • list:避免内存碎片化,但每次操作涉及动态内存分配,性能略低

4、队列的应用场景
  1. 广度优先搜索(BFS):遍历图或树时按层访问节点,队列存储待处理节点

  2. 消息队列:解耦生产者和消费者,实现异步任务处理

  3. 缓存管理:使用队列淘汰最早进入的元素(如LRU缓存策略)

  4. 多线程同步:任务调度中维护待执行任务的顺序

5、代码示例
  1. 使用std::queue实现队列操作

    #include <queue>
    #include <iostream>
    
    int main() {
        std::queue<int> q;
        q.push(10);  // 入队
        q.push(20);
        std::cout << "队头元素:" << q.front() << "\n";  // 输出10
        q.pop();  // 出队
        std::cout << "新队头元素:" << q.front() << "\n";  // 输出20
        return 0;
    }
    
  2. 自定义循环队列(数组实现)

    class CircularQueue {
    private:
        int *data;
        int front, rear, capacity;
    public:
        CircularQueue(int size) : capacity(size + 1), front(0), rear(0) {
            data = new int[capacity];
        }
        bool enqueue(int val) {
            if ((rear + 1) % capacity == front) return false; // 队满
            data[rear] = val;
            rear = (rear + 1) % capacity;
            return true;
        }
        bool dequeue() {
            if (front == rear) return false; // 队空
            front = (front + 1) % capacity;
            return true;
        }
    };
    
6、复杂度与注意事项
  • 时间复杂度:标准库实现的std::queue入队和出队均为O(1),但若底层容器为动态数组,扩容时存在均摊复杂度

  • 线程安全std::queue非线程安全,多线程环境下需加锁

  • 异常处理:自定义队列需处理队空时出队、队满时入队的边界条件

队列作为基础数据结构,其高效性和简单性使其在算法和系统设计中广泛应用。理解其实现原理及标准库的适配器特性,有助于在开发中灵活选择最优方案。

六、二叉树

1、BST树

(1)BST的基本概念与特性

二叉搜索树(Binary Search Tree, BST) 是一种特殊的二叉树,其核心特性为:

  1. 有序性:任意节点的左子树所有节点值均小于该节点,右子树所有节点值均大于该节点

  2. 递归结构:每个节点的左、右子树本身也是BST

  3. 中序遍历有序:中序遍历BST会得到一个递增序列,这是其核心性质之一

重要性质

  • 最小值位于最左叶子节点,最大值位于最右叶子节点

  • 若树保持平衡(如AVL树、红黑树),增删查操作的时间复杂度为 O(logn);若退化为链表(不平衡),时间复杂度为 O(n)

(2)BST的核心操作
1. 查找操作
  • 原理:从根节点开始,根据目标值大小递归向左/右子树搜索

  • 代码示例(递归与非递归)

// 递归查找
TreeNode* searchBST(TreeNode* root, int target) {
    if (!root) return nullptr;
    if (target < root->val) return searchBST(root->left, target);
    else if (target > root->val) return searchBST(root->right, target);
    return root;
}

// 非递归查找
TreeNode* searchBST(TreeNode* root, int target) {
    while (root) {
        if (root->val == target) break;
        root = (target < root->val) ? root->left : root->right;
    }
    return root;
}
2. 插入操作
  • 原理:找到合适位置(叶子节点)插入新节点,保持BST的有序性

  • 代码示例

TreeNode* insert(TreeNode* root, int val) {
    if (!root) return new TreeNode(val);
    if (val < root->val) root->left = insert(root->left, val);
    else if (val > root->val) root->right = insert(root->right, val);
    return root;
}
3. 删除操作

删除操作需分三种情况处理:

  • 叶子节点:直接删除。

  • 单子节点:用子节点替换被删节点。

  • 双子节点:用左子树最大值或右子树最小值替换被删节点,并递归删除替换节点

代码示例

TreeNode* deleteNode(TreeNode* root, int key) {
    if (!root) return nullptr;
    if (key < root->val) root->left = deleteNode(root->left, key);
    else if (key > root->val) root->right = deleteNode(root->right, key);
    else {
        // 找到要删除的节点
        if (!root->left) return root->right;    // 只有右子树
        if (!root->right) return root->left;    // 只有左子树
        // 找到右子树最小节点替换
        TreeNode* minNode = getMin(root->right);
        root->val = minNode->val;
        root->right = deleteNode(root->right, minNode->val);
    }
    return root;
}

解释:

  • 如果要删除的节点没有左子树,则直接返回其右子树。

  • 如果要删除的节点没有右子树,则直接返回其左子树。

  • 如果要删除的节点既有左子树又有右子树,则找到右子树中的最小节点(即右子树的最左节点),用其值替换要删除的节点的值,然后递归删除右子树中的这个最小节点。

(3)BST的应用场景
  1. 高效查找与排序:BST支持 O(logn) 的查找效率,常用于数据库索引、符号表等

  2. 动态集合管理:支持动态数据的快速插入、删除和查询

  3. 范围查询:利用中序遍历特性,快速获取有序数据区间

  4. 算法基础:作为平衡树(如AVL、红黑树)的基础结构,用于优化更复杂的数据操作

(4)复杂度与注意事项
  • 时间复杂度

    • 查找、插入、删除:平均 O(logn),最坏 O(n)(树不平衡时)
  • 注意事项

    • 验证BST:需确保所有子树满足左小右大,不能仅判断父子节点关系,可通过中序遍历或传递最大/最小值验证

    • 平衡优化:实际应用中需结合平衡树技术避免性能退化

(5)C++代码实现示例

以下为BST的完整C++实现框架:

class BST {
private:
    struct Node {
        int val;
        Node *left, *right;
        Node(int v) : val(v), left(nullptr), right(nullptr) {}
    };
    Node* root;

    Node* insert(Node* node, int val) {
        if (!node) return new Node(val);
        if (val < node->val) node->left = insert(node->left, val);
        else if (val > node->val) node->right = insert(node->right, val);
        return node;
    }

    Node* deleteNode(Node* node, int val) { /* 同上 */ }

public:
    BST() : root(nullptr) {}
    void insert(int val) { root = insert(root, val); }
    void remove(int val) { root = deleteNode(root, val); }
    bool contains(int val) { return search(root, val) != nullptr; }
};
(6)扩展思考
  • 平衡BST:通过旋转操作保持树高平衡,如AVL树(严格平衡)和红黑树(近似平衡)

  • 工程应用:数据库索引(如MySQL的B+树索引)和内存缓存(如C++ STL的std::map基于红黑树实现)

2、AVL树

(1)基本概念与特性

AVL树(Adelson-Velsky-Landis树)是首个提出的自平衡二叉搜索树,其核心特性是通过动态调整树结构,保持任意节点的左右子树高度差(平衡因子)绝对值不超过1。这种平衡性使得AVL树在最坏情况下的时间复杂度仍为 O(logn),适用于需要高效查询的场景

核心特性:

  1. 平衡条件:每个节点的左右子树高度差(平衡因子)必须为 -1、0 或 1,即 ∣hleft​−hright​∣≤1。

  2. 递归性质:所有子树本身也是AVL树。

  3. 中序遍历有序:与普通二叉搜索树相同,中序遍历结果为递增序列

(2)平衡因子与节点结构

平衡因子(Balance Factor) 定义为一个节点左子树高度减去右子树高度的值。若平衡因子超出 [-1, 1] 范围,则需通过旋转调整树结构

节点结构(C++示例) AVL树节点通常包含三叉链(父、左、右指针)、键值对及平衡因子:

template<class K, class V>
struct AVLNode {
    AVLNode<K, V>* _left, *_right, *_parent;
    pair<K, V> _kv; // 键值对
    int _bf;        // 平衡因子(或直接记录高度)
    // 构造函数初始化...
};

通过三叉链可回溯父节点,便于旋转操作中调整节点关系

(3)旋转操作:AVL树的自平衡核心

当插入或删除节点导致平衡因子超出范围时,通过四种旋转操作恢复平衡:

1. 单旋操作
  • 左单旋(RR型) 适用场景:新节点插入到右子树的右子树(右右失衡)。操作步骤:

    1. 将失衡节点的右子节点提升为新父节点。

    2. 原右子节点的左子树挂到失衡节点的右侧。

    3. 更新父指针和平衡因子

  • 右单旋(LL型) 适用场景:新节点插入到左子树的左子树(左左失衡)。操作步骤与左单旋对称

二叉树LL失衡

2. 双旋操作
  • 左右双旋(LR型) 适用场景:新节点插入到左子树的右子树(左右失衡)。操作步骤:
    1. 先对左子节点执行左旋(转换为LL型)。

    2. 再对失衡节点执行右旋

二叉树LR失衡

  • 右左双旋(RL型) 适用场景:新节点插入到右子树的左子树(右左失衡)。操作步骤与左右双旋对称

旋转后处理:更新所有相关节点的平衡因子和父指针,确保树结构合法

(4)插入与删除操作
1. 插入操作
  • 步骤

    1. 按二叉搜索树规则插入新节点。

    2. 向上回溯更新父节点的平衡因子。

    3. 若某节点平衡因子变为 ±2,触发旋转调整

  • 平衡因子更新规则

    • 插入左子树:父节点平衡因子减1(_bf--)。

    • 插入右子树:父节点平衡因子加1(_bf++

2. 删除操作
  • 步骤

    1. 删除节点后,向上回溯调整平衡因子。
    2. 若出现失衡,执行旋转操作(可能需多次旋转)。
  • 复杂度:删除操作可能导致从被删节点到根节点的路径上多个节点失衡,需逐层调整

(5)复杂度与性能分析
  • 时间复杂度

    • 查找、插入、删除均需 O(logn),得益于严格的平衡性。

    • 旋转操作本身为 O(1),但插入/删除后可能触发多次旋转

  • 空间复杂度:O(n),每个节点需额外存储平衡因子和父指针

对比普通二叉搜索树:AVL树牺牲部分插入/删除性能(频繁旋转)换取查询效率,适用于读多写少的场景(如数据库索引)

(6)应用场景
  1. 数据库索引:如MySQL的InnoDB引擎使用B+树,但内存数据库可能选用AVL树优化查询。

  2. 实时系统:需要稳定查询响应的场景(如游戏引擎中的空间划分)。

  3. 高频查询场景:如编译器的符号表管理

(7)代码实现要点(C++)
  1. 插入与旋转示例
// 右单旋示例(LL型)
Node* rightRotate(Node* y) {
    Node* x = y->_left;
    y->_left = x->_right;
    if (x->_right) x->_right->_parent = y;
    x->_right = y;
    x->_parent = y->_parent;
    y->_parent = x;
    // 更新平衡因子...
    return x;
}
  1. 平衡因子更新:插入后需沿路径更新父节点直至根节点或平衡因子为0
(8)验证AVL树的平衡性

通过递归检查每个节点的平衡因子是否在 [-1, 1] 范围内,并验证子树高度差:

bool isBalanced(Node* root) {
    if (!root) return true;
    int leftH = height(root->left);
    int rightH = height(root->right);
    if (abs(leftH - rightH) > 1) return false;
    return isBalanced(root->left) && isBalanced(root->right);
}

需同时确保中序遍历有序性

(9)扩展与局限性
  • 局限性:频繁插入/删除时旋转代价较高,此时红黑树(近似平衡)可能更优

  • 改进方向:结合其他平衡策略(如替罪羊树的惰性删除)减少旋转次数

3、红黑树

对于一个AVL树,为了让它重新维持在一个平衡状态,就需要对其进行旋转处理, 那么创建一颗平衡二叉树的成本其实不小. 这个时候就有人开始思考,并且提出了红黑树的理论,红黑树在业界应用很广泛,比如 Java 中的 TreeMap,JDK 1.8 中的 HashMap、C++ STL 中的 map 均是基于红黑树结构实现的。那么红黑树到底比AVL树好在哪里?

(1)红黑树简介

红黑树是一种自平衡的二叉查找树,是一种高效的查找树。它是由 Rudolf Bayer 于1978年发明,在当时被称为平衡二叉 B 树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的红黑树。红黑树具有良好的效率,它可在 O(logN) 时间内完成查找、增加、删除等操作。

(2)为什么需要红黑树?

对于二叉搜索树,如果插入的数据是随机的,那么它就是接近平衡的二叉树,平衡的二叉树,它的操作效率(查询,插入,删除)效率较高,时间复杂度是O(logN)。

但是可能会出现一种极端的情况,那就是插入的数据是有序的(递增或者递减),那么所有的节点都会在根节点的右侧或左侧,此时,二叉搜索树就变为了一个链表,它的操作效率就降低了,时间复杂度为O(N),所以可以认为二叉搜索树的时间复杂度介于O(logN)和O(N)之间,视情况而定。

那么为了应对这种极端情况,红黑树就出现了,它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。

(3)红黑树的特性

红黑树通过以下规则确保平衡性:

  1. 颜色特性:每个节点非红即黑。

  2. 根节点:根必须是黑色。

  3. 叶子节点:所有空节点(NIL节点)视为黑色。

  4. 红色节点约束:红色节点的子节点必须为黑色(不可连续红节点)。

  5. 黑高一致性:从任意节点到其所有叶子节点的路径包含相同数量的黑色节点(称为黑高)

平衡性保证:最长路径长度不超过最短路径的两倍(因最长路径为红黑交替,最短路径为全黑节点)

(4)红黑树的操作原理
  1. 插入操作

    • 新节点默认为红色,插入后若破坏性质则调整:

      • 颜色反转:若父节点和叔节点均为红色,将父、叔变黑,祖父变红,递归向上调整

      • 旋转调整:若父红而叔黑,通过左旋/右旋修复(如LL/RR型需单旋,LR/RL型需双旋)

    • 示例:插入导致连续红节点时,通过旋转将红节点上移并调整颜色

  2. 删除操作

    • 删除后若破坏黑高,需通过借兄弟节点或合并节点调整:

      • 兄弟节点为红色:旋转父节点并重新着色。

      • 兄弟节点为黑色且其子节点有红色:通过旋转和变色修复

    • 复杂度:删除调整最多涉及3次旋转

  3. 旋转操作

    • 左旋:将节点的右子节点提升为父节点,原节点变为左子节点。

    • 右旋:将节点的左子节点提升为父节点,原节点变为右子节点

    • 目的:减少树高差,恢复红黑树性质

颜色标记的本质——减少结构调整频率

红黑树通过红黑交替规则(红色节点不能相邻)和黑高平衡规则(所有路径黑色节点数相同),以颜色为信号动态调整平衡。这种设计允许红黑树在插入或删除时:

  • 优先通过颜色翻转修复冲突:例如插入时若父节点和叔叔都是红色,只需将父、叔变黑,祖父变红即可(无需旋转)。这种操作代价低,仅需局部调整。

  • 仅在必要时触发旋转:若颜色翻转无法解决(如父红叔黑),才通过左旋/右旋调整结构,旋转次数最多2次(插入)或3次(删除),远低于严格平衡的AVL树

示例:插入新节点时,若父节点为红色且叔叔为红色,仅需“父叔变黑、祖父变红”即可(颜色翻转)。这种局部调整避免了全局结构变化。

(5)红黑树与AVL树的对比
特性红黑树AVL树
平衡标准近似平衡(最长路径≤2倍最短)严格平衡(高度差≤1)
插入/删除效率调整次数少,适合频繁修改调整次数多,适合查询为主
查询效率略低于AVL(因树高稍高)最优(严格平衡)
应用场景数据库、文件系统、Java集合内存敏感型场景(如缓存)

总结:红黑树以略微牺牲查询效率为代价,换取了更高的插入/删除性能,适合动态数据集

(6)红黑树的应用场景
  1. 数据库索引:如MySQL的InnoDB引擎使用红黑树优化范围查询。

  2. 内存管理:Linux内核用红黑树管理进程控制块和内存块

  3. 编程语言库:Java的TreeMap、C++ STL的map、JDK 1.8后的HashMap链表树化

  4. 文件系统:如NTFS的文件块管理

(7)红黑树的局限性
  • 线程不安全:并发修改需依赖锁机制(如读写锁、分段锁)

  • 内存占用:非叶子节点需存储颜色标记,略高于普通二叉查找树

4、B树

B树(Balanced Tree)是一种多路平衡搜索树,专为磁盘或其他直接存储设备设计的高效数据结构,广泛应用于数据库、文件系统等场景。与红黑树不同,B树通过多分支结构减少树的高度,从而降低磁盘I/O次数,提升大规模数据访问效率。

(1)B树的核心特性
  1. 多路平衡

    • 每个节点最多包含m个子节点(m为B树的阶数,通常m ≥ 3)。
    • 除根节点外,所有非叶子节点至少有⌈m/2⌉个子节点。
    • 所有叶子节点位于同一层(高度平衡)。
  2. 键值分布规则

    • 每个节点包含k个有序键值(⌈m/2⌉−1 ≤ k ≤ m−1)。
    • 节点中的键值按升序排列,且子节点的键值范围由其父节点键值分割。
  3. 高效磁盘访问

    • 节点大小通常设置为磁盘页(如4KB),单次I/O读取整个节点数据。
    • 通过降低树的高度,减少磁盘访问次数。
(2)B树的节点结构

B树节点包含以下核心字段:

  • 键值数组:存储有序的键值(如K keys[m-1])。
  • 子节点指针数组:指向子节点的指针(如Node* children[m])。
  • 当前键值数量:记录节点中实际存储的键值数(如int num_keys)。
  • 是否为叶子节点:标识节点类型。
template <class K, int M>  // M为B树的阶
struct BTreeNode {
    K keys[M-1];          // 键值数组(最多M-1个键)
    BTreeNode* children[M]; // 子节点指针数组(最多M个子节点)
    int num_keys;          // 当前键值数量
    bool is_leaf;          // 是否为叶子节点
};
(3)B树的查找操作
  1. 流程
    • 从根节点开始,逐层向下查找。
    • 在每个节点中,通过二分法找到第一个不小于目标键的位置。
    • 若当前节点包含目标键,返回结果;否则进入对应子节点。
  2. 时间复杂度:O(logₘN),其中m为阶数,N为总键值数。
(4)B树的插入操作

插入操作需保证节点键值数量不超过m-1,否则需分裂节点

1. 插入步骤
  1. 查找插入位置:找到目标叶子节点并插入键值。
  2. 节点溢出检查
    • 若键值数超过m-1,将中间键值提升到父节点,分裂当前节点为两个子节点。
  3. 递归调整父节点:若父节点溢出,继续分裂并向上传递中间键值。
2. 分裂示例(阶数m=3
  • 原节点:键值[10, 20, 30](溢出)。
  • 分裂后
    • 父节点新增中间键20
    • 分裂为左子节点[10]和右子节点[30]
(5)B树的删除操作

删除操作需保证节点键值数量不小于⌈m/2⌉−1,否则需合并节点借用键值

1. 删除步骤
  1. 查找目标键
    • 若键在叶子节点,直接删除。
    • 若键在非叶子节点,用前驱或后继键替换,转化为删除叶子节点。
  2. 节点下溢检查
    • 借键:若兄弟节点有富余键值,从兄弟节点借一个键。
    • 合并:若兄弟节点无富余键,与兄弟节点合并,并删除父节点中的分割键。
  3. 递归调整父节点:若父节点下溢,继续向上调整。
2. 合并示例(阶数m=3
  • 父节点键值[20],左子节点[10],右子节点[ ](下溢)。
  • 合并后:父节点键值删除,合并左右子节点为[10, 20]
(6)B树与红黑树的对比
特性B树红黑树
分支数多路平衡(阶数m≥3)二叉平衡
平衡规则节点键值数量约束颜色标记和旋转操作
适用场景磁盘存储(减少I/O次数)内存数据(快速动态操作)
树高O(logₘN),更低O(log₂N)
应用实例数据库索引(如MySQL InnoDB)C++ STL(map、set)
(7)B树的应用场景
  1. 数据库索引
    • B+树(B树变种)是数据库的主流索引结构,叶子节点通过链表连接,支持高效范围查询。
    • 例如:MySQL InnoDB引擎的聚簇索引和非聚簇索引。
  2. 文件系统
    • NTFS、ReiserFS等文件系统使用B树管理文件元数据(如目录结构)。
  3. 大规模数据存储
    • 分布式存储系统(如Google Bigtable)利用B树变种优化数据分布和查询。
(8)B树的变种与优化
  1. B+树
    • 所有数据存储在叶子节点,非叶子节点仅作索引。
    • 叶子节点通过链表连接,支持高效范围扫描。
  2. B*树
    • 通过增加节点最小填充因子(如2/3 * m),减少节点分裂频率。
  3. 压缩B树
    • 对键值进行压缩存储,提升单节点存储密度。
总结

B树通过多路平衡设计,将树高控制在极低水平,特别适合磁盘存储场景。其核心优势在于通过单次磁盘I/O读取更多数据,减少访问延迟。与红黑树相比,B树更适用于大规模数据存储和高并发访问,而红黑树则更适合内存中的动态数据操作。理解B树的分裂、合并规则及其变种(如B+树),是掌握现代数据库和文件系统设计的基石。

5、B+树

B+树是一种多路平衡查找树,是B树的优化变体,广泛应用于数据库、文件系统等需要高效处理海量数据的场景。以下从结构特征、操作原理、优势对比及实际应用等方面进行详解:

(1)B+树的核心结构特征
  1. 分层节点设计

    • 内部节点(非叶子节点):仅存储索引键(Key),不保存实际数据(Data),用于导航查询路径

    • 叶子节点:存储全部数据记录(或数据地址),并通过双向链表连接,形成有序序列,支持高效的范围查询

  2. 节点容量规则

    • 对于m阶B+树,每个节点最多包含m-1个键,且子节点数等于键数

    • 所有叶子节点位于同一层级,保证查询路径长度稳定

  3. 键值分布特性

    • 内部节点的键值为子树中的最大值(或最小值),确保层级间的有序性

    • 叶子节点包含完整的键值集合,且数据按顺序紧密排列

(2)B+树的操作原理
  1. 查找操作

    • 从根节点开始,逐层比较键值,定位到目标叶子节点后返回数据。时间复杂度为O(log n),且所有查询均需到达叶子节点,性能稳定

    • 示例:查找键值为K的记录时,通过内部节点的索引快速缩小范围,最终在叶子节点链表中定位

  2. 插入操作

    • 插入数据时,若叶子节点未满,直接插入并保持有序性;若节点已满,则分裂为两个节点,并将中间键提升至父节点,递归调整直至平衡

    • 分裂示例:假设5阶B+树的节点最多容纳4个键,插入第5个键时分裂为两个节点,中间键(如30)上移至父节点

  3. 删除操作

    • 删除数据后,若叶子节点键数低于下限(如m/2),需向兄弟节点借键或合并节点,递归调整父节点键值以维持平衡
(3)B+树与B树的本质区别
  1. 数据存储位置

    • B树:键和数据可能分布在所有节点中。

    • B+树:数据仅存于叶子节点,内部节点仅作索引

  2. 查询稳定性

    • B树:查询可能在内部节点提前终止,性能波动较大。

    • B+树:所有查询必须到达叶子节点,路径长度固定

  3. 范围查询支持

    • B树:需通过中序遍历实现范围查询,效率较低。

    • B+树:叶子节点的链表结构可直接遍历,时间复杂度为O(1)~O(n)

  4. 空间利用率

    • B+树的内部节点仅存键值,单节点可容纳更多索引,树高更低,减少磁盘I/O次数
(4)B+树的优势与应用场景
  1. 优势总结

    • 减少磁盘I/O:高扇出特性降低树高,单次查询访问的磁盘块更少

    • 适合范围查询:叶子链表支持高效的范围扫描和排序

    • 缓存友好:紧凑的索引结构提升内存利用率,局部性原理适配磁盘预读机制

  2. 典型应用

    • 数据库索引(如MySQL InnoDB):B+树索引支持快速点查与范围查询,同时减少全表扫描

    • 文件系统(如NTFS、ReiserFS):高效管理大文件的分块存储与检索

(5)B+树的局限性
  • 插入/删除成本较高:节点分裂与合并可能引发连锁调整,影响写入性能

  • 内存占用:非叶子节点需额外存储索引,可能占用更多内存

(6)与其他数据结构的对比
  1. 红黑树:虽平衡但树高较大,适合内存操作,而B+树专为磁盘设计,减少I/O次数

  2. 哈希表:仅适合等值查询,无法支持范围操作,B+树在综合场景下更具优势

通过上述分析可见,B+树通过独特的结构设计,在数据存储、查询效率与扩展性之间实现了平衡,成为大规模数据管理的核心数据结构之一。其设计理念对现代数据库和存储系统的影响深远,是理解高效数据检索机制的关键

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值