查漏补缺 [一] (树结构, 异常, 归并, 宏定义, 链表创建销毁)

本文介绍了C++中的引用特性,包括常量引用的使用和初始化规则。接着讨论了字符串处理,异常处理机制,如自定义异常类型,并探讨了链表的头插法、尾插法和析构。文章还涵盖了宏定义的原理和作用,归并排序的实现细节,以及红黑树的删除和插入操作。最后,概述了各种平衡二叉树(如BST、AVL、RB树、B树和B+树)的特性和应用场景。
摘要由CSDN通过智能技术生成

引用

引用
- 引用必须初始化
- 引用只能和一个对象绑定, 初始化的值不能是一个字面量 或者 表达式
- 因为通过引用修改原来的值时, 后面两个无法修改
- 引用有严格的类型要求

  • 常量引用
    • 必须初始化
    • 可以和任何值绑定, 对象, 字面量, 表达式都可
      • 因为常量引用无法通过别名改变原来的值
    • 没有类型要求
  • 引用本质上是一个指针常量, 它的指向不可更改, 所以说引用一旦绑定一个对象后就无法更改了. & = * const
int& const a;  // 相当于int* const const a; 自然报错
const int & a ;  // 相当于 const int * const a;
int &a; //不可
int &a = 1; // 不可
int b = 1;
int &a = (b + 1); //不可
double c = 1.0;
int &a = c; // 不可

const int& a; //不可
const int& a = 1; // 可
int b = 2;
const int& a = b+2; // 可
double c = 1.0;
const int& a = c; //可
// 相当于 const int temp = c; const int& a = temp;

字符串输入带空格

void test01()
{
    int a;
    cin>>a;
    cin.ignore(1024, '\n'); // cin.ignore();
    string s;
    getline(cin, s);
    char ss[40];
    cin.getline(ss, 4);
    cout<<s<<endl;
    cout<<ss<<endl;
}

字符串大小写转换

class ToLower
{
public:
    char operator()(char c)
    {
        int inteval = 'A' - 'a';
        if(c >= 'a' && c <= 'z')
            return char(c + inteval);
        else if (c >= 'A' && c <= 'Z')
            return char(c - inteval);
        else
            return c;
    }
};

class MyPrint
{
public:
    void operator()(char c)
    {
        cout<<c;
    }
};

void test02()
{
    string s;
    getline(cin,s);
    transform(s.begin(), s.end(), s.begin(), ToLower());
    for_each(s.begin(), s.end(), MyPrint());
    cout<<endl;
}

string变量占用的内存空间

  • C++中string 是一种类, 它的对象是由固定大小的, 就好像int, char这些内置数据类型. string变量的大小并不随字符串的长度而改变.
  • string s = “asdf”; 变量a存放在栈区, 它指向(堆区中?)存放“asdf”的内存空间
  • 自定义空类的大小为1

异常

  • try块中放置可能发生异常的代码块
  • catch块紧跟try块后面, 用来接收try中抛出的异常
    • 当有多个catch块时, 按照优先匹配原则, 而不是最佳匹配原则.
  • throw 用于在任何地方抛出异常, throw后面的操作数可以是任意类型的表达式, 表达式的类型决定了抛出的异常类型
void A()
{
    try
    {
        throw "A exception";
    }
    catch (bad_alloc e)  //未捕捉到抛出的异常类型, 继续自动向外抛出
    {
        cout<<"A: "<<endl;
    }
}

void B()
{
    try
    {
        A();
    }
    catch (const char* s)	// 捕捉到异常类型, 进行处理. 然后再次向外抛出
    {
        cout<<"B: "<<s<<endl;
        throw s;
    }
}

void C()
{
    try
    {
        B();
    }
    catch (const char* s) // 捕捉到异常, 进行处理后, 不再向外抛出
    {
        cout<<"C: "<<s<<endl;
    }
}

void test06()
{
    C();
}

B: A exception
C: A exception
Program ended with exit code: 0

自定义异常类型

class MyExecption : public exception
{
public:
    // what() 是异常类提供的一个公共方法,它已被所有子异常类重载。返回异常产生的原因。
    // 第一个cosnt表示返回值是一个常量指针, 指向的内容不能修改. 后面一个const表示函数体内不能有值的更改
    // 如果只是返回一个普通变量的引用没有任何意义, 因为函数的返回值只是个赋值操作. 并不会对其进行修改; 除非是f()++, 不赋值直接操作.
    // const修饰
    const char* what() const throw()
    {
        return "this is my exception";
    }
};
void test06()
{
    try
    {
        throw MyExecption();
    }
    catch (MyExecption& e)
    {
        cout<<e.what()<<endl;
    }
}

链表头插法 & 尾插法 & 析构

  • 尾插法需要两个头部节点
  • 当用全局函数析构链表时, 需要传入指针的指针类型, 因为我们是想把一个指针型变量释放, 就必须传入指针型变量的指针. 否则只是把指针型变量的值赋值了一份传入. 无法改变本身

class List
{
public:
    List()
    {
        head = NULL;
        fake_head = new ListNode(0);
        fake_head_backup = fake_head;
    }
    ~List()
    {
        while(head != NULL)
        {
            ListNode* temp = head->next;
            delete head;
            head = temp;
        }
        while(fake_head_backup != NULL)
        {
            ListNode* temp = fake_head_backup->next;
            delete fake_head_backup;
            fake_head_backup = temp;
        }
        cout<<"~List"<<endl;
    }
    void createList_head(int val){
        ListNode* temp = new ListNode(val);
        temp->next = head;
        head = temp;
    }
    void createList_tail(int val) {
        ListNode* temp = new ListNode(val);
        fake_head->next = temp;
        fake_head = temp;
    }
    void print()
    {
        while(head != NULL)
        {
            cout<<head->val<<" ";
            head = head->next;
        }
        while(fake_head_backup->next != NULL)
        {
            fake_head_backup = fake_head_backup->next;
            cout<<fake_head_backup->val<<" ";
        }
    }
    ListNode* head;
    ListNode* fake_head;
    ListNode* fake_head_backup;
};

void destroy(ListNode** head)
{
    while(*head != NULL)
    {
        ListNode* temp = (*head)->next;
        delete *head;
        *head = temp;
    }
    cout<<"destroy list"<<endl;
}

void test07()
{
    List l;
    for (int i = 0; i < 10; i++)
    {
        l.createList_head(i);
    }
    l.print();
}

宏定义

  • 预处理阶段将所有宏名直接替换, 不做计算
  • 放在最前面, 宏名大写, 无类型的, 不做检查, 不计算, 直接替换
  • 宏展开占用编译时间, 函数调用占用运行时间
#define PI 3.14
#define Addone(x) x+1
#define F(x) x*x

void test08()
{
	cout<<Addone(PI)<<endl;
	cout<<F(3+3)<<endl;  //预想6*6, 实际结果是3+3*3+3=15. 宏定义需要加括号#define F(x) (x)*(x)
}

4.14
#define Conn(x,y) x##y  //同类型直接拼接, 返回原来型. x,y只能是常量不能是变量.
// C++中也不能用于字符串类型, 因为字符串类型可以直接用+拼接.
#define ToString(x) #x // 增加两个双引号

int c = 1; int d = 2;
int a = Conn(c, d);  // wrong
string s = Conn("asd", "asd"); //wrong
int a = Conn(12, 23); //a = 1223
string s = ToSting(asdf); // s = "asdf"
  • 用来选择是否包含某些头文件
    • 如果有PI的宏定义, 就包括#include <iostream>文件. 如果没有就不包括
#define PI 3.14

#ifdef PI
#include <iostream>
#endif

归并排序

  • merge时需要临时的额外空间, 为了避免每次都开辟耗费时间, 最初就把这个临时空间的引用当参数传入

  • v.resize(n, vaL) 从新分配v的大小, 比越来小直接截取, 比原来大用val填充.

    • resize后, v.size() 变成了n, v.capacity() 变成了n
  • v.reserve(n) 给容器预留n的内存空间, 防止之后频繁扩大, 有额外开销. 但是这些内存空间是没有初始化的.

    • reserve后, v.size() 还是0, v.capacity() 变成了n
  • 当capacity=0时, 不能用[]索引访问和赋值.

  • 归并排序中的merge部分, 可以用来求逆序对

void merge(vector<int> &v, int l, int mid, int r, vector<int>& temp)
{
    int i = l;
    int j = mid+1;
    int k = 0;
    temp.resize(v.size());
    while(i <= mid && j <= r)
    {
        if (v[i] < v[j])
            temp[k++] = v[i++];
        else
            temp[k++] = v[j++];
    }
    while (i <= mid)
        temp[k++] = v[i++];
    while (j <= r)
        temp[k++] = v[j++];
    k = 0;
    while(l <= r)
        v[l++] = temp[k++];
}
void mergeSort(vector<int> &v, int l, int r, vector<int>& temp)
{
    if (l < r)
    {
        int mid = l + (r-l)/2;
        mergeSort(v, l, mid, temp);
        mergeSort(v, mid+1, r, temp);
        merge(v, l, mid, r, temp);
    }
}

class PrintInt
{
public:
    void operator()(int v)
    {
        cout<<v<<" ";
    }
};

void test09()
{
    int arr[] = {1,5,4,9,8,2,6};
    vector<int> v(begin(arr), end(arr));
    vector<int> temp;
    temp.reserve(v.size());
    for_each(v.begin(), v.end(), PrintInt());
    cout<<endl;
    mergeSort(v, 0, v.size()-1, temp);
    for_each(v.begin(), v.end(), PrintInt());
    cout<<endl;
}

红黑树

  • 所有结点要么是红色, 要么是黑色
  • 根结点必须是黑色
  • 叶子结点的两个字节点(NULL)必须是黑色
  • 不能有连续两个红结点
  • 任意一个结点到其所有叶子结点路径上, 黑色结点数量一样多

红黑树的平衡是指黑色结点数量的平衡, 而不是左右子树高度差不超过1

在这里插入图片描述

删除结点

  • 如果删除结点有后继结点, 就用其后继结点替换原结点作为删除结点
  • 在上一步不满足的情况下, 如果删除结点有前继结点, 就用其前继结点替换原结点作为删除结点
  • 循环上述两步, 直到删除结点为叶子结点
  • 所以删除结点实际上只有两种情况
    • 删除的叶子结点是红色, 直接删除
    • 删除的叶子结点是黑色, 删除后, 进行平衡修复
      • 看兄弟结点

图片来源----@Cailiang 在知乎中关于红黑树删除的回答
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

图片来源----@安卓大叔 在简书中《30张图带你彻底理解红黑树》的回答

在这里插入图片描述

插入操作

  • 找到插入位置后, 最先插入的都是红结点, 之后再进行平衡修复和颜色调整
  • 只有父结点是红色时才需要调整
    • 看叔叔结点
      在这里插入图片描述

在这里插入图片描述

BST、AVL、RB、B、B+

BST

  • 二叉查找树 或 二叉搜索树 : 左结点小于根结点, 右结点大于根结点
  • 查找, 插入, 删除的时间复杂度O(logN)
  • 极端情况, 成了一个类似线性表的结构 (只有左儿子, 或只有右儿子), 时间复杂度O(N)
    • 因此诞生了平衡二叉树

AVL

  • 平衡二叉树 (平衡二叉搜索树) : 在BST的基础上, 规定了左子树和右子树的高度差不超过1
  • 高度差不超过1, 避免了线性链表的情况. 查找稳定
  • 为了维护自身的平衡性, 需要在插入和删除结点时, 进行大量的旋转操作.

RB树

  • 红黑树 : 也是一种自平衡树, 但是不控制左右子树高度差不超过1. 它将结点分为红色和黑色, 通过满足下列五条性质, 也避免了线性链表的情况
    • 所有节点要么黑色要么红色
    • 根结点黑色
    • 叶子结点的两个空自结点也为黑色
    • 不能有两个连续的红色结点
    • 任何一个结点到其所有叶子结点路径上黑色结点数量相同
  • RB树 查找性能上和 AVL树相同.
  • 在插入和删除上, 红黑色牺牲了严格的高度平衡, 结合红黑结点, 减少了旋转操作. 任何不平衡的情况, 都可以在三次旋转内完成. (插入最多两次, 旋转最多三次)
  • RB树的插入, 删除, 查找的时间复杂度都是 O(logN);

B树

  • B树是一种多路平衡树(上面三个都是二路), B是Balance, 描述一个B树, 需要指定它的阶数m, B树有下述五条性质

    • 每个结点最多有m-1个关键字
    • 根结点最少有1个关键字
    • 非根结点最少有m/2个关键字
    • 每个结点中关键字从小到大排列, 每一个关键字的左儿子结点中的关键字都小于它, 右儿子结点中的关键字都大于它
    • 所有叶子结点在同一层, 根结点到每个叶子结点的路径长度相同
  • 插入时, 如果结点数量大于m-1, 就把中间结点上升到父结点, 进行左右分裂

  • 删除时, 如果是非叶子结点, 就用后继结点代替.

  • 删除时如果小于m/2

    • 如果兄弟结点>m/2, 就从父结点中下移一个, 然后用兄弟结点补位
    • 如果兄弟结点=m/2, 就从父结点中下移一个, 然后和兄弟结点合并成一个结点
      • 从父结点下移哪一个, 就要用相应的兄弟结点补位 或 合并

B+树

  • 在B树的基础上进行改进

  • 相同点

    • 每个结点最多m-1个关键字
    • 根结点至少一个, 非根结点至少m/2个
  • 不同点

    • B+树中结点分为内部结点 (索引结点)和叶子结点(数据结点)
    • 内部结点中关键字从小到大排列, 每个关键字左子树的关键子都小于它, 右子树的关键字都大于等于它. 叶子结点中的关键字也是从小到大排列
    • 每个叶子结点都有指向像一个叶子结点的指针, 按关键字从小到大顺序链接
    • 父结点存有右边第一个元素的索引
  • 插入, 当关键字数量大于m-1时, 将中间结点提升为父结点, 进行分裂, 同时中间结点需要在右儿子中

  • 删除, 直接通过兄弟结点移动

    • 如果兄弟结点关键字数>m/2, 则直接移动, 然后更新父结点索引
    • 如果兄弟结点关键字数=m/2, 则合并,然后删除父结点相应索引

B+树的优点

  • 每个结点存储的元素更多, 使得IO次数减少, 更适合作为数据库底层数据结构
  • 所有查询都需要查询到叶子结点, 查询性能更稳定, 而B树在每个结点都有可能查询到结果
  • 所有叶子结点形成一个有序链表, 便于查找.
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值