hot100的解析
- 哈希
- 两数之和
使用哈希表
存储已遍历的值及其下标,遍历时检查
是否在哈希表中
- 字母异位词分组
- 最长连续序列
使用哈希表
- 双指针
- 移动零
- 盛最多水地容器
- ①直觉上,跨度大的肯定比跨度小的盛水多,比如[1,8,6,2,5,4,8,3,7]这个例子中,1~7肯定比1~8盛的水多
- 因此刚开始维护的区间是1~7,左指针在1,右指针在7
- ②每次选择高度小的指针移
- 如果移动大的指针,移动到一个更大的位置,但是跨度变小了,盛水取决于短板,因此是小跨度*高度小的指针,盛的水肯定没有移动前多
- ③并且移动到大于当前高度的位置
- 如果移动的位置小于当前的位置,跨度小了,短板小了,盛的水肯定没有移动前多
- 三数之和
题目要求:三个数的在数组中的位置不同,且答案不能重复
①答案不能重复,第一个数不能和前面的数相同,第二个数也不能和前面的相同
②最后两个数的求取
固定倒数第二个,然后从后向前遍历倒数第一个且不回溯
双指针指向倒数第一个和倒数第二个,相等退出
四数之和很类似,但是要看清数据范围
- 先排序
- 确定第一个数
- 要找后面两个数的所有组合
- 如果当前遍历的
和前一个值一样,那么就重复了(以当前
开头的三元组都整完了),应该跳过 - 确定第二个数
- 如果当前遍历的
和前一个值一样,那么就重复了(以当前
和
确定的三元组都整完了),应该跳过 - 确定第三个数:第三个数应该从最大值,也就是最右边开始往左边收拢
-
-
-
- 原因:
- ①
如果
,那么
,就不用回溯
指针 -
时,
是一直变大的,如果
了,那么
,就不用回溯
指针
- ①
两种做法,
- 接雨水
- 每次都找到当前值最左边比它大的值,然后计算接水的数量
- 如果没有的话,就反过来,找到它右边最大的值计算接水量
- 滑动窗口
- 无重复字符的最长子串
- 使用哈希表
存储当前字符串中各个字符串的个数 - 每次判断新加的字符是否重复
- 重复的话移动
指针向后移动,直到不重复
- 找到字符串中所有字母异位词
- 难点:如何快速判断两个字符是不是字母异位词
- 对俩字符串都
,然后比较 - 因为只包含
个小写字母,开一个
(或者数组)存储字符串中各字符的数量。如果俩字符串各自的
相等,说明是字母异位词。
- 遍历字符串
,查找有没有和
相同的字母异位词 - 方法一:滑动窗口,存储当前窗口的各个字符的个数
- 方法二:滑动窗口,存储当前窗口和要比较字符串的各个字符的差异
- 子串
- 和为
的子数组
- 方法1:前缀和+哈希表
-
- 将
的值和个数存放进哈希表
以 开头的子串 | | | | | |
以 开头的子串 | | | | | |
以 开头的子串 | | | | | |
| | | | | |
以 开头的子串 | | | | | |
- 横着看:
- 对于以
开头的子串,只需要找
中是否有等于
的值,有的话找到等于
值的个数 - 对于以
开头的子串,字串和都满足
的形式。因此只需要找
中是否有等于
的值,有的话找到等于
值的个数
- 竖着看:
- 添加
-
- 对于以
结尾的子串,只需要找
中是否有等于
的值,有的话找到等于
值的个数 - 对于以
结尾的子串,只需要找
、
中是否有等于
的值,有的话找到等于
值的个数 - 对于以
结尾的子串,只需要找
、
中是否有等于
的值,有的话找到等于
值的个数
- 竖着看比横着看的好处是只需要一个
循环,因为以i结尾的字串,只需要用到
、
的值 - 时间复杂度是
- 方法2:两层循环
- 第一层循环以
开头 - 第二层循环以
结尾 - 超时了
- 收获
- 哈希表
可以像数组一样使用,增删改查都是
- 滑动窗口最大值
单调队列
- 最小覆盖子串
滑动窗口
- 方法一:哈希滑动窗口
- 一个左指针,一个右指针
- 开两个哈希数组,分别存储t中的字符的个数和当前窗口内的字符的个数
- 用have变量来判断当前窗口内是否包含t中所有的字符
- 移动右指针,一直移动到包含所有t中字符
- 然后移动左指针,直到不包含所有t中的字符
- 然后再次移动右指针,一直重复,直到左右指针无法移动
- 方法二:数组滑动窗口
- 一个左指针,一个右指针
- 使用一个数组来存储t中有哪些字符,有几个就是负几,然后遍历的时候也使用该数组
- 只有t中有的字符,遍历且++之后的值才<=0
- char 可以用作数组的索引,其中a~z是97~122 A~Z是65~90
- 然后移动左指针,直到不包含所有t中的字符
- 然后再次移动右指针,一直重复,直到左右指针无法移动
标准ASCII码:0-127,共128个字符;扩展ASCII码:128-255,共128个字符;完整范围:0-255,共256个字符。ASCII码有
- 普通数据
- 最大子数组和
- 方法一:动态规划
- 状态表示:集合f[i]:所有以i结尾的子数组 属性:和的最大值
- 状态计算:状态划分:前一个节点无或有
- f[i]=max(f[i],f[i-1]+nums[i])
- 优化:因为只用到了f[i-1],不需要开数组,只需要开一个变量保存前一个
- 方法二:前缀和,维护一个最小值
- 当前的节点的前缀和减去前面最小的前缀和,就得到了以该节点结尾的子数组的最大和
- 优化:前缀和也可以相同的优化,只需要保存pre和minnum这两个变量。初始时前缀和pre=minnum=0
- 方法三:分治法,线段树
- 类似归并,先划分区间,然后再合并
- 好处是,可以找到任意子区间的最大子序和
- get(nums,0,nums.size()-1) 维护一个区间,用来获得当前区间的最大子序和
- 合并的时候,对每个区间[l,r]都要更新lmax(以l开头的子序的最大值),rmax(以r结尾的子序的最大值),imax(区间和),mmax(区间内子序的最大值)
- 两个区间合并时,imax_zong=imax_left+imax_right;
- lmax_zong=max(lmax_left,imax_left+lmax_right)
- rmax_zomg=max(rmax_right,imax_left+rmax_right)
- mmax_zong=max(mmax_left,mmax_right,lmax_right+rmax_left)
- 三种情况,最大子序和在左半区间mmax_left,在右半区间mmax_right,跨越两个区间
- 如果跨越两个区间,那么左半区间的r和右半区间的l都要在这个序列里,因此左半区间找以r结尾的最大子序和,右半区间找以l开头的最大子序和lmax_right+rmax_left
- 合并区间
略
- 轮转数组
- 方法一:直接轮转到指定位置
- 从位置 0 开始,一直轮转回到0,此时不知道数组中哪些没有轮转到
- 当回到初始位置0时,一共遍历了多少个元素
- 假设遍历了b个元素,因为回到了0,所以有an=bk
- b=an/k,an要足够小(第一次回到0),取n和k的最小公倍数lcm(n,k)
- 总共n个元素,每次遍历b个,总共遍历n/b=n*k/lcm(n,k)=gcd(n,k)次,最大公约数次数
- 定理:lcm(n,k)*gcd(n,k)=n*k
- 从0开始往后遍历,因为0的下一个是从k开始,0~k-1要遍历
- 方法二:翻转
- 先全部翻转,然后局部翻转两个部分
- 细节:k可以比n大的,所以要先求余
- 除自身以外数组的乘积
- 方法一:开两个数组记录从左乘到右和从右乘到左的乘积
- 对于i节点,它的答案就是left[i-1]*right[i+1]
- 空间复杂度O(2n)
- 方法二:用输出数组,输出数组不算空间复杂度
- 计算从右到左的乘积存在res数组中
- 从左到右遍历,维护一个存储乘积和的变量pre,对于i节点,它的答案就是pre*res[i+1]
- 空间复杂度O(1)
- 缺失的第一个正数
假设数组长度为n,最小的正整数只能在[1,n+1]之间,所以数组中的元素不在[1,n]范围内的都不用管
如果数组中1~n都有,那么答案是n+1,否则的话就在中间的数
- 方法一:变体哈希表,做标记
- 把数组中所有为<=0的数都变成n+1,这样的话就可以用负号做标记
- 维护一个最大值u,初始化为n+1,对于不在[1,u)范围内的数,u--(这步可以忽略,一直使用n+1就行)
- 遍历数组,对于在[1,u)范围内的元素nums[i],把它对应位置的元素nums[nums[i]变成负的
- 需要注意,遍历到当前元素时,它的值可能已经变成负的了,所以要用绝对值和u进行比较
- 最后,从0遍历到u-1,如果为正的话,就返回下标
- 否则就是u
本质就是,数组的长度为0~n-1,用下标对应的元素的正负来确定下标的值有没有出现
- 方法二:置换
- 遍历数组
- 如果nums[i]=x,且x在[1,n]之间,就交换nums[i]和nums[x-1]
- 换过来的数,可能还没有遍历到,所以要停在这里(while),再次查看nums[i]
- 但是会遇到死循环的情况,比如置换过来的值和当前的值一样,那就没必要再次置换了
- 比如当前的nums[i]=3,nums[2]的位置已经是3了,就没必要置换了
- 比如当前的nums[i]=3,nums[2]=-1,必须要置换,而且还要再次查看置换后的nums[i]=-1
- 矩阵
- 矩阵置零
要求原地修改,不要开额外的矩阵存储
- 方法一:开两个标记数组分别存储行和列的情况
- 第一次遍历矩阵,某个位置有0,就将其行和列设置为true
- 第二次遍历矩阵,对某个位置,判断其行或列有无true,有的话置0
- 方法二:不开数组,使用两个变量
- 用矩阵的第0行和第0列代替两个标记数组
- 使用两个变量存储第0行和第0列是否有0
- 然后从(1,1)节点开始遍历,如果该行有0的话,把marix[i][0]置为0;如果该列有0的话,把marix[0][j]置为0
- 第二遍遍历,把(1,1)节点后的点都更新
- 最后根据两个变量来更新第0行和第0列
- 方法三:使用一个变量
- 使用一个变量来存储第0行是否有0,第0列的情况由matrix[0][0]存储
- 从(1,0)节点开始遍历
- 疑惑点:如果该列有0,已经把matrix[0][j]置为0了,所以不用担心第0行和第0列没有更新
- 怎么看从哪里开始遍历,根据下面的图
- 更新之后,对于(1,1)以后的元素,只需要看matrix[i][0]和matrix[0][j]有无0
- 对于第0行的元素(0,j),要看它本身(代表的列),还有第0行的情况
- 对于第0列的元素(i,0),要看它本身(代表的行),还有第0列的情况
- 螺旋矩阵
- 方法一:模拟
- 右下左上:注意注意:拐不了顺时针的弯就说明结束了,不需要拐四个弯
- 需要开一个bool类型的数组,看是否遍历过当前的点,比如右下左上一圈之后,可能会回到原点
- 更新之后的点如果遍历过或者超出边界,就转换方向
- 第二个方法:分层遍历 top bottom left right
- 考虑三个特殊情况,单行、单列、1*1
- 左的时候要考虑是不是单行,否则会重复遍历
- 上的时候要考虑是不是单列,否则会重复遍历
- 旋转图像
- 方法一:分层做:每层顺时针移动,右下左上 略
- top bottom left right
- 这里每次都是方形,对于1*1来说,不用转
- 对于2*2来说,转一次
- 对于3*3来说,转两次
- 方法二:每四个坐标一轮交换,用一个temp变量存储中间变量
- (i,j)->(j,n-i-1)
- (j,n-i-1)->(n-i-1,n-j-1)
- (n-i-1,n-j-1)->(n-j-1,i)
- (n-j-1,i)->(i,j)
- 应该遍历哪些部分呢
- 设矩阵的边长为n
- 对于偶数来说是四分之一,n^2/4
- 对于奇数来说,因为中心节点不变化,所以是去除最中心节点之后的四分之一,(n^2-1)/4
- 方法三:翻转代替旋转
- 先水平翻转,然后再主对角线翻转
- 也可以先主对角线翻转,然后再垂直翻转,垂直不太好翻转,因为是两个vector嵌套
- 搜索二维矩阵II
这个题的矩阵中,左上角是最小值,右下角是最大值
右上角和左下角不大不小
右上角的行是小的,列是大的;左下角的列是小的,行是大的
- 方法一:每行进行二分查找
- 时间复杂度O(mlogn)
- 方法二:Z字形查找
- 维护一个矩形,以矩形的右上角为基准,也可以以左下角为基准
- 如果target大于矩形的右上角,那就取小的部分,抛弃右上角所在的那一列
- 如果target小于矩形的右上角,那就去大的部分,抛弃右上角所在的那一行
- 如果等于的话直接返回true
- 时间复杂度O(m+n)
- 链表
环形链表II【基础算法精讲 07】_哔哩哔哩_bilibili
- 快慢指针
- 在无环的链表中,快慢指针能以O(n)的时间复杂度找到链表的中间节点
- 具体步骤:
- 初始时:slow和fast指针都在链表的head,然后slow走一步,fast走两步
- 终止条件:fast==NULL(偶数)||fast->next==NULL(奇数)
- 偶数找的是靠后的那个中间节点
- 在有环的链表中,快慢指针能①判断有无环②环的起始位置
- ①判断有无环
- 有环的话,当slow指针进入环以后,fast指针一定能追上它,因为两指针的相对速度为1,判断二者是否相等就行了
- ①环的起始位置
- 这里的距离指的是边,不是节点,n个相连的节点只有n-1步
- 环长=b+c
- slow指针移动距离=a+b
- fast指针移动距离=a+b+k(b+c)
- fast指针移动的距离是slow指针的两倍
- 2(a+b)=a+b+k(b+c) k>=1
- a-c=k-1(b+c)→a=c+k(b+c)
- 这意味着,一个指针从head开始走完a的距离,到达入口
- slow指针从相遇点出发,走完a的距离,也会到达入口
- 入口就找到了
结论:当快慢指针相遇时,slow指针还没有走完一圈环
考虑最坏情况,如下图,假设slow指针刚进入入口,此时fast指针在他后面一个,那么fast指针要追b+c-1步(距离/相对速度),由于slow指针的是每次1步,所以slow最多走到b+c-1,没到b+c,所以slow指针不会走完一圈
(入口下标是1,它后面的是下标是2,此图中的fast指针指向的位置)
时间复杂度O(a+b+a) 根据结论b<环长,a+b<n --->O(2n)
- 反转链表的代码非常重要,参见第题
- 合并两个有序链表的代码也很重要
subLength <<= 1;将变量 subLength 的二进制位向左移动1位,并将结果重新赋值给 subLength
- 相交链表
- 方法一:哈希存储链表节点!!!
- 时间复杂度O(m+n);空间复杂度O(n)
- 方法二:双指针算法
- 由于两个链表的长度不相等,两个指针分别指向两个链表会发生错位的问题
- 为了使两个链表的长度相等,两指针遍历完指向的链表之后,指向对面的链表,再次遍历(就是为了两个链表向右对齐)
假设链表A在相交之前的长度为a,链表B则为b,相交之后的长度为c 那么p指针走到a+b+c步时,会和q指针走到a+b+c步时会合。
- 反转链表
- 方法一:递归,输入是节点a
- 递归找到最后一个节点,然后往前返回,用a->next->next=a;a->next=NULL,把后面节点的next指针指向自己,反转
- 结束条件:a==NULL(传进来为空)||a->next==NULL(链表的最后一个节点)
- 方法二:迭代
- 遍历链表,维护一个pre指针,初始时为NULL
- 涉及三个指针
- 回文链表
- 方法一:将链表的val复制到数组用双指针算法(logn)
- 时间复杂度O(n) 空间复杂度O(n)
- 方法二:递归
回文的比较需要知道边界l和r,从外向内比较,需要知道最后的节点,所以进入递归函数之后一直往后走
- 维护一个前指针,初始时为head
- 进入递归函数往后走,一直走到最后,再往前返回
- 比较当前节点和pre,相等的话pre=pre->next,返回true,否则返回false
- 环形链表
- 方法一:哈希表存储每个节点,遍历
- 方法二:快慢指针
- 初始时,快慢指针都处于head处,快指针每次走2步,慢指针每次走1步,相对速度为1
- 如果有环的话,慢指针进入环之后,快指针一定能追上
- 所以判断快指针==慢指针就行了
- 环形链表II
- 方法一:快慢指针
- 判断有无环,类似上一题
- 初始时,快慢指针都处于head处,快指针每次走2步,慢指针每次走1步,相对速度为1
- 如果有环的话,慢指针进入环之后,快指针一定能追上
- 如何判断环的位置
- 假设慢指针和快指针在环的第b个位置相遇了
- 此时快指针走的距离为a+b+k(b+c),慢指针走的距离为a+b
- 2(a+b)=a+b+k(b+c)->a=c+k(b+c)
- 此时慢指针走a步,会到达环形入口,另一个指针从head开始走a步会在环形入口相遇
- 合并两个有序链表
- 方法一:迭代
- 设置哑节点,其next指针用来存储合并后新链表的头节点,维护一个pre节点,作为已合并节点的最后一个节点,初始时pre=哑节点
- 当两个链表都不为空时,每次选择两个链表中较小的那个值
- 如果有一个为空,pre直接连接不为空的那个就行
- 方法二:递归
- 输入两个链表头节点,每次返回val值较小的节点,它的next等于剩下节点再次求较小值返回的节点
- 两数相加
- 类似高精度加法
- 删除链表倒数第N个数
- 方法一:只遍历一遍,用map记录
- unordered_map<int,ListNode*>
- 方法二:遍历两遍
- 第一遍记录长度
- 第二遍找到删除元素的前一个位置
- 使用哑节点,哑节点的下标为0,把对头节点的操作统一化
- 方法三:栈
- 全存进去,弹出来N个就行了
- 两两交换链表中的节点
条件:不能修改节点内部的值
- 方法一:递归,传入一个节点
- 递归函数返回两个一组交换后开头的那个点
- 如果当前节点node为空,或node->next为空,不需要交换,直接返回
- 如果都不为空,记录nt=node->next(也是递归返回的节点),node的next变成下一组俩元素返回的开头的节点
- 方法二:迭代
- 保存当前节点的node->next->next,维护一个pre指针,它指向前一组两元素的结尾的节点
- K个一组反转链表
- 利用反转链表的函数,但是传进去的链表长度为k且第k个节点(截断点)的next=NULL,返回的反转后的头节点
- 主函数(也是递归)里判断是否有k个,有的话送进反转链表的函数,它返回的是反转后的头节点
- 然后截断点的next是下一组k个节点反转后的开头的节点
- 随机链表的复制
- 暴力法:O(n^2):
- 先建好链表,不存random指针
- 然后依次存每个节点的random指针
- 方法一:把复制的节点和原节点一一对应的存在哈希表内,遍历两遍
- 时间复杂度O(n) 空间复杂度O(n)
- 方法二:递归+哈希表
- 遍历到某个节点时,它的random指针可能还没创建,所以递归创建节点,创建完节点后立马存进哈希表中,避免循环
- 时间复杂度O(n) ~2n空间复杂度O(n)
- 方法三:把新建的节点连在原本节点的后面,找random的时候,只需要找原本节点的random的后一个节点即可:例如A->A’->B->B’->C->C’
- 第一次遍历生成新节点,并连接在原本节点的后面
- 第二次遍历,根据原本节点的random,保存当前节点的random
- 如果原本节点的random指针不为NULL,那么当前节点的random就是原本节点的random指针的后一个
- 如果原本节点的random指针为NULL的话,当前节点的random就是NULL
- 第三遍遍历,还原原始链表,生成新链表
- 空间复杂度O(1)
- 排序链表
- 尝试冒泡,时间复杂度O(n^2)~~~TLE
- 归并,时间复杂度O(nlogn)
- 要是用前面的合并两个有序链表
- 使用快慢指针找到分的区间,然后合并区间(这里的快慢指针初始指向哑节点)
- 归并又分为(自顶向下--》递归)和(自底向上--》迭代)略
- 合并K个升序链表
- 方法一:递归
- 和合并两个链表保持一致。函数返回当前所有链表的最小值
- 每次找到最小的点,它的next就是剩余链表的合并的返回值
- 方法二:利用合并两个升序链表的函数,一类方法
- ①顺序法:初始化一个空链表ans,每次把lists中的链表和它合并
- merge(ans,list[1~n])
- ②分治法,每次合并俩
- 类似归并,维护一个区间[l=0,r=lists.size()-1] ->[l,mid]、[mid+1,r]
- 方法三:优先队列,需要重写比较函数
- 使用优先队列找所有链表中的最小值,但是重写比较函数
- 把所有链表没有合并的第一个节点入栈
- 优先队列的符号,大于就是小根堆
- LRU缓存
- 双链表+哈希
- 写一些通用函数,良好的代码风格
- 尾插
- 删除
- 哈希表的元素要删除
- 二叉树
- 二叉树的中序遍历
- 前序遍历:根左右
- 中序遍历:左根右
- 后序遍历:左右根
如果是结构体的指针,访问元素用->
如果是结构体的实例,访问元素用.
- 二叉树的最大深度
递归遍历每个二叉树的节点,返回左右子树的最大高度加上1(当前节点的高度)
- 翻转二叉树
递归遍历每个二叉树的节点,交换其左右节点的值
- 对称二叉树
- 递归遍历两个节点node1和node2,初始时node1=root,node2=root
- 首先比较这两个节点的值是否相等,
- 然后递归比较
- node1节点的左孩子和node2节点的右孩子
- node1节点的右孩子和node2节点的左孩子
- 返回值是上面三个比较有一个为false,就返回fasle
- 所有比较过程中,如果出现不相等直接返回false
- 二叉树的直径
- 这里最长的直径不一定是包含root的路径,所以要遍历所有的节点
- 计算每个节点的直径=左树的高度+右树的高度(不用加1,节点的个数-1才是边数)
- 可以定义一个保存最大直径的变量,计算出每个节点的直径后比较
- 递归遍历每个二叉树的节点计算树的高度
类似第13题:二叉树的最大深度
好好读题,理解直径的含义
- 二叉树的层次遍历
- Bfs:层次遍历,使用队列存储
- 难点:返回的是vector<vector<int>>,每层存储vector<int>中。队列中的的元素不知道属于哪一层
- 解决办法:
- 每次存储当前层的个数len,然后pop出来len个节点之后,再次计算个数,再次pop
- 因为二叉树的第一层入栈后,个数为1,pop出去之后,计算第二层的长度(que中的元素个数)
- 将有序数组转换为二叉搜索树
- 二叉搜索树的性质
- 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
- 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
- 任意节点的左、右子树也分别为二叉搜索树
- 二叉搜索树中没有键值相等的节点
- 二叉搜索树的中序遍历是升序序列
- 平衡二叉树是左右子树的高度差不超过1的树
- 题解:每次选择中间数字作为二叉搜索树的根节点,这样分给左右子树的数字个数相同或只相差 1,可以使得树保持平衡
- 中间可以是(l+r)/2
- 也可以是(l+r+1)/2
- 也可以随便选
- 只不过生成的树不同
- TreeNode* a=new TreeNode(nums[mid]);
- 验证二叉搜索树
- 利用“二叉搜索树的中序遍历是升序”的性质
- 中序遍历二叉搜索树:
- 遍历到当前点,它一定比前面的点(左孩子)大,所以只需要存储前面的点就可以了(边界问题,第一个点比较的时候一定要比初始化的大)
- 也可以存储中序遍历的数组
- 二叉搜索树中的第k小元素
- 利用“二叉搜索树的中序遍历是升序”的性质
- 中序遍历二叉搜索树:
- 记录当前遍历节点的个数,如果为k就返回该节点的值
- 二叉树中的右视图
- bfs进行层次遍历,每层最后一个元素
- 注意递归的条件,bfs如果传进来的节点是NULL 也需要判断的
- 类似二叉树的层次遍历那道题
- queue可以访问front和back
- 二叉树展开为链表
- 方法一:存储前序遍历二叉树的每个节点,然后遍历存储的节点,改变它的左右孩子 空间复杂度O(n)
- 方法二:为了防止连接左节点时把右节点丢了,所以先连左孩子上,后面再改
- 前序遍历访问各节点的顺序是根节点、左子树、右子树。
- 如果一个节点的左子节点为空,则该节点不需要进行展开操作
- 如果一个节点的左子节点不为空,则该节点的左子树中的最后一个节点被访问之后,该节点的右子节点被访问。
- 也就是说左子树最后一个节点是右子树根节点的前驱节点
- 所以先序遍历二叉树,对以每个节点为根的子树的返回它最后一个访问的节点
- 对于某个位置,先dfs左边的,返回左边的最后一个指针,用来连接右边的节点,返回dfs右边的指针
- 方法三:直接连接在右边,无需二次遍历
- 每次前序遍历时,都要先存储当前节点的左右孩子,然后再改变当前节点的左右指针
- 为了防止连接左节点时把右节点丢了,这里把左右节点都先存储下来
- 方法四:反后序遍历
- 略
- 二叉树中的右视图从前序与中序遍历序列构造二叉树
- 前提条件:preorder和inorder均无重复,即每个节点的值是独一无二的
- 前序遍历 [根][左子树][右子树]
- 中序遍历 [左子树][根][右子树]
- 步骤:
- 前序遍历数组中的第一个元素就是根,通过在中序数组中遍历找到根,可以知道左子树的长度,进而就知道右子树的长度
- 左子树和右子树也满足上面的中序前序性质,递归的做就可以了
- 可以用哈希表来存储中序遍历每个节点的位置,节省遍历中序数组的时间
- 路径总和III
- 方法一:搜索所有路径,记录从每个节点出发所有路径的值(用vector存储并返回,需要注意路径和会大于INT_MAX)
从下到上
- 获取左右孩子的所有路径值,加上当前节点的val,判断是否为target,是的话加1,
- 还要判断当前节点的val是否为target
- 方法二:搜索从每个节点i出发路径和为targetSum的个数,
- 从左孩子出发路径和为targetSum-i.val的个数+从左孩子出发路径和为targetSum-i.val的个数+(i.val=?targetSum)
- 方法三:前缀和
从上到下
- 记录根节点到当前节点路径的前缀和curr,然后在已保存的前缀和中查找有没有等于curr-targetSum,存在的话说明从前缀和=curr-targetSum对应节点的后一个节点出发到当前节点的路径和为targetSum
- 前缀和使用哈希表存储,刚开始要初始化mp[0]=1,代表从根节点到当前节点的路径
- 因为路径是很多条,所以前缀和的值记得要回溯
- 二叉树的最近公共祖先
- 设f_x表示以当前节点为根的子树中是否包含p或q节点
- p和q的最近公共祖先只有两种情况:
- ①公共祖先是p(q),且q(p)位于其子树内;(x==p||x==q)&&(f_l&&f_r)
- ②p、q分别位于公共祖先的左子树和右子树内。(f_l&&f_r)
- 这样求的满足条件的父节点唯一,因为如果已经找到了x节点是公共祖先,那么f_x=true,x的父节点就只有它这一个分支为true的情况,不能为true了
- 二叉树的最大路径和
- 遍历二叉树,对每个节点i,计算所有包含i节点的路径的最大值
- 最大值=(i.val,i.val+l_max,i.val+r_max,i.val+l_max+r_max)
- 但是返回的是当前节点+一个孩子的最大路径
- 类似二叉树的直径,但是添加了部分限制
- 图论
- 岛屿数量
三种方法
第一种:dfs,搜索的次数就是答案
第二种:bfs,搜到陆地之后就置0,防止再次被搜到
第三种:并查集
- 腐烂的橘子
Bfs求最短路径,边的长度都一样
但是要注意条件,没有好橘子和没有坏橘子的优先级
- 课程表
拓扑序列,从入度为0的点找
- 实现Trie树
有一些C++的语言内容需要留意,
Malloc 、指针->、free
还有节点的数量60000
- 回溯
回溯的时机,最好画图
分为①走或不走,②走哪些
- 全排列
错误的地方,从第0层就进dfs,找到之后就进下1层。
第0层是排列开头的数字
- 子集
这里不是全排列的问题,而是走不走的问题了
因为[1,2]和[2,1]算一个结果
- 电话号码的字母组合
全排列问题,每层取得值不同,所以不需要str数组判断状态
- 组合总和
要组合不是个数,只能是dfs了,两种做法:
第一种,每次选不选第i数,不选走下一个数i+1(否则会死循环),选的话再走第i个数
第二种,每次只能从第i到第n中选择数字,保证选出来的列表,前面的数字的小标都小于等于后面数字的小标,这样可以避免重复
- 括号生成
走或不走,但是括号匹配有个原则,右括号不能比左括号多
- 单词搜索
dfs搜索 这里回溯的条件很厉害
- 分割回文串
有一个额外的函数,判断要加入的字符串是不是回文
每次判断剩余的字符串,所有长度的情况
- N皇后问题
两种方法
一行一行:列、正对角线、副对角线
一步一步:行、列、正对角线、副对角线
- 二分查找
如果是查找边界就分两个区间,如果是查某个值就分三个区间
- 搜索插入位置
二分法
时间复杂度:O(logn) 每次都分一半
边界判断 ①[l,mid] [mid+1,r] mid=(l+r)/2;
②[l,mid-1] [mid,r] mid=(l+r+1)/2;
每次选择有答案的区间,当区间的长度为1时,就是答案
如果答案不在区间内,求得是<=,所以答案l+1,
还要考虑一种情况,比数组中的所有数小,此时l=0,那么答案就是0,不用加1了
- 搜索二维矩阵
和一维一样,下标展开成一维就行了
- 在排序数组中查找元素的第一个和最后一个位置
>=x和<=x各做一次
考虑数组为空的时候,直接返回-1 -1
- 搜索旋转排序数组
不是全部有序,而是一半一半有序
- ①找到分界点,两边升序的子序列分别做二分
- ②二分:
- 找到mid,判断左右两边的序列哪个是有序,哪个是无序的
根据nums[mid]是否大于nums[l]判断左右区间
- 大于的话[l,mid-1]是有序,[mid+1,r]是无序;
- 小于的话[l,mid-1]是无序,[mid+1,r]是有序;
- 俩区间不包含mid,所以要判断nums[mid]==target,不在的话
- 看target是否在升序里面,在的话,进去升序子序列,不在的话,进去混乱子序列
(每次选择答案所在的区间,这里有点像三个区间了,mid,mid左区间,mid右区间)
原理:对于任意一个index,其左区间和右区间至少有一个是有序的,那么就可以根据这个区间的最大值和最小值来判断Target是否在该区间内,由此就可以确定新的查找区间为有序半区还是无序半区
- 旋转排序数组中的最小值
还是一半有序一半无序
- 步骤
- 找到mid,左右子区间一个为有序,一个为无序
- 能找到有序区间的最小值,然后进入无序里再次寻找最小值
- 边界判断,当长度为1时,直接返回
- 寻找两个正序数组的中位数
两个正序数组的下标从0开始,寻找中位数
- 中位数
- 如果总数是偶数(比如6),找数组中的第totoal/2、(total/2)+1小的数
- 如果总数是奇数(比如5),找数组中的第(total+1)/2小的数 或(total/2)+1
- 可以把上面的中位数问题等价于寻找两个有序数组中第k小的数(k从1开始)
- 寻找两个有序数组中第k小的数
- 找两个数组中的前k/2个数据,[0,(k/2)-1](两个数组的长度都要大于等于k/2,小于用[0:]) 不一定从0开始,举例子
- 如果nums2[(k/2)-1]<=nums1[(k/2)-1],那么nums2数组的[0,(k/2)-1]可以被丢弃了,不可能是第k小
- 原因:计算一下nums2[(k/2)-1]最多是第几小,nums2[(k/2)-1]<=nums[k/2]且nums2[(k/2)-1]<=nums1[(k/2)-1],所以确定比nums2[(k/2)-1]小的是nums2[0,(k/2)-2],总共(k/2)-1个,假设nums1[0,(k/2)-2]也比nums2[(k/2)-1]小,那么就又有(k/2)-1个,所以最多是((k/2)-1)*2+1=k-1大
- 这样的话,每次都可以抛弃k/2个数据,k=k-(k/2),如果长度小于k/2,减去对应值就可以了
- 考虑边界情况
- 当k=1时,比较两个数组当前开头数字的大小,返回较小值
- 当其中一个数组空了时,直接返回另一个数组的第k个元素即可步骤
- 实现时还要保证第一个数组的长度小于第二个
- 栈
- 最大元素有效的括号
良好的代码风格
用栈存储左括号,三种
每次碰到右括号,就出栈左括号比对
- 最小栈
第一种解法:可以删除任意值的小根堆
n是栈的size,hton[] 堆内的下标对应栈的哪个位置
size是堆中的元素个数,ntoh 栈内的下标对应堆中的哪个位置
用这个
第二种解法:辅助栈
一个栈用来存储元素
一个栈用来存储与每个元素对应的最小值。
两个栈同删同存,大小一样
- 字符串解码
双栈法
一个栈存储数字
一个栈存储字符串和‘[’
碰到数字,把它所有位都加出来,入数字栈
碰到字母,把所有连在一起的字母串起来,如入字符串栈
碰到‘[’,直接入字符串栈,这里不能是字符的形式,必须是string的形式
碰到‘]’,①弹出字符串栈的字符,直到碰到‘[’;②弹出数字栈中的数字k,把前面的字符串重复k次,入字符串栈
遍历完s后,拼接字符串栈中的字符,返回即可
拼接字符串技巧,两个一拼,先出来的是后面的,后出来的是前面的,拼完入字符串栈
这道题和acwing中的表达式求值很类似
- 每日温度
和acwing找左侧第一个比自己小的数很类似,这里是找右侧第一个大于自己的数
第一种方法,类似kmp中的ne数组
①维护一个数组,这个数组存储右侧第一个大于自己的数
②从大到小遍历原数组,如果第i个值小于它右边的数,直接存储i+1
③否则的话就找i+1这个值它的右侧第一个大于自己的数,即res[i+1]
④比较第i个值和第res[i+1]的值,以res[i+1]=0或找到大于它的数为止
这里是省略了第i个值后面小于第i+1的值的判断
第二种方法:单调栈
从大到小遍历,如果当前的数大于栈顶的元素就存储,否则话把小于等于当前数的栈内元素全部弹出来
入栈之后,栈的前一个位置就是右侧第一个大于它的数
- 柱状图中最大的矩形
暴力做法:
①枚举宽度(区域),两重循环(左边界和右边界),高度就是区域内柱子的最小值(想到了)
②枚举高度,从当前位置出发,找到左面第一个小于当前高度的位置,找到右面第一个小于当前高度的位置
解释:对数组中的每个元素,若假定以它为高,能够展开的宽度越宽,那么以它为高的矩形面积就越大
都是O(n^2)
优化
//优化
通过单调栈找到每个位置,左边第一个小于该位置高度的位置,右边第一个小于该位置的高度
遍历两次找到(单调栈或next数组)
//优化
遍历一次找到,
入栈找到左侧,出栈找到右侧,虽然不是恒小于,但是等于的情况下,这个值没有找对右边界,但是和它等值且位于最右侧的那个位置准确的找到了左边界和右边界,连续等值的边界是一样的,有一个找对就行
总结:最值的时候,找一个表示(例如动态规划中的状态表示)能够代表一类情况,求出这类情况下要求的属性,比如这里的枚举高度,枚举区域
- 堆
- 数组中的第k个最大元素
快排 O(n) n+n/2+n/4+...< 2n
堆 O(n+klogn)
时间复杂度要求O(n)
- 前k个高频元素
和第12题非常类似
- 首先计算每个元素的频率,也就是个数
没给出元素的范围 所以使用哈希表来存储每个元素的个数
- 堆
①直接排序,维护一个大根堆,找到前k个
②维护一个小根堆,堆中的元素只有k个;
如果堆中的元素个数小于k,直接入堆
如果堆中的元素大于等于k,比较该元素和栈顶元素的大小
如果小于栈顶元素,肯定不可能是k的最大的
如果大于栈顶元素,弹出栈顶,入堆
O(n+)
- 快排
快排分成两个数组,前一个数组比较大,后一个数组小
1)前一个数组中元素的个数小于k的话,全部都是答案,只需要从后一 个数组中找k-j+l-1个
2)前一个数组中元素的个数大于k的话,答案只在前半部分
3)等于k直接返回
- 数据流的中位数
- 使用一个大根堆和一个小根堆,
- 大根堆存储较小的值,堆顶元素是较小值中的最大值,接近中心
- 小根堆存储较大的值,堆顶元素就是较大值的最小值,也接近中心
即让小根堆中的所有值都大于等于大根堆中的值
- 每次添加数都和小根堆的栈顶元素比较,小于的话进入大根堆,大于的话进入小根堆
- 还要维护大根堆和小根堆的元素个数,偶数时,二者相等,奇数时,小根堆多一个
- 如果大根堆的size大于小根堆,就把大根堆的堆顶元素入小根堆
- 如果小根堆的size大于大根堆的size+1,就把小根堆的栈顶元素入大根堆
- 贪心
- 买卖股票的最佳时机
遍历数组,维护一个最小值即可;
例如,如果遍历到第i个位置,那么最小值是0~i之间的最小值
res=min(a[i]-min,res)
- 跳跃游戏
遍历数组,维护最远可以到达的位置:对于位置i,它能到达的最远位置为i+nums[i]
在最远到达的位置之内,如果>=最后一个位置,就立刻返回true
- 跳跃游戏II
题目暗含了一定能跳到n-1
- 反向查找
用tail记录当前要跳到的点,刚开始tail=n-1
然后从tail处往前反向查找,遍历0~tail-1中能跳到tail的位置,「贪心」地选择距离tail最远的那个位置t(从前往后遍历,第一个满足的就是),然后让tail=t,再次遍历,直到tail=0结束;
时间复杂度O(n^2)
- 正向查找
优化:如果当前位置的跳跃到达不了n-1,就跳跃到它能到达的位置里(跳跃能到达最远的那个位置)
从前往后遍历0~n-2,如果到达前面位置能跳跃到的最大位置,step++
一次遍历,时间复杂度O(n)
- 划分字母区间
同一个字母最多出现在一个片段中,而且要划分尽可能多的片段
记录每个字母出现的最后位置
从前往后遍历字符串,维护一个划分片段结束的最大位置,最大位置一直被片段内的所有字符更新,取最大位置
如果遍历到结束的最大位置,就划分一个片段
其实就是利用贪心的思想寻找每个片段可能的最小结束下标
- 动态规划
- 爬楼梯
- 动态规划:
- 状态表示f[i]:①集合:从0走到第n个台阶的所有方案
②属性:所有方案的个数
- 状态划分:①集合划分:按照所有方案中最后一个步骤划分,最后一个步骤是上1阶楼梯,最后一个步骤是上2阶楼梯
②状态转移公式: f[n]=f[n-1]+f[n-2](n>=2的时候才有f[n-2])
- 初始化:f[0]=1;
- 记忆化搜索:
- dfs(n)
- 杨辉三角
题目不难,但是vector不太熟悉,如果想使用res[i],就要实现初始化一下
- 动态规划:
- 状态表示f[i][j]:②状态转移公式: f[i][j]=f[i-1][j]+f[i-1][j-1]
- 初始化:f[0][0]=1;
- 打家劫舍
- 动态规划:
- 状态表示:①集合f[i][0]:从第1家偷到第i家,且第i家没偷的所有方案
f[i][1]:从第1家偷到第i家,且第i家偷了的所有方案
②属性:所有方案价值的最大值
- 状态转移公式:u①集合划分:根据不能偷相邻的人家且上一家偷没偷划分: f[i][0]:上一家可以偷也可以不偷; f[i][1] :上一家不能偷
②状态转移公式:f[i][0] = max(f[i-1][1],f[i-1][0])
f[i][1] =f[i-1][0]+w[i];
- 初始化:f[0][0]=0;f[0][1]=0;
- 改进:因为求第i个屋状态的时候只用到了第i-1个屋,所以使用滚动数组来做
- 完全平方数
- 动态规划:完全背包问题
- 状态表示:①集合f[i][j]:表示只能从1到i的平方和中选,且和恰好为n的所有方案
②属性:所有方案的数字个数最少
- 状态转移公式:u①集合划分:根据平方和中有几个i的平方划分,0个、1个、2个...
②状态转移公式: f[i][j]=min(f[i-1][j-k*i*i]) k满足j-k*i*i>=0;
- 初始化:全部初始化为无穷大,因为可能不存在恰好为n的方案,不存在用无穷大表示,另外f[i][0]=0;f[0][i]=+∞
- 优化: f[i][j]=min(f[i-1][j],f[i-1][j-i*i],f[i-1][j-2*i*i],...)
f[i][j-i*i]=min( f[i-1][j-i*i],f[i-1][j-2i*i])
因此 f[i][j]=min(f[i-1][j],f[i][j-i*i]);
- 改进:第i个状态的更新只用到了第i-1个状态,所以可以使用滚动数组 更新j时需要用到更新过的j-i*i<j和没更新过的j 所有从小到大遍历可以满足
- 零钱兑换
和上一题一样
- 动态规划:完全背包问题
- 状态表示:①集合f[i][j]:只能从前i种金额的硬币中选,且总和为j的所有方案
②属性:所有方案的硬币个数的最小值
- 状态转移公式:u①集合划分:根据方案种有几个第i类金额的硬币划分,有0个有1个有2个,...
②状态转移公式: f[i][j]=min(f[i-1][j-k*coins[i]]);k满足j-k*coins[i]>=0
- 初始化:全部初始化为无穷大,因为可能不存在恰好为n的方案,不存在用无穷大表示,另外f[i][0]=0;f[0][i]=+∞
- 优化: f[i][j]=min(f[i-1][j],f[i-1][j-i*i],f[i-1][j-2*i*i],...)
f[i][j-i*i]=min( f[i-1][j-i*i],f[i-1][j-2i*i])
因此 f[i][j]=min(f[i-1][j],f[i][j-i*i]);
- 改进:第i个状态的更新只用到了第i-1个状态,所以可以使用滚动数组 更新j时需要用到更新过的j-i*i<j和没更新过的j 所有从小到大遍历可以满足
- 单词拆分
String类型获得子串,string类型比较
- 动态规划:
- 状态表示:①集合f[i]:表示字符串s中1~i之前的子串是否可以用字典中的单词表示
②属性:是否可以 true or false
- 状态转移公式:u①集合划分:根据1~i字串中最后一个单词是谁划分,是字典里的第2个单词,是字典里的第2个单词,...,是字典里的第k个单词
②状态转移公式:f[i]=f[i-w.size()]&&substr(i-a.size(),a.size())
- 初始化:全部初始化为fasle; f[0]=true;
- 略:另一种做法,哈希表,判断f[1~i-1]是否为true,是的话判断s[i-j,i]之间有没有字典中的单词,有的话,1~i就可以用字典里的单词表示
- 最长递增子序列
- 动态规划:
- 状态表示:①集合f[i]:只能从前i个数字中选,且以i结尾的所有递增子序列
②属性:所有递增子序列长度的最大值
- 状态转移公式:u①集合划分:子序列的前一个数字谁,第1个数字,第2个数字,...第i-1个数字;但是要满足a[k]<a[i]
②状态转移公式:f[i]=max(f[1~i-1]+1);
- 初始化:
- 优化:维护一个所有长度的递增子序列结尾最小值的数组b;对于每一个a[i],使用二分法找到小于它最大的值b[j],把b[j+1]=a[i]; b数组的长度就是答案
- 乘积最大子数组
子数组:数组中连续几个数,和前面的子序列不一样,子序列保持顺序但是可以不连续;既然必须连续,那就好办了,只需要判断它前一个能不能连起来就行
- 动态规划:
- 状态表示:①集合f[i]:在前i个数字组成数组中,以i结尾的所有子数组
②属性:所有子数组乘积的最大值
- 状态转移公式:u①集合划分:子数组中的前一个值是谁,是第i-1个值,没有值②状态转移公式:由于要求的是最大值,且数组中的值有正有负,如果第i个值是负数,求最大值的话,应该乘以i-1结尾的子数组的最小值(而不是最大值了);维护minf[i]和maxf[i],分别存储以i结尾的所有子数组乘积的最大值和最小值minf[i]=min(a[i],minf[i-1]*a[i],max[i-1]*a[i]);
maxf[i]=max(a[i],minf[i-1]*a[i],max[i-1]*a[i]);
- 初始化:
- 分割等和子集
- 动态规划:
- 状态表示:①集合f[i][j]:只从前i个数字中选,其和为j的组合
②属性:组合是否存在 false or true
- 状态转移公式:①集合划分:组合中包含第i个数字的个数:包含1个;包含0个
②状态转移公式:f[i][j]= max( f[i-1][j] , f[i-1][j-nums[i]] )
- 初始化:f[0~n][0]=true
- 优化,因此第i行的值只需要用到第i-1行的值,所以可以使用滚动数组;要更新j时,要用到未更新的j和j-nums[i],所以从大到小遍历j可以满足
- 有效括号
有效括号子串是形如( (()) )的形式,开头必须是'(',结尾必须是')',从右往左遍历时,左括号的数量不能大于右括号的数量'(()',否则的话,右边就没有多余的右括弧和它匹配,就不合法了
- 动态规划:
- 状态表示:①集合f[i]:以第i个字符结尾的所有有效括号子串(满足条件s[i]必须为')')
②属性:所有有效括号子串长度的最大值
- 状态转移公式:①集合划分:从第i个字符遍历到第0个字符,且左括号数要一直小于右括号数,如果左括号数等于右括号数,且当前遍历的字符是左括号时,更新f[i]的值
②状态转移公式:f[i]=max(f[i],i-j+1);
- 初始化:
- 不同路径
- 动态规划:
- 状态表示:①集合f[i][j]:从(1,1)走到(i,j)的所有路径
②属性:所有路径的个数
- 状态转移公式:①集合划分:以路径上的上一个节点划分,上一个节点来自上面,上一个节点来自左面
②状态转移公式: f[i][j]=f[i-1][j]+f[i][j-1] 需满足(i-1,j)和(i,j-1)在表格内
- 初始化:f[1][1]=1 :起点有一条路径
- 最小路径和
- 动态规划:
- 状态表示:①集合f[i][j]:从(0,0)点走到(i,j)点的所有路径
②属性:所有路径上数字之和的最小值
- 状态划分:①集合划分:按照路径上的上一个值划分,上一个值来自上面,上一个值来自左面
②状态转移公式: f[i][j]=min(f[i-1][j],f[i][j-1])+w[i][j];需满足(i-1,j)和(i,j-1)在表格内
- 初始化:f[1][1]=1 :起点有一条路径
- 最长回文串
对于一个子串而言,如果它是回文串,并且长度大于 2,那么将它首尾的两个字母去除之后,它仍然是个回文串
区间DP:不是顺序求取,而是按照区间的长度从小到大求取
- 动态规划:区间DP
- 状态表示:①集合f[i][j]:从i到j的子串
②属性:是否是回文
- 状态划分:①集合划分:
②状态转移公式:当[i,j]区间为1时,没有条件就是回文串;若区间长度为2,就只有一个条件f[i][j]=(s[i]==s[j]);若区间长度大于2时,有两个条件 f[i][j]=(f[i+1][j-1])&&(s[i]==s[j])
从长度较短的字符串向长度较长的字符串进行转移的
- 初始化:
- 优化:所有的状态在转移的时候的可能性都是唯一的,即f[i][j]=f[i-1][j-1] ← f[i-2][j-2] ← ... ← 某一边界情况
因此,可以从每一种边界情况开始「扩展」,边界情况即为子串长度为 1 或 2 的情况。如果两边的字母相同,可以继续扩展;如果两边的字母不同,可以停止扩展
- 最长公共子序列
- 动态规划:
- 状态表示:①集合f[i][j]:在第1个序列前i个字符中出现,又在第2个的序列前j个字符中出现的所有子序列
②属性:所有子序列长度的最大值
- 状态划分:①集合划分:根据a[i]和b[j]是否在子序列中划分,四种情况:都不在,a[i]在,b[j]在,都在(但是要满足a[i]=b[j])
②状态转移公式:f[i][j]=max(f[i-1][j-1],f[i-1][j],f[i][j-1],f[i-1][j-1]+1);
f[i-1][j]包含了两种情况,即不包含a[i]但包含b[j],不包含a[i]和b[j]
f[i][j-1]也包含了两种情况,但是求最大值的话不影响
- 初始化:
- 编辑距离
- 动态规划:
- 状态表示:①集合f[i][j]:把word1[1~i]变成了word2[1~j]的所有方法
②属性:所有方法包含操作数量的最小值
- 状态划分:①集合划分:根据方法中的最后一种操作划分:最后一种操作是插入;最后一种操作是删除;最后一种操作是替换,俩序列最后的字符相同的话就不用替换了
②状态转移公式:f[i][j]=min(f[i][j-1]+1,f[i-1][j]+1,f[i-1][j-1]+(a[i]!=b[j]))
- 初始化:f[i][0]=i;f[0][i]=i;
- 技巧
- 只出现一次的数字
题意:一个数组中,只有一个出现一次的数字,其余的数字都出现2次
- 方法:异或
- a异或a=0;
- a异或0=a
- 异或还有结合律和分配律
- a异或b异或c = (a异或b)异或c
- 数组中所有出现2次的数字都互相变为0了,出现1次的数字和0异或还是本身
- 多数元素
题意:多数元素是大于[n/2]个的元素
- 方法一:排序
- 排序之后,第[n/2]个元素一定是答案
- T:O(nlogn) S:O(logn)
- 方法二:分治,维护一个区间[l,r]
- 数组平均分成两部分,原数组的众数一定是其中一部分的众数
- 划分区间[l,mid] [mid+1,r]
- 查看左区间和右区间的多数元素哪个是合并之后的多数元素
- 方法三:删除数组中两个不同的数,数组的多数元素
- 因为多数元素的个数大于[n/2],那多数元素和其他元素消掉之后,剩下的就都是多数元素
- 遍历一遍数组,维护一个候选众数c和该候选众数的个数count
- 如果count=0,就把当前的值赋给候选众数,并且count=1
- 如果count!=0,当前的值和候选众数相等,count++
- 否则的话就count--,抵消一个
- T:O(n) S:O(1)
- 颜色分类
- 方法一:单指针,遍历两边
- 第一遍把0排在前面
- 第二遍把1排在前面
- 方法二:双指针
- 维护一个0指针,一个1指针,遇到0或1直接交换
- 但是有个问题,如果p0<p1,遇到了0,可以看出p0所在的位置已经被1占领了,先把当前位置的0换到p0处,p0处原来的1换到p1处
- swap[nums[i],nums[p0]];swap(nums[i],nums[p1])
- 不用对交换回来的值进行再次判定,因为换过来的数要么是1要么是2,2的话不用管,1的话会p0<p1再换回去的
- 方法二:双指针
- 维护一个0指针,一个2指针,遇到0或2直接交换
- 从前开始遍历,遍历到p2,因为p2后面的值都是2
- 遇到0就交换p0,从前面交换过来的只可能是1,因为前面遇到2的话会换后面去,遇到0的话会换前面去
- 遇到2就交换p2,从后面交换过来的可能是0、1、2,所以要再次判断
- 下一个排列
- 如果序列是降序的(最大值),那么答案就是升序的(最小值)
- 从后往前找,判断两个相邻的元素,nums[i]和nums[i-1],
- 找到第一对升序的,然后从i开始遍历到数组结束,找到大于nums[i-1]的最小值,交换,对i以后的数字进行升序排列
- 寻找重复数
- 不修改 数组 nums 且只用常量级 O(1) 的额外空间
- 方法一:二分法
- cnt[i]表示小于等于i的个数,target表示要找的重复数
- 由于数组中的数字只在[1,n]中取,所以在[1,target-1]的取值范围内,cnt[i]<=i,在[target,n]的取值范围内,cnt[i]>i
- 只需要二分法找到这个数即可,边界就是cnt[i]>i
- 厉害啊 只要满足某种性质把整个区间一分为2就能用二分
- 答案在[1,n]中,[1,n]可以划分为cnt[i]<=i和cnt[i]>i两部分
- T:O(nlogn)
- 方法二:二进制
- 记录nums数组二进制展开后第i位为1的数有x个
- 数字[1,n]二进制展开后第i位为1的数有y个
- 定理:当x>y时,重复的数该位为1
- 证明:
- 如果数组中是[1,n]且加一个重复数k,那么也就是在[1,n]的基础上加上k为1位的值,满足定理
- 重复数的第i位为1,x=y+1
- 重复数的第i位为0,x=y
- 如果数组中重复的数出现了3次及以上:
- 当缺失的数第i位为1时,重复数的第i位是1,x不变,还是x>y
- 当缺失的数第i位为0时,重复数的第i位是1,x>y
- 当缺失的数第i位为1时,重复数的第i位是0,x<y
- 当缺失的数第i位为0时,重复数的第i位是0,x不变,还是x=y
- 使用位运算记录数组中所有数和[1,n]中每位1的个数和
- 方法三:快慢指针
- 对nums数组建图,每个位置i连一条 i→nums[i]的边判环
- 对于数组[1,3,4,2,2]来说,其实就是:
- 0->1有一条边
- 1->3有一条边
- 2->4有一条边
- 3->2有一条边
- 4->2有一条边
- n个节点,n+1个有向边,一定有环,且0是起始点,入度为0,因为数组中的取值为[1,n],取不到0
- 有两个入度的就是环的入口,也就是要找的重复的数
- 快慢指针找环的入口请看环形链表的推导
- ①存在环一定能相遇,且相遇时slow没有走完环的一圈
- ②a=c+(k-1)(b+c)
- T:O(2n)
- 开根号
- 方法一:二分法
- cnt[i]表示小于等于i的个数,target表示要找的重复数
- 由于数组中的数字只在[1,n]中取,所以在[1,target-1]的取值范围内,cnt[i]<=i,在[target,n]的取值范围内,cnt[i]>i
- 只需要二分法找到这个数即可,边界就是cnt[i]>i
- 厉害啊 只要满足某种性质把整个区间一分为2就能用二分
- 答案在[1,n]中,[1,n]可以划分为cnt[i]<=i和cnt[i]>i两部分
- T:O(nlogn)
- 方法二:牛顿法
- 牛顿法能够找到方程 f(x)=0 的根,是因为它利用了函数的局部线性近似(切线)来逐步逼近方程的根
- 如果切线能够很好地近似函数 f(x),那么切线与 x 轴的交点可以作为方程 f(x)=0 的一个近似解。
- 牛顿法通过不断迭代上述步骤,逐步逼近方程 f(x)=0 的根。每次迭代都会用新的 xn+1替代 xn,并重复计算切线与x轴的交点。随着迭代次数的增加,近似值xn会越来越接近方程的根。
- 二次收敛速度:在根的附近,牛顿法具有二次收敛速度。例如,如果初始误差是 ϵ,那么经过一次迭代后,误差可能变为 ϵ 2 ,经过两次迭代后,误差可能变为 ϵ4