LeetCode知识补充
对还不清楚的知识作补充,每个点中自己熟悉的部分已略过(有的part为了知识完整性即使已知也会补充)
- 部分参考资料
时间复杂度
- 稳定:如果a原本在b前面,且a=b,排序之后a仍然在b的前面
- 不稳定:如果a原本在b的前面,且a=b,排序之后a可能会出现在b的后面
村里有两只做事很稳的动物:插帽龟和统计鸡。插帽龟喜欢去插人家堆起来的帽子,统计鸡喜欢做加减乘除。但有天插帽龟挑选帽子插的时候,恩姓长老看见就慌了,恩老大喊:“快点归还给堆”
O(1)常数阶<O(log2n)对数阶<O(n)线性阶<O(nlog2n)线性对数阶<O(n2)平方阶<O(n3)立方阶<O(2n)指数阶<O(n!)阶乘阶<O(nn)
常对幂指阶
- 顺序执行的代码只会影响常数项,可以忽略
- 只需挑循环中的一个基本操作分析它的执行次数与n的关系即可
- 如果有多层嵌套循环,只需关注最深层循环循环了几次
练习
- 计算该算法的时间复杂度T(n)
void loveYou(int n){//n为问题规模 |
设最深层循环的语句频度(总共循环的次数)为x,则由循环条件可知,循环结束时刚好满足2x>n
x=log2n+1
T(n)=O(x)=O(log2n)
- 计算该算法的时间复杂度T(n)
void loveYou(int flag[], int n){//n为问题规模 |
最好情况:元素n在第一个位置——最好时间复杂度T(n)=O(1)
最坏情况:元素n在最后一个位置——最坏时间复杂度T(n)=O(n)
平均情况:假设元素n在任意一个位置的概率相同为——平均时间复杂度T(n)=O(n)
循环次数
内存管理
以C++为例来介绍一下编程语言的内存管理
- 栈区(Stack):由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈
- 堆区(Heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS收回
- 未初始化数据区(UninitializedData):存放未初始化的全局变量和静态变量
- 初始化数据区(InitializedData):存放已经初始化的全局变量和静态变量
- 程序代码区(Text):存放函数体的二进制代码
计算程序占用多大内存
为什么64位的指针就占用了8个字节,而32位的指针占用4个字节呢?
1个字节占8个比特,那么4个字节就是32个比特,可存放数据的大小为232,也就是4G空间的大小,即:可以寻找4G空间大小的内存地址
安装64位的操作系统的计算机内存都已经超过了4G,也就是指针大小如果还是4个字节的话,就已经不能寻址全部的内存地址,所以64位编译器使用8个字节的指针才能寻找所有的内存地址
264是一个非常巨大的数,对于寻找地址来说已经足够用了
内存对齐
- 平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐
- 硬件原因:经过内存对齐后,CPU访问内存的速度大大提升
struct node{ |
相对来说计算机内存资源一般都是充足的,我们更希望的是提高运行速度
编译器一般都会做内存对齐的优化操作,也就是说当考虑程序真正占用的内存大小的时候,也需要认识到内存对齐的影响
vector
C++标准模板库(STL)中的一个容器类,用于表示一个动态数组。它可以存储多个元素,并且可以根据需要自动调整大小。
vector<int>& nums |
接收一个引用参数,这个参数是一个存储整数的动态数组。在函数内部,可以通过nums这个名字直接访问和操作传入的数组,而不会对数组进行复制
vector<vector<int>> result
二维向量
vector<vector<int>> matrix={ |
|
区别普通数组、std::array、vector
- 普通数组
//数据类型 数组名[数组大小]; |
- std::array
数组大小在编译时确定,不可改变,但它是C++标准库中的一个类模板,提供了更多成员函数和方法
//array<数据类型, 数组大小> 数组名; |
- vector
|
>><<
- >>:右移运算符
a>>b表示将a的二进制表示向右移动b位
a=20;//二进制10100 |
快速除以2的幂次方
右移运算符在处理有符号数和无符号数时有所不同:
- 有符号数:算术右移,对于一个负数,右移后空出的位会被1填充,保持数的符号不变
- 无符号数:逻辑右移,空出的位会被0填充
- <<:左移运算符
a<<b表示将a的二进制表示向左移动b位
a=5;//二进制101 |
快速乘以2的幂次方
delete
用于释放动态分配的内存的关键字,与new操作符相对应,new用于分配内存并构造对象,而delete用于销毁对象并释放其占用的内存
- 释放单个对象
int* ptr=new int(10); |
- 释放数组
int* arr=new int[10]; |
※delete后的指针tmp的变化
使用delete或delete[]释放指针tmp指向的内存后,tmp本身并不会自动变为nullptr。tmp仍然会保留原来的内存地址值,但这个地址所指向的内存已经不再有效。此时,tmp被称为“野指针”
为了避免野指针问题,建议在释放内存后立即将指针设置为nullptr
红黑树
- 定义
满足如下红黑性质的二叉排序树:
- 每个结点或是红色,或是黑色的
- 根节点是黑色的
- 叶结点(虚构的外部结点、NULL结点、失败结点)都是黑色的
- 不存在两个相邻的红结点 (红结点的父节点和孩子结点都是黑色的)
- 对每个结点,从该节点到任一叶结点的简单路径上,所含黑结点的数目相同
左根右,根叶黑,不红红,黑路同
结点的黑高bh:从某结点出发(不含该结点)到达任一空叶结点的路径上黑结点总数
- 性质
- 从根节点到叶结点的最长路径不大于最短路径的2倍
- 有n个内部节点的红黑树高度h≤2log2(n+1)
- 新插入红黑树中的结点初始着色为红色
红黑树查找操作时间复杂度=O(log2n)
- 红黑树的插入
详见BV1b7411N798——7.3.3_2_红黑树的插入
- 红黑树的插入示例
unordered_set
- 构造函数
可以接受一个范围(如两个迭代器),直接将该范围内的元素初始化为集合中的元素
- find function
unordered_set的find方法用于查找指定元素。如果找到了元素,返回指向该元素的迭代器;如果没有找到,返回end()
|
范围for循环
C++11引入了范围for循环,用于简化容器的遍历操作,可以直接遍历容器中的每个元素
for(范围声明:范围表达式){ |
vector<int>v={1,2,3,4}; |
iterator
迭代器是一种检查容器内元素并遍历元素的数据类型,通常用于对C++中各种容器内元素的访问,但不同的容器有不同的迭代器,初学者可以将迭代器理解为指针
begin()&end()
begin()是指向容器第一个元素的迭代器
end()是指向容器最后一个元素的下一个位置的迭代器
迭代器
通用功能
- 比较两个迭代器是否相等(==、!=)
- 前置和后置递增运算(++)
- 读取元素的解引用运算符(*)。只能读元素,也就是解引用只能出现在赋值运算符的右边
- 箭头运算符(->),解引用迭代器,并提取对象的成员
类型
- 输入迭代器(input iterator)
- 输出迭代器(output iterator)
- 前向迭代器(forward iterator)
- 双向迭代器(bidirectional iterator)
- 随机访问迭代器(random-access iterator)
- 输入迭代器
- 通用的四种功能
- 只能利用迭代器进行输入功能
- 它只能用于单遍扫描算法
- 输出迭代器
- 通用的四种功能
- 只能利用迭代器进行输出功能
- 只能用于单遍扫描算法
- 前向迭代器
- 通用的四种功能
- 能利用迭代器进行输入和输出功能
- 能用于多遍扫描算法
- 双向迭代器
- 通用的四种功能
- 能利用迭代器进行输入和输出功能
- 能用于多遍扫描算法
- 前置和后置递减运算(–),意味着它能够双向访问
- 随机访问迭代器
- 通用的四种功能
- 能利用迭代器进行输入和输出功能
- 前置和后置递减运算(–),意味着它是双向移动的
- 比较两个迭代器相对位置的关系运算符(<、<=、>、>=)
- 支持和一个整数值的加减运算(+、+=、-、-=)
- 两个迭代器上的减法运算符(-),得到两个迭代器的距离
- 支持下标运算符(iter[n]),访问距离起始迭代器n个距离的迭代器指向的元素
- 能用于多遍扫描算法。在支持双向移动的基础上,支持前后位置的比较、随机存取、直接移动n个距离
迭代器 | 功能 | 读写及支持 |
---|---|---|
输入迭代器 | 提供对数据的只读访问 | 只读,支持++、==、!= |
输出迭代器 | 提供对数据的只写访问 | 只读,支持++ |
前向迭代器 | 提供读写操作,能向前推进迭代器 | 读写,支持++ |
双向迭代器 | 提供读写操作,能向前和向后操作 | 读写,支持++、– |
随机访问迭代器 | 提供读写操作,能以跳跃的方式访问容器任意数据,是功能最强的迭代器 | 读写,支持++、–、[n]、-n、<、<=、>、>= |
常用容器的迭代器
- vector——随机访问迭代器
- deque——随机访问迭代器
- list——双向迭代器
- set/multiset——双向迭代器
- map/multimap——双向迭代器
- stack——不支持迭代器
- queue——不支持迭代器
实例
双向迭代器和随机访问迭代器最为常用,下面演示这两种迭代器用法
- map
|
对于迭代器来说,虽然都是加1或者减1,但--不等同于-=1,++不等同于+=1,他们实现的是不同的功能
- vector
|
对于vector容器来说,其迭代器有失效的可能
vector容器有动态扩容的功能,每当容器容量不足时,vector就会进行动态扩容,动态扩容不是在原来空间后追加空间,而是寻找一段新的更大的空间,把原来的元素复制过去
但是这样容器存储元素的位置就改变了,原来的迭代器还是指向原来的位置,因此每次进行动态扩容后原来的迭代器就会失效
辅助函数
STL中有用于操作迭代器的三个函数模板,它们是:
-
advance(iter,n)
使迭代器iter向前或向后移动n个元素
-
distance(iter1,iter2)
计算两个迭代器之间的距离,即迭代器iter1经过多少次++操作后和迭代器iter2相等。如果调用时iter1已经指向iter2的后面,则这个函数会陷入死循环
-
iter_swap(iter1,iter2)
用于交换两个迭代器iter1、iter2指向的值
- map
|
在std::map中,每个元素是一个键值对(std::pair<constKey,Value>)。iter_swap函数会交换两个迭代器所指向的元素的内容。然而,std::map的键(Key)是常量,不能被修改。因此,当你尝试交换两个元素时,iter_swap会试图修改键的值,这会导致编译错误
- list
|
continue
continue是一条流程控制语句,用于提前结束当前循环迭代的剩余语句,并直接进入下一次循环的条件判断
- continue不会跳出整个循环,只是跳过当前这次迭代
KMP算法
最长公共前后缀
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串
后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串
前缀表要求的就是相同前后缀的长度
以文本串:aabaabaafa和模式串:aabaaf举例
匹配过程在下标5的地方遇到不匹配,模式串是指向f
然后就找到了下标2,指向b,继续匹配
下标5之前这部分的字符串(aabaa)的最长相等的前缀和后缀字符串是子字符串aa,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么找到与其相同的前缀的后面重新匹配就可以了
前缀表
记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀,next数组就是一个前缀表(prefix table)
前缀表用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配,使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配
- 求得的最长相同前后缀的长度就是对应前缀表的元素
可以看出模式串与前缀表对应位置的数字表示的就是:下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀
- 利用前缀表找到当字符不匹配的时候应该指针应该移动的位置
当字符不匹配的时候看它的前一个字符的前缀表的数值是多少
为什么要前一个字符的前缀表的数值?
要找前面字符串的最长相同的前缀和后缀
next数组
以前缀表统一减一之后的next数组来做演示
- 时间复杂度
n为文本串长度,m为模式串长度,匹配的过程中要根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)
暴力解法时间复杂度O(nm),KMP在字符串匹配中极大地提高了搜索的效率
构造next数组
void getNext(int* next, const string& s) |
- 初始化
- 处理前后缀不相同的情况
- 处理前后缀相同的情况
- 初始化
定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置,对next数组进行初始化赋值
int j=-1; |
next[i]表示i(包括i)之前最长相等的前后缀长度(其实就是j),所以初始化next[0]=j
- 处理前后缀不相同的情况
j初始化为-1,那么i就从1开始,进行s[i]与s[j+1]的比较,所以遍历模式串s的循环下标i要从1开始
for(int i=1;i<s.size();i++){ |
如果s[i]与s[j+1]不相同,也就是遇到前后缀末尾不相同的情况,就要向前回退
next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度
那么s[i]与s[j+1]不相同,就要找j+1前一个元素在next数组里的值(就是next[j])
while(j>=0&&s[i]!=s[j+1]){//前后缀不相同 |
- 处理前后缀相同的情况
如果s[i]与s[j+1]相同,说明找到了相同的前后缀,那么就同时向后移动i和j,同时还要将j(前缀的长度)赋给next[i],因为next[i]要记录相同前后缀的长度
if(s[i]==s[j+1]){//找到相同的前后缀 |
- 完整代码
void getNext(int* next,const string& s){ |
使用next数组来做匹配
定义两个下标,j指向模式串起始位置,i指向文本串起始位置
j初始值为-1,i从0开始遍历文本串
for(int i=0;i<s.size();i++) |
如果s[i]与t[j+1]不相同,j就要从next数组里寻找下一个匹配的位置
while(j>=0&&s[i]!=t[j+1]){ |
如果s[i]与t[j+1]相同,那么i和j同时向后移动
if(s[i]==t[j+1]){ |
如果j指向模式串t的末尾,说明模式串t完全匹配文本串s里的某个子串
在文本串字符串中找出模式串出现的第一个位置(从0开始),所以返回当前在文本串匹配模式串的位置i减去模式串的长度,就是文本串字符串中出现模式串的第一个位置
if(j==(t.size()-1)){ |
- 完整代码
int j=-1;//next数组里记录的起始位置为-1 |
前缀表减一C++代码实现
class Solution { |
- 时间复杂度:O(n+m)
- 空间复杂度:O(m)
前缀表减一C++代码实现详见《代码随想录》
堆
堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值
父亲结点大于等于左右孩子(满足条件1)就是大顶堆,小于等于左右孩子(满足条件2)就是小顶堆
n个关键字序列L[1…n]满足:
-
L(i)≥L(2i)且L(i)≥L(2i+1)或
-
L(i)≤L(2i)且L(i)≤L(2i+1)
- 二叉树中第i个节点的左孩子:2i
- 二叉树中第i个节点的右孩子:2i+1
- 二叉树中第i个节点的父节点:
- i所在层次:
或
若完全二叉树有n个节点,
i是否有左孩子?2i≤n?
i是否有右孩子?2i+1≤n?
i是否是叶子节点?
i是否是分支节点?
构造初始堆
从后往前检查所有分支结点看是否满足堆的要求,若不满足则对以该分支结点为根的子树进行调整。n个节点的完全二叉树,最后一个结点是第
个结点的孩子。对以第
个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对以各结点
为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点
//建立大根堆 |
堆排序思想
堆排序的思路很简单:首先将存放在L[1…n]中的n个元素建成初始堆,因为堆本身的特点(以大顶堆为例),所以堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止
前 K 个高频元素中提到的优先队列也是利用了堆排序的思想
void HeapSort(ElemType A[],int len){ |