左程云算法学习
左程云算法学习
二分法
- 二分查找法:704.二分查找
- 代码如下:
1 |
|
- 变式 1:查找第一个和
target
相等的元素 - 代码如下:
1 |
|
- 变式 2:查找第一个大于等于
target
的元素 - 代码如下:
1 |
|
- 变式 3:查找最后一个小于等于
target
的值 - 代码如下:
1 |
|
异或运算
题目一
- 如何不用额外变量交换两个数
- 关键部分:
1 |
|
- 代码:
1 |
|
- 证明:
- 1、异或可以看成不进位二进制位加法;
- 2、具有
a^a=0
和a^0=a
以及a^b=b^a
的性质。 - 各个步骤详细推导如下:
题目二
- 一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数?
- 关键部分:
1 |
|
- 代码:
1 |
|
- 证明:
- 1、具有
a^a=0
和a^0=a
的性质; - 2、偶数个数据累积异或后是
0
,奇数个数据累积异或后就剩下它自身。
- 1、具有
题目三
- 怎么把一个
int
类型的数,提取出最右侧的1
出来。 - 关键部分:
ans=a&(-a);
- 代码:
1 |
|
- 证明:
题目四
- 一个数组中有两个不相等的数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两个数?
- 关键代码:
1 |
|
- 代码:
1 |
|
题目五
- 一个数组中有一种数出现
K
次,其他数都出现了M
次,M>1
,K<M
,请找到出现了K
次的数,要求额外空间复杂度O(1)
,时间复杂度O(N)
。 - 关键代码:
1 |
|
- 代码:
1 |
|
数组
题目一
- 有序数组的平方:977.有序数组的平方
- 思路:
- 能够找到数组
nums
中负数与非负数的分界线,那么就可以用类似「归并排序」的方法。 - 具体地,设
index
为数组nums
中负数与非负数的分界线,也就是说,nums[0]
到nums[index]
均为负数,而nums[index+1]
到nums[len−1]
均为非负数。当我们将数组nums
中的数平方后,那么nums[0]
到nums[index]
单调递减,nums[index+1]
到nums[len−1]
单调递增。 - 由于我们得到了两个已经有序的子数组,因此就可以使用归并的方法进行排序了。具体地,使用两个指针分别指向位置
index
和index+1
,每次比较两个指针对应的数,选择较小的那个放入答案并移动指针。当某一指针移至边界时,将另一指针还未遍历到的数依次放入答案。
- 能够找到数组
- 代码:
1 |
|
题目二
- 长度最小的子数组:209.长度最小的子数组
- 思路:
- 滑动窗口就是满足总和
sum
大于等于target
的长度最小的连续子数组。 - 滑动窗口的起始位置如何移动:如果当前窗口的总和
sum
大于等于target
了,窗口就要向前移动了(也就是该缩小i++
了)。 - 滑动窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是
for
循环里的索引,j++
。 - 可以发现滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将 暴力解法降为 。
- 滑动窗口就是满足总和
- 代码:
1 |
|
题目三
- 螺旋矩阵 II:59.螺旋矩阵 II
- 思路:
- 这道题就是模拟螺旋矩阵生成的过程,分为四个方向进行循环,同时设定相应的循环边界条件达到模拟的实现。
- 注意在 C++ 代码中
vector
的二维动态数组可以定义为:vector<vector<int>> mat(n,vector<int>(n));
- 代码:
1 |
|
题目四
- 区间和:58. 区间和(第九期模拟笔试)
- 本题主要用于熟悉 ACM 模式的输入输出!
- 代码如下:
1 |
|
题目五
- 开发商购买土地:44. 开发商购买土地(第五期模拟笔试)
- 思路:
- 前缀和计算
- 横向前缀和:计算每一行从左到右的累加和,存储在
heng
数组中; - 竖向前缀和:计算每一列从上到下的累加和,存储在
shu
数组中。
- 横向前缀和:计算每一行从左到右的累加和,存储在
- 寻找最小差值:
- 竖向分割:尝试所有可能的竖向分割线位置,计算左右两部分和的差值,差值计算公式为
shu[m-1] - 2*shu[i]
(总列和减去两倍左侧和); - 横向分割:尝试所有可能的横向分割线位置,计算上下两部分和的差值,差值计算公式为
heng[n-1] - 2*heng[i]
(总行和减去两倍上方和); - 记录所有可能分割中的最小绝对差值。
- 竖向分割:尝试所有可能的竖向分割线位置,计算左右两部分和的差值,差值计算公式为
- 前缀和计算
- 代码:
1 |
|
链表
题目一
- 移除链表元素:203.移除链表元素
- 直接解法代码:
1 |
|
- 虚拟头节点解法代码:
1 |
|
题目二
- 移除链表元素:707.设计链表
- 代码:
1 |
|
题目三
- 反转链表:206.反转链表
- 代码:
1 |
|
题目四
- 两两交换链表中的节点:24.两两交换链表中的节点
- 代码:普通解法
1 |
|
- 虚拟头节点解法:
1 |
|
题目五
- 删除链表的倒数第
N
个节点:19.删除链表的倒数第 N 个节点 - 思路:使用双指针的方法,在
now
指针往后移动的同时使得pre
指针与now
指针保持N
的距离,当now
指针遍历到链表末尾后就可以采用pre
指针来删除链表的倒数第N
个节点。 - 代码:
1 |
|
题目六
- 相交链表:160.相交链表
- 思路:
- 代码:
1 |
|
题目七
- 环形链表 II:142.环形链表II
- 思路:代码随想录-142.环形链表
- 代码:
1 |
|
哈希表
题目一
- 四数相加 II:454. 四数相加 II
- 思路:
- 哈希表的构建:通过双层循环遍历
nums1
和nums2
,计算所有可能的和a + b
,并使用哈希表unordered_map<int,int> mp1;
记录每个和出现的次数(key:a + b
,value:a + b
出现的次数)。 - 补数查找:遍历
nums3
和nums4
,计算每对元素的和c + d
,并查找其相反数-(c + d)
是否存在于哈希表中。若存在,则说明找到了一组满足条件的四元组,累加对应的次数到结果ans
中。
- 哈希表的构建:通过双层循环遍历
- 代码:
1 |
|
题目二
- 三数之和:15. 三数之和
- 思路:
- 这道题目与上一两数之和不同之处在于采用双指针法进行处理。
- 排序预处理:将数组排序,使相同的元素相邻,便于后续跳过重复元素,同时为双指针法创造条件。
- 遍历固定第一个数(
a
):遍历数组,固定当前元素作为三元组的第一个数a
。若a > 0
,由于数组已排序,后续元素无法使三数之和为0
,直接返回结果。 - 去重处理(
a
的去重):若当前a
与前一个元素相同(i > 0 && nums[i] == nums[i-1]
),跳过该元素,避免重复的三元组。 - 双指针寻找后两个数(
b
,c
):- 定义左右指针
left
和right
,初始分别指向i+1
和数组末尾。 - 计算三数之和
temp
:- 若
temp > 0
:右指针左移(减小总和)。 - 若
temp < 0
:左指针右移(增大总和)。 - 若
temp == 0
:找到有效三元组,加入结果,并对b
和c
去重。
- 若
- 定义左右指针
- 去重处理(
b
和c
的去重):找到有效三元组后,跳过所有与当前b
(nums[left]
)和c
(nums[right]
)相同的元素,避免重复。
- 代码:
1 |
|
题目三
- 四数之和:18. 四数之和
- 思路:
- 与前一题的三数之和类似,主要差距仅在外层再加一次循环。
- 需要注意一下一级提前终止(
nums[i]>target && nums[i]>0
)和二级提前终止(twoSum > target && twoSum > 0
)的条件!
- 代码:
1 |
|
字符串
题目一
- 替换数字:54. 替换数字(第八期模拟笔试)
- 思路:首先扩充数组到每个数字字符替换成
number
之后的大小,再从后向前进行填充。其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。- 好处如下:
- 不用申请新数组。
- 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。
- 好处如下:
- 代码:
1 |
|
题目二
- 反转字符串中的单词:151.反转字符串中的单词
- 思路:
- 移除单词中多余的空格->整体反转字符串->逐个单词反转
- 这道题目需要注意的是自己写
string deleteExtraSpaces(string s)
函数代码来移除单词中多余的空格。
- 代码:
1 |
|
KMP 算法
- 字符串的前缀:不包含最后一个字符的所有以第一个字符开头的连续子串。
- 字符串的后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串。
- 举例:字符串
"aab"
- 其前缀有:
“a”
、"aa"
- 其后缀有:
“b”
、"ab"
- 其前缀有:
- 举例:字符串
- 前缀表:前缀表要求的就是相同前后缀的长度。
- 字符串
"aab"
的最长公共前后缀的长度为 1。 - 前缀
"aa"
和后缀"ab"
重合的部分。
- 字符串
- KMP 算法在的
next[]
数组储存的就是前缀表。- 前缀表是用来回退的,它记录了模式串与主串 (文本串) 不匹配的时候,模式串应该从哪里开始重新匹配。
next[]
数组的计算方式:- 初始化:注意初始化
i=1
- 不相等的清空:使用
while()
循环持续回退 - 相等的情况
- 初始化:注意初始化
- 代码:
1 |
|
题目三
- 找出字符串中第一个匹配的下标:28.找出字符串中第一个匹配的下标
- 思路:KMP 算法的最重要应用,使用前缀表
next[]
数组当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。 - 代码:
1 |
|
题目四
- 重复的子字符串:459.重复的子字符串
- 思路:
- 如果
s.size() % (s.size()-next[s.size()-1]) == 0
,则说明数组的长度正好可以被最长相等前后缀不包含的子串的长度整除,说明该字符串有重复的子字符串。 - 具体思路和证明:459.重复的子字符串思路
- 如果
- 代码:
1 |
|
队列和栈
题目一
- 用栈实现队列:232.用栈实现队列
- 思路:代码随想录-232.用栈实现队列
- 代码:
1 |
|
题目二
- 用队列实现栈:225.用队列实现栈
- 思路:采用两个队列,始终保持一个队列为空,当要
pop
数据的时候,把非空队列的元素依次输出到另外一个空队列,剩下最后一个元素进行返回和pop
。push
数据时要向非空队列传入数据,若两个队列都是空的就向任意一个队列传数据即可。 - 代码:
1 |
|
题目三
- 删除字符串中的所有相邻重复项:1047. 删除字符串中的所有相邻重复项
- 思路:
- 初始化一个空栈:用于存储字符。
- 遍历字符串:
- 如果栈为空,或者当前字符与栈顶字符不同,则将当前字符压入栈。
- 如果当前字符与栈顶字符相同,则弹出栈顶字符(表示删除这对相邻重复字符)。
- 构建结果字符串:
- 遍历结束后,栈中剩下的字符就是删除所有相邻重复字符后的结果。
- 由于栈是后进先出的,我们需要将栈中的字符逆序才能得到正确的顺序。
- 字符串逆序采用
reverse(res.begin(),res.end());
函数,需要引入#include <algorithm>
头文件,该函数可以用于翻转数组、字符串和向量(vector
)。
- 代码:
1 |
|
题目四
- 逆波兰表达式求值:150. 逆波兰表达式求值
- 注意点:将
string
变量转换为int
型、long
型、long long
型变量可以使用std::stoi
、std::stol
和std::stoll
函数! - 代码:
1 |
|
题目五
- 滑动窗口最大值:239. 滑动窗口最大值
- 思路:
- 实现单调队列,并且该队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。
- 设计单调队列的时候,
pop
,和push
操作要保持如下规则:pop(value)
:如果窗口移除的元素value
等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作;push(value)
:如果push
的元素value
大于入口元素的数值,那么就将队列入口的元素弹出,直到push
元素的数值小于等于队列入口元素的数值为止。- 保持如上规则,每次窗口移动的时候,只要问
que.front()
就可以返回当前窗口的最大值。
- 单调队列实现代码:
1 |
|
- 代码:
1 |
|
- 时间复杂度: ,空间复杂度: 。
二叉树
二叉树的递归遍历
- 前序遍历:144. 二叉树的前序遍历
1 |
|
- 中序遍历:94. 二叉树的中序遍历
1 |
|
- 后序遍历:145. 二叉树的后序遍历
1 |
|
二叉树的迭代遍历
- 前序遍历迭代法:144. 二叉树的前序遍历
1 |
|
- 中序遍历迭代法:94. 二叉树的中序遍历
- 由于在中序遍历的过程中,要访问的元素和要处理的元素顺序不一致,在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
1 |
|
- 后序遍历迭代法:145. 二叉树的后序遍历
- 前序遍历顺序:中左右;
- 后序遍历顺序:左右中;
- 那么可以简单修改前序遍历顺序实现中右左,然后再反转答案数组即可。
1 |
|
二叉树的统一迭代遍历
- 空指针标记法:就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记,三种不同的遍历方式只需修改少量代码。
- 中序遍历迭代法:94. 二叉树的中序遍历
1 |
|
- 前序遍历迭代法:144. 二叉树的前序遍历
1 |
|
- 后序遍历迭代法:145. 二叉树的后序遍历
1 |
|
排序问题
选择排序
普通选择排序
- 选择排序理论:
- 代码:
1 |
|
- 分析:
冒泡排序
普通冒泡排序
- 冒泡排序理论:
- 代码:
1 |
|
- 分析:
插入排序
普通插入排序
- 插入排序理论:
- 代码:
1 |
|
- 性质和分析:
- 在原数组有序的最好情况下,算法时间复杂度是 !
- 空间复杂度为 !
- 插入排序算法具有稳定性(假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序后,这些记录的相对次序保持不变)
- 使用场景:
- 数组长度比较小;
- 当数组基本有序(离排好序的最终位置很近),插入排序可以在 时间内完成排序!
希尔排序
普通希尔排序
- 希尔排序理论:
- 代码:
1 |
|
- 分析:
- 时间复杂度和
gap
序列相关,一般而言平均情况小于 ; - 空间复杂度为 ;
- 插入排序算法不具有稳定性,因为发生了长距离交换,通过牺牲不稳定性来换取时间。
- 时间复杂度和
归并排序
普通归并排序
- 递归方法:912.排序数组
- 思路:采用二分法和递归的方式实现 的归并排序;
- 代码:
1 |
|
- 分析:
- 归并排序算法是稳定的,但是空间复杂度比较高;
小和问题
- 问题描述:
- 在一个数组中,每一个数的左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。
- 例子:对于数组
[1,3,4,2,5]
,1
左边比1
小的数,没有;3
左边比3
小的数,1
;4
左边比4
小的数,1,3
;2
左边比2
小的数,1
;5
左边比5
小的数,1,2,3,4
;- 所以小和为
1+1+3+1+1+2+3+4 = 16
。
- 例子:对于数组
- 思路:
- 此处使用归并排序,在
merge
时,由于左右两部分都已经有序,可以确定一侧的数都大于正在比较的数,例如, - 归并
2 4 5 | 1 3 7
两个部分时,2
比3
小,此时可以确定后面的数都大于2
,此时便可以一次性计算小和2 * 2
(两个数大于2
),而不用一个个遍历。
- 此处使用归并排序,在
- 代码:
1 |
|
逆序对问题
- 交易逆序对总数:LCR 170 交易逆序对总数
- 思路:参考归并排序的算法,区别在于在
merge
的时候从右到左进行比较,- 左组数比右组数大时,通过
ans+=j-(M+1)+1;
计算逆序对的个数,并将左组数拷贝到temp
数组并左移一位; - 右组数比左组数大时,无需计算逆序对个数,只需要将右组数拷贝到
temp
数组并左移一位即可。 - 注意递归的边界条件!
- 左组数比右组数大时,通过
- 代码:
1 |
|
双倍逆序对问题
- 题目描述:
- 给定一个整数数组
nums
,统计其中满足以下条件的逆序对数量:
- 条件:存在两个下标
i
和j
(i < j
),使得nums[i] > 2 * nums[j]
。要求设计一个时间复杂度为 的算法解决此问题。 - 示例:
- 输入:
nums = [8, 3, 5, 1, 2]
- 输出:
6
- 解释: 满足条件的逆序对为:
(8,3)
,(8,1)
,(8,2)
,(3,1)
,(5,1)
,(5,2)
,共6
对。
- 输入:
- 给定一个整数数组
- 思路:
- 基于归并排序的分治框架,在合并两个有序子数组的过程中,利用双指针滑动窗口高效统计满足条件的逆序对。具体步骤如下:
- 递归划分:将数组递归分割为子数组,直到子数组长度为
1
。 - 合并与统计:在合并两个已排序的子数组时,遍历左半部分的每个元素,通过滑动窗口找到右半部分中满足
nums[i] > 2 * nums[j]
的元素数量。 - 排序保证:合并完成后,保证当前子数组有序,为后续递归合并提供正确输入。
- 代码:
1 |
|
区间和的个数问题
- 区间和的个数问题:327.区间和的个数
- 思路:
- 转化为归并排序进行求解,复杂度为 。将原问题转化为前缀和数组的有序对统计问题,区间和
[i, j]
对应前缀和sum[j] - sum[i]
,要求满足lower ≤ sum[j] - sum[i] ≤ upper
,即等价于统计满足sum[j] - upper ≤ sum[i] ≤ sum[j] - lower
的有序对(i, j)
(i < j
)。
- 转化为归并排序进行求解,复杂度为 。将原问题转化为前缀和数组的有序对统计问题,区间和
- 代码:
1 |
|
快速排序
颜色分类
- 颜色分类问题:75.颜色分类
- 思路:
- 代码:
1 |
|
随机快排
- 思路:
- 在前面所述的颜色分类问题的基础上,随机快排的
partition()
函数将数组分为<基准值区域
、=基准值区域
、>基准值区域
三个区域,通过pre
和last
标记边界; - 快速排序随机选择从
[L, R-1]
范围上选择基准值与nums[R]
进行交换,在代码中通过swap(nums,R,rand()%(R-L+1))
体现; - 随机快排采用递归的方式完成对数组的排序,需要注意随机快排依赖
qickSort()
函数中的递归终止条件(L >= R
)。
- 在前面所述的颜色分类问题的基础上,随机快排的
- 代码:
1 |
|
- 分析:
- 快排的空间复杂度是 ,并且快排算法是不稳定的。
左程云算法学习
http://example.com/2025/03/07/zuo_algorithm/