文章目录
引用
引用
- 引用必须初始化
- 引用只能和一个对象绑定, 初始化的值不能是一个字面量 或者 表达式
- 因为通过引用修改原来的值时, 后面两个无法修改
- 引用有严格的类型要求
- 常量引用
- 必须初始化
- 可以和任何值绑定, 对象, 字面量, 表达式都可
- 因为常量引用无法通过别名改变原来的值
- 没有类型要求
- 引用本质上是一个指针常量, 它的指向不可更改, 所以说引用一旦绑定一个对象后就无法更改了. & = * 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树在每个结点都有可能查询到结果
- 所有叶子结点形成一个有序链表, 便于查找.