C++总结(二)

二叉搜索树(搜索二叉树、二叉排序树)

二叉搜索树又称二叉排序树,它要么是一棵空树,要么是具有以下性质的二叉树:
1.若它的左子树不为空,则左子树上所有节点的值都小于根节点的值

2.若它的右子树不为空,则右子树所有节点的值都大于根节点的值

3.它的左右子树也分别为二叉搜索树

搜索二叉树的查找:

最多查找高度次,效率较高

搜索二叉树具有排序+去重的功能

叫它二叉排序树是因为中序遍历时,其遍历结果就是一个有序的结果。

二叉搜索树的删除

 替换法删除的代码实现

 按照上面的写法同时解决两种情况(即上述和下述情况)是会出错的(指针问题)。

二叉搜索树不支持修改。

二叉搜索树的缺陷:

二叉搜索树的增删查的时间复杂度:O(h),最坏的情况是h = N

改进方案:平衡树:主要是AVL树和红黑树

平衡树和搜索树仅仅只是效率的区别,功能上并没有区别。

二叉搜索树的应用

Key的搜索模型:判断关键字在不在。

1.例如:宿舍门禁

(二叉搜索树还可以顺便排序+去重)

2.检查一篇英文文档中单词拼写是否正确

我们可以把词库中的单词都插入到一棵搜索树中,然后查的时候就查在不在这里面,不在就说明单词有问题

Key/Value模型:通过Key去找value

1.简单的中英翻译程序

2.统计水果出现的次数

具体实现:第二十七节2:40:00到完 只听了一遍

map和set

STL分为序列式容器(vector、list、deque)和关联式容器(map、set)

序列式容器:数据与数据之间没有很强的联系。(各个数据之间没什么关联)。底层为线性序列的数据结构,里面存储的是元素本身。栈和队列是适配器。

关联式容器:数据和数据之间有很强的关联关系(在数据检索时比序列式容器效率更高。底层是红黑树,再底层是一棵搜索树。)

set

set的本质是Key模型。典型的set底层是二叉搜索树

Compare是一个仿函数,用来比较。默认给的是less,相当于operator<,我们可以传别的,来控制这里的比较规则、比较方式。

 key_comp/value_comp获取key/value的比较器。  提供它两是为了保持set与map的一个兼容

set支持增删查,不支持修改,因为Key模型的底层是二叉搜索树,其一旦被修改,整个树就有可能错误了,所以是不允许修改的。

set的拷贝构造赋值都是深拷贝,而且是一棵树,消耗比较大。

lower_bound、upper_bound

删除30-60

itlow = lower_bound(30);//返回大于等于30这个的值的位置//如果给35,则是40到60

itup = upper_bound(60);//返回大于60这个的值的位置,这里返回的是70//如果给55,返回的是60的位置,但是因为是左闭右开,所以60并没有被删掉

//10 20 30 40 50 60 70 80 90

set.erase(itlow, itup);//删除30到70所括住的区间[30, 70)

equal_range

x <= val < y(左闭右开)

map 

map底层存储的结构是pair,pair是一个模板的键值对。map的底层是一棵树

pair的原型大致为

 这是SGI-STL原码中对于键值对的定义

 这里能不能用auto推导?不能,它推不出来

因为其插入时,只看key,val相不相同无所谓,但是map不支持冗余,所以只要key有了,就不支持再插入了。

 其实也不用typedef pair

我们可以用make_pair。前面的都是调用构造。而make_pair是一个函数模板,它的实现方式大致如下,它的优势就是自动推导,不需要我们显式地写模板参数了。make_pair是构造一个匿名pair然后返回

 一般被定义成inline

 有时候有些头文件我们每包含,但是我们仍能使用的原因:

它们被其他头文件包含了。

map的遍历

void test_map1()
{
	map<string, string> dict;
	pair <string, string> kv1("hello","你好");
	dict.insert(kv1);
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("push", "插入"));
	dict.insert(make_pair("pop","删除"));

	//dict.insert(make_pair("hello", "hi"));//优势:自动推导类型,不用显式地写模板参数

	//map的遍历
	map<string, string>::iterator it = dict.begin();
	//auto it = dict.begin();
	while (it != dict.end())
	{
	    cout << it->first << it->second << endl;
		//cout << (*it).first <<(*it).second << endl;//pair不支持流插入,它没有重载流插入。
		//我们用kv键值对
		++it;
	}
	cout << endl;
	for (const auto& kv : dict)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;

}

map和set的迭代器都是双向迭代器,也就是可以正向和反向遍历。

第三十节00:11:00-00:25:00 将双箭头做特殊处理 只看了一遍,没笔记,代码只写了一点

map中的[]

它去容器里找k对应的元素,然后返回对应的value。如果这个key没有被匹配到,它会插入一个新的元素,这个元素就用这个key,然后返回它的映射类型,如果它没有对应的映射类型,value用它的默认构造(default constructor)

即:

1.map中有这个key,返回value的引用(可用于查找,修改value)

2.map中没有这个key,会插入一个新元素,该元素为pair(key, V()); ,返回value(这里的value就是pair里自动创建的value)的引用(可用于插入,修改)    V()是value类型的匿名对象。这个value是一个缺省值,该value如果是int类型,则其缺省值为0,若为string,则其为"",若为指针,则其为nullptr

[]的底层是这样实现的

 map中的insert

上面的value_type实际上就是pair<K, V> val;

返回的不是true和false,而是pair

 insert一个值,如果已经有这个值了,就不插入,bool就是false。如果没有,那就插入,bool是true

再讲解:

返回一个pair,pair的first被设置为迭代器,这个迭代器指向新插入的元素或者是跟这个key相等的元素,second是bool,如果插入成功,返回true。如果插入失败(即有key与它相等),则返回false。

插入时只看key。

1.key已经在map中,返回pair<key_iterator, false>//返回新插入元素的迭代器或与这个值相等的元素的迭代器;如果与key相等的元素已经存在,返回false(即已存在且相等的话,返回false)

2.key不在map中,返回pair(new_key_iterator, true);

第三十节1:06:30-1:15:00模拟实现[]

insert返回pair就是为[]准备的。

multimap为什么没有[]呢?

因为它允许键值冗余了,例如我们要返回苹果,我们应该返回哪次苹果来时的value呢?multi也不能用之前的代码帮我们统计次数了,因为它可以多次插入一样的值。

multimap也是看key,但是不管key有没有出现过,它都是插入,value相不相同无所谓

两个题:第三十节1:38:40-2:58:00

unordered_map、unordered_set

map/set与unordered系列的区别

1.map和set遍历是有序的,该类为无序

2.unordered系列只有单向迭代器,map和set是双向迭代器

数据越多,unordered系列效率更高,但是它的插入稍微差一点,因为它插入时要扩容,扩容的代价有点大

unordered系列的优点:

在处理大量数据时,其增删查改效率更优,尤其是查找。

当我们遇到这种情况时(一个不能被运算的类型需要运算)。

unordered_map里有一个参数帮助我们解决这种情况。hash<Key>

hash<Key>是一个仿函数,

所以要在这里添加一个Hash的仿函数

AVL树(以人名命名,但也叫高度平衡二叉搜索树)

二叉搜索树虽然可以缩短查找效率,但在数据有序或接近有序时,二叉搜索树可能会退化为单支树,其查找元素相当于在顺序表中搜索元素,效率会比较低。如何解决呢?

用AVL树。尝试控制平衡。控制平衡的核心是红黑树

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整,即旋转),即可降低树的高度,从而减少平衡搜索长度。

一棵AVL树要么是空树,要么是具有以下性质的二叉搜索树:

1.它的左右子树都是AVL树(其任意一棵子树都是AVL树)

2.左右子树高度之差(简称平衡因子)的绝对值不超过(-1/0/1)(第2点非必须,因为有些地方用平衡因子,有些地方不用。即可以用平衡因子,也可以不用。我们用平衡因子是为了方便实现)

平衡因子 = 右子树高度 - 左子树高度

为什么AVL树是控制高度平衡,而不是让高度相等呢?

做不到,假设插入两个结点,如何保证两边平等?所以是控制高度平衡,不是让高度相等。

如果一棵二叉搜索树是高度平衡(AVL树是通过控制高度来控制它的平衡的)的,它就是AVL树,如果它有n个结点,其高度可保持在log以2为底的N次方,搜索的时间复杂度为

 为什么AVL树需要三叉链呢?

因为插入一个节点后,需要看平衡因子。

红黑树

红黑树:最长路径不能超过最短路径的2倍(AVL树要求左右高度差不超过1,对于平衡较为严格,红黑树没有那么严格,以此来达成:插入同样的数据,AVL树旋转更多,红黑树旋转更少)

AVL树的增删查改的时间复杂度都是logN。为什么有AVL树还需要红黑树呢?

在实际中,AVL树用的比较少,且红黑树在其基础上做了优化。

一棵红黑树的(可能为)最短路径为:全黑的那条。最长路径为红黑交错的那一条

由AVL树的严格平衡到红黑树的近似平衡。

红黑树达到的效果:相对来说,插入同样的数据,AVL树的旋转较多,红黑树的旋转较少

红黑树的缺点:插入同样多的值时,AVL树最多查找接近logN次,红黑树最多查找接近2*logN次。AVL树的树高为logN,红黑树的最短路径接近logN,最长路径接近2*logN

红黑树的路径是要算到空的,不然有些地方会出问题

红黑树的路径:

路径是要算到空节点的

为什么要加NIL结点(叶节点)呢?到空才算路径完,所以上面有11条路径,算路径上的红/黑结点时,算不算NIL结点都可以

不能两个都是黑的,第二个结点必然是红的,不然会相悖 

其不满足条件4

NIL之所以画成黑色,是一种特殊情况,例如是一棵空树,那么即其为空

为什么满足了上面的性质,红黑树就能保证其最长路径中的节点数不会超过最短路径节点数

的二倍呢?

因为2、3、4这三条规则互斥。若有一棵树,该树极限最短为:全黑

极限最长为:(一黑一红)*n

新插入结点,插入什么颜色?

限制红结点个数:规则3

限制黑结点个数:规则4

所以我们选择插入红色结点,规则三不一定有什么影响,规则四可能会影响所有路径

只要插入,父亲是红的,一定要把父亲变黑。

没说不能有连续的黑

哈希/散列

哈希/散列是基于关联映射的一种查找,本质是让值跟存储位置建立映射的关联关系,

值跟存储位置建立映射关联关系

常用除留余数法,但是这样会使得不同的值映射相同的位置,引起哈希冲突/哈希碰撞

我们有几种方法解决哈希冲突

1.闭散列--开放定址法

2.开散列--拉链法/哈希桶(库里用的是这种,这种更好)

闭散列思路:往后找一找没有被占用的位置。

闭散列的方法:

1.线性探测

2.二次探测

线性探测在这种极端情况下,很坑

 下图:

如果删除3,然后查找20,会发现3这里是空的,会很坑。明明20在,但是我们却找不到。

注意:需要考虑表满了并且找一轮都没找到的情况且没有空位置来结束while循环。
所以哈希表引入了“负载因子”。

负载因子越小,冲突的概率越小。负载因子越大,冲突的概率越大。

注意,当负载因子到了一个基准值就扩容。基准值越大,冲突概率越大,效率越低,但是空间利用率越高。一般限制在0.7-0.8以下

我们可以用二次探测法(不是探测两次的意思)

是按i的平方来探测

开散列-拉链法、哈希桶

哈希桶时间复杂度看平均。

其他:

1.一般寄存器是4byte或8byte

2.栈是向下生长,堆是向上生长

智能指针是解决内存泄漏的恶一个东西,不是指针

CPU只认识二进制的机器码

输入的数据一定在内存里,数据量很大时,不是存在内存中,可能是存在文件或磁盘中

管理:先描述,再组织

delete、virtual一个关键字两种作用

ctrl c是杀掉进程,ctrl z呢。为什么也能关闭进程

emplace涉及到右值引用

string支持单参数的隐式类型转换

第三十节 43:00 int、double、指针的缺省值

哈夫曼树不是一个存储型的数据结构。哈夫曼树是一个特性型的,功能型的数据结构。它的其中一个用途是通过哈夫曼树生成哈夫曼编码,哈夫曼编码可以用来文件压缩。

什么是左值?什么是左值引用?

左值的特点:可以取地址。左值引用就是给左值取别名

以下都是左值

 

什么是右值、右值引用?

 

 右值引用就是给右值取别名

 左值引用可以引用右值吗?第一个不可以,第二个可以

 所以我们平时写的时候这样写,使得x既能接收左值,也能接收右值

 

move是库里面的一个函数 

 

rr1和rr2本质上还是左值,因为右值引用会开空间把右值存起来,这样它就能变成左值了,rr2不能被赋值是因为右const修饰了

 给右值取别名后,就可以取地址了。第39节00:25:00

引用的价值:减少拷贝

 它效率提高的不是很好,有些场景下它无法将效率提高。

我们在使用to_string后,将整型转换成string,这个string是一个局部对象,出了作用域就销毁了,是不能用左值引用返回的。并且在杨辉三角中,左值引用也很难处理这些。(全局变量有线程安全问题)

 C++右值引用一个重要功能就是要解决上面的问题

第三十九节1:02:00实际中函数压栈

 C++中,右值分为1.纯右值-内置类型右值2.将亡值-自定义类型右值

举例:

拷贝构造的代价比移动构造代价大 

可变参数模板

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

列宁格勒的街头

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值