基础算法训练1-CSDN博客
基础算法训练2-CSDN博客
基础算法题3-CSDN博客
基础算法训练4-CSDN博客
目录
搜索插入位置
寻找数组的中心下标
两整数之和
Z字性变换
数组中的第K个最大元素
计算右侧小于当前元素的个数
重排链表
存在重复元素
基本计数器II
二叉树的最大深度
35. 搜索插入位置 - 力扣(LeetCode)
该问题具有明显的二段性,同时给定数组是有序的,因此非常适合使用二分查找的思想来解决。
与标准的二分查找不同,本题在找不到目标值时,需要返回目标值按顺序插入的位置。
根据题目示例:
示例 2:
输入: nums = [1,3,5,6] , target = 2 输出: 1
解释: 2 应该插入到 1 和 3 之间,即下标 1 的位置。
示例 3: 输入: nums = [1,3,5,6] , target = 7 输出: 4
解释: 7 比数组中所有元素都大,应该插入到数组末尾,即下标 4 的位置。
从上述示例可以看出,本质上我们需要找到数组中第一个大于或等于目标值的位置。因此,可以将数组分成两段:
第一段:所有小于 target 的元素。
第二段:所有大于或等于 target 的元素。
我们的目标是找到第二段的左端点,即第一个满足 nums[i] >= target 的下标 i 。这可以直接套用二分查找的模板来实现。当二分查找的循环结束时, left 和 right 会指向同一个位置,即第二段的左端点,直接返回left或right都可。
注意事项:
如果目标值比数组中所有元素都大(即 target > nums[nums.length - 1] ),此时 left 会指向数组的最后一个元素的下标。因此,需要额外判断目标值是否大于当前 left 所指的值。如果是,则插入位置应为 left + 1 。
class Solution {
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length-1;
while(left
724. 寻找数组的中心下标 - 力扣(LeetCode)
根据题目要求,我们需要找到一个中心下标,使得该下标左侧元素的和等于右侧元素的和。需要注意的是,中心下标本身不包含在左右区间的和中。
如果使用暴力解法,对于每个下标 i ,需要分别计算:
左区间的和:从 0 到 i - 1 的元素和。
右区间的和:从 i + 1 到 len - 1 的元素和。
每次计算左右区间的和都需要遍历数组,时间复杂度为 O(N^2),这在数据规模较大时效率较低。
所以使用前缀和思想:前缀和的核心是预处理一个数组 tmp ,其中 tmp[i] 表示从 0 到 i 的元素和。通过前缀和,可以快速计算任意区间的和。
前缀和的实现步骤:
构建前缀和数组:
tmp[i] 表示从 0 到 i 的元素和,即 tmp[i] = nums[0] + nums[1] + ... + nums[i] 。
初始化 tmp[0] = nums[0] ,然后通过递推公式 tmp[i] = tmp[i - 1] + nums[i] 计算前缀和数组。
计算左右区间的和:
左区间的和:如果中心下标 i 为 0 ,则左区间和为 0 ;否则,左区间和为 tmp[i - 1] 。
右区间的和:如果中心下标 i 为 len - 1 ,则右区间和为 0 ;否则,右区间和为 tmp[len - 1] - tmp[i] (即整个数组的和减去中心下标及其左侧的和)。
判断是否为平衡中心:
如果左区间和等于右区间和,则当前下标 i 就是平衡中心,直接返回 i 。
如果遍历结束后未找到平衡中心,则返回 -1 。
class Solution {
public int pivotIndex(int[] nums) {
// 创建前缀和数组,tmp[i] 表示从 0 到 i 的元素和
int len = nums.length;
//计算数组长度
int[] tmp = new int[len];
tmp[0] =nums[0]; // 初始化前缀和数组的第一个元素
// 计算前缀和数组
for(int i = 1; i < len; i++){
tmp[i] = nums[i] + tmp[i-1];
}
for(int i=0; i < len; i++){
// 计算左侧和,如果 i == 0,左侧和为 0,否则为 tmp[i-1]
int leftSum = (i==0) ? 0 : tmp[i-1];
// 计算右侧和,如果 i == len-1,右侧和为 0,否则为 tmp[len-1] - tmp[i]
int rightSum= (i==len-1)?0 : tmp[len-1]-tmp[i];
if(leftSum ==rightSum) return i;
}
// 如果没有找到中心索引,返回 -1
return -1;
}
}
第二种前缀和优化方法使用两个数组:前缀和数组 f和 后缀和数组 g,分别记录不同区间的和
前缀和数组 f :f[i]表示区间[0, i-1]内所有元素的和。
递推公式:f[i] = f[i - 1] + nums[i - 1]
即当前前缀和等于前一个前缀和加上当前元素的前一个值。
初始化:当 i = 0时,f[0]表示区间 [0, -1]的和,这是一个无效区间,因此初始化为 0。
后缀和数组 g :g[i]表示区间 [i+1, len-1]内所有元素的和。
递推公式:g[i] = g[i + 1] + nums[i + 1]
即当前后缀和等于后一个后缀和加上当前元素的后一个值。
初始化:当 i = len - 1时,g[len - 1]表示区间 [len, len-1]的和,这是一个无效区间因此初始化为 0。
前缀和数组 f的初始化:从左向右遍历,递推计算 f[i]。当 i = 0时,f[0] = 0,因为 [0, -1]区间无效。
后缀和数组 g的初始化:从右向左遍历,递推计算 g[i]。
当 i = len - 1时,g[len - 1] = 0,因为 [len, len-1]区间无效。
class Solution {
public int pivotIndex(int[] nums) {
int len = nums.length;
// 创建前缀和数组 f,其中 f[i] 表示 [0, i-1] 区间所有元素之和
int[] f = new int[len];
// 创建后缀和数组 g,其中 g[i] 表示 [i+1, len-1] 区间所有元素之和
int[] g = new int[len];
// 预处理前缀和数组,从前向后遍历
// f[0] 默认为 0,因为 [0, -1] 区间无效
for(int i=1; i=0; i--){
g[i]= g[i+1] +nums[i+1];
}
// 使用前缀和数组 f 和后缀和数组 g 寻找平衡中心
for(int i=0; i
371. 两整数之和 - 力扣(LeetCode)
使用位运算操作,位运算里的异或操作有着特殊的作用,它能够实现无进位相加。以二进制加法为例,无进位相加的本质是不考虑进位情况,仅关注对应位的相加结果。
异或运算(^)恰好满足这一需求,其运算规则为“相同为0,不同为1”,正好契合无进位相加的逻辑。例如,对于二进制数101和110进行无进位相加,通过异或运算:101 ^ 110 = 011。
计算进位,可以借助与运算(&)和左移运算(<<)来实现。在二进制中,只有当两个bit位都为1时才会产生进位,与运算能够找出这些会产生进位的位置,得到的结果通过左移一位,表示进位要加到下一位。比如,101 & 110 = 100,左移一位后得到1000,表示在这两个数相加时,第三位产生了进位,要加到第四位。
不断循环上述过程:将无进位相加的结果和进位结果再次进行相同的操作,即无进位相加与计算进位,直到进位为0。此时,无进位相加的结果就是最终的加法结果。
class Solution {
public int getSum(int a, int b) {
// 异或运算实现无进位相加
// 异或运算的特性:0^0=0, 0^1=1, 1^0=1, 1^1=0
// 因此,a ^ b 的结果是两个数相加但不考虑进位的值
while(b != 0){
int x = a ^ b; //得到无进位的相加
// 与运算计算进位
// 与运算的特性:0&0=0, 0&1=0, 1&0=0, 1&1=1
// 只有当两个bit位都为1时,才会产生进位
// 左移一位是因为进位需要加到下一位
b = (a & b) << 1; //计算进位
a = x;
}
return a;
}
}
6. Z 字形变换 - 力扣(LeetCode)
解法一:模拟整个过程,通过创建一个二维矩阵来模拟Z字形变换的过程。在这个过程中,通过观察可以发现,当处于Z字形的垂直部分时,对应的行索引 x 等于0。此时,便可以将字符按顺序填充到二维矩阵中,直至填充到矩阵的最后一行,或者待转换的字符串结束。
完成垂直部分的填充后,需要调整行索引 x ,使其指向倒数第二行,开始进行斜向填充。斜向填充会从倒数第二行开始,朝着第一行的方向进行,同样也是填充到第一行或者字符串结束就停止。如此持续模拟这两种填充方式(垂直填充和斜向填充),直至整个字符串都被填充到二维矩阵中。
不过需要注意的是,这种方法虽然直观,但无论是空间复杂度还是时间复杂度都比较高。因为它需要额外创建一个二维矩阵来存储变换过程中的字符,并且在填充矩阵时,需要对每个字符进行多次判断和操作 。
class Solution {
public String convert(String s, int numRows) {
int len = s.length();
// 创建一个二维字符数组,用于模拟Z字形排列
char[][] ch = new char[numRows][len];
初始化矩阵坐标 x表示行,y表示列
int x=0,y=0;
int i = 0;
// 模拟Z字形排列的过程
while(i < len){
// 如果x == 0,表示当前处于Z字形的垂直部分
if(x==0){
// 向下填充字符,直到到达最后一行或字符串结束
while(x 0 && i
解法二:通过绘图分析不难发现,其中存在明显规律。
以解法一的矩阵为例,其第一行元素对应的字符串下标,呈现出一定的等差数列特征。首项为0,公差可通过简单的计算得出:第一列元素是满的,而除第一行第二个元素之前的列,每列仅有一个元素,因此公差就等于两倍列数减2(之所以减2,是因为第一列第一个元素以及最后一列最后一个元素不参与此公差规律)。
有了公差,就能轻松确定第一行元素。从字符串起始位置开始,每次跳过公差数量的元素,便能依次获取第一行对应下标的字符。
对于最后一行元素,是从字符串对应最后一行的位置开始,每次跳过最后一个元素来确定。
中间行的情况稍复杂些,每次需要确定两个位置:一个是垂直方向的元素位置,另一个是斜向方向的元素位置。起始时,从当前行的行号出发确定垂直方向的元素位置;而斜向方向的元素位置,只需用之前计算出的公差减去垂直方向元素位置的下标就能得到。按照这样的方式不断移动位置,就可以遍历并确定中间行的所有元素位置。
class Solution {
public String convert(String s, int numRows) {
//处理特殊情况 numRows为1,公差会为为自己,会死循环
if(numRows==1) return s;
//首先计算公差 即 Z 字形排列中一个完整周期的字符数
// 例如,当 numRows = 3 时,d = 4(PAYP 是一个周期)
int d = 2*numRows-2;
int len= s.length();
StringBuilder builder = new StringBuilder();
//先处理第一行
// 第一行的字符在字符串中的索引为:0, d, 2d, 3d, ...
for(int i=0; i
215. 数组中的第K个最大元素 - 力扣(LeetCode)
使用 PriorityQueue 来实现最小堆。最小堆显著特征为,堆顶的元素始终是整个堆中最小的元素。
举例来说,假设有 5 个元素,设定参数 k 为 3 ,我们构建一个容量为 3 个元素的最小堆。在这种情况下,堆顶元素不仅是堆中的最小值,从另一个角度看,它也可以被认为是这组数据中的第三大值。倘若此时堆中的元素数量增加到 4 ,那么堆顶元素就成为了第四大的值,此时只需将堆顶元素删除,便可重新维持堆的性质。
具体实现步骤为:遍历给定的数组,依次将数组中的每个元素添加到最小堆中。在添加过程中,一旦发现堆中的元素个数超过了 K ,就移除堆顶元素(因为堆顶是最小的,移除它可以保证堆内剩余元素相对较大)。当整个数组遍历结束后,此时堆顶元素就是我们最终需要的结果,返回即可。
class Solution {
public int findKthLargest(int[] nums, int k) {
//进行建小堆(优先级队列,默认是升序)
PriorityQueue heap = new PriorityQueue<>();
for(int num : nums){
heap.add(num);
// 如果堆的大小超过了 k,则移除堆顶元素
// 因为堆顶元素是当前堆中最小的元素,而我们需要的是第 k 个最大的元素
// 所以当堆的大小超过 k 时,堆顶元素肯定不是第 k 大的,而是第 k+1 大的,因此直接移除
if(heap.size() > k){
heap.poll();
}
}
// 最终堆顶元素就是第 k 个最大的元素
return heap.poll();
}
}
315. 计算右侧小于当前元素的个数 - 力扣(LeetCode)
在处理一问题时,采用与基础算法3中逆序对相同的归并思想。
核心思想
具体逻辑
关键点强调
class Solution {
int[] ret; // 返回数组,存储每个元素右侧小于它的元素个数
int[] index; // 标记 nums 中当前元素原始的下标
int[] tmpIndex; // 临时数组,用于归并排序时存储下标
int[] tmpNums; // 临时数组,用于归并排序时存储元素值
public List countSmaller(int[] nums) {
int len = nums.length;
ret = new int[len]; // 初始化返回数组
index = new int[len]; // 初始化下标数组
tmpIndex = new int[len]; // 初始化临时下标数组
tmpNums = new int[len]; // 初始化临时元素数组
// 初始化 index 数组,记录每个元素的原始下标
for (int i = 0; i < len; i++) {
index[i] = i;
}
// 调用归并排序方法,计算右侧小于当前元素的个数
mergeSort(nums, 0, nums.length - 1);
// 将结果数组 ret 转换为 List 并返回
List list = new ArrayList<>();
for (int num : ret) {
list.add(num);
}
return list;
}
// 归并排序方法,计算右侧小于当前元素的个数
public void mergeSort(int[] nums, int left, int right) {
// 如果左边界大于等于右边界,直接返回
if (left >= right) return;
// 计算中间位置,将数组划分为两个区间 [left, mid] 和 [mid+1, right]
int mid = left + (right - left) / 2;
// 递归处理左区间
mergeSort(nums, left, mid);
// 递归处理右区间
mergeSort(nums, mid + 1, right);
// 合并两个有序区间,并计算右侧小于当前元素的个数
int cur1 = left; // 左区间的起始位置
int cur2 = mid + 1; // 右区间的起始位置
int i = 0; // 临时数组的起始位置
// 遍历左右区间,合并有序区间
while (cur1 <= mid && cur2 <= right) {
if (nums[cur1] <= nums[cur2]) {
// 如果左区间的当前元素小于等于右区间的当前元素
// 将右区间的元素和下标存入临时数组
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
} else {
// 如果左区间的当前元素大于右区间的当前元素
// 说明右区间中 cur2 及其之后的元素都小于 nums[cur1]
// 因此,nums[cur1] 的右侧小于它的元素个数增加 right - cur2 + 1
ret[index[cur1]] += right - cur2 + 1;
// 将左区间的元素和下标存入临时数组
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
}
// 如果左区间还有剩余元素,将其全部存入临时数组
while (cur1 <= mid) {
tmpNums[i] = nums[cur1];
tmpIndex[i++] = index[cur1++];
}
// 如果右区间还有剩余元素,将其全部存入临时数组
while (cur2 <= right) {
tmpNums[i] = nums[cur2];
tmpIndex[i++] = index[cur2++];
}
// 将临时数组中的元素和下标拷贝回原数组
for (int j = left; j <= right; j++) {
nums[j] = tmpNums[j - left];
index[j] = tmpIndex[j - left];
}
}
}
143. 重排链表 - 力扣(LeetCode)
该题就是一道模拟题,从模拟时候发现只需要找到链表的中心节点,然后把后半部分进行逆序,该题可以从slow所指的部分进行逆序,也可以从slow下一个节点开始逆序,但是推荐从slow下一个节点开始逆序,因为可以把前半部分和后半部分断开连接,如果是从slow位置开始则不能断开连接。接着把前半部分和后半部分进行合并即可
这道模拟题的解题思路十分清晰。解题时,关键的第一步是找链表中心节点,可借助快慢指针法,快指针每次走两节点、慢指针每次走一节点,快指针到链表末尾时,慢指针所指即中心节点。
在这里可以把前半部分和后半部分断开,不然合并时可能出错
找到中心节点后,需对链表后半部分逆序。通过改变指针指向来实现逆序,为后续合并做准备。
最后一步是合并前半部分与逆序后的后半部分链表。合并时仔细处理节点指针指向,以形成完整新链表。按找中心节点、逆序后半部分、合并这三步,就能成功解出这道模拟题。
class Solution {
public void reorderList(ListNode head) {
//处理边界
if(head==null || head.next==null || head.next.next==null) return;
//找到链表的中心节点 采用快慢指针
ListNode fast = head, slow = head;
while(fast!=null && fast.next!=null){
fast = fast.next.next;
slow = slow.next;
}
//翻转slow后半部分
ListNode head1 = slow.next;
slow.next = null; //将两个链表分离
ListNode cur = head1;
ListNode prev = null; //存放的是头结点
while(cur!=null){
ListNode n = cur.next; //存放下一个节点
//头插
cur.next = prev;
prev = cur;
cur = n;
}
//合并两个链表
head1 = prev;
while(head !=null && head1!=null){
ListNode tmp1 = head.next; //保存head1的下一个节点
ListNode tmp2 = head1.next; //保存head2的下一个节点
head.next = head1;
head1.next = tmp1;
head = tmp1;
head1 = tmp2;
}
}
}
217. 存在重复元素 - 力扣(LeetCode)
在解答这道题时,我依据数组中是否存在负数,将解题过程分为了两个部分。
数组存在负数的情况:
当数组中存在负数时,我采用了 Set 集合来解决问题。遍历数组的过程中,将每个元素逐个添加到 Set 集合中。 Set 集合的 add 方法有一个特性:若待添加的元素已经存在于集合中,该方法会返回 false ,利用这一特性,恰好可以满足本题的判断需求。
数组不存在负数的情况:
若数组中没有负数,我使用位图这一数据结构。位图的大小依据数组中的最大值来确定,因为位图需要能够容纳数组中的所有元素。确定位图大小后,计算元素在位图中的存储位置:通过将元素除以一个固定值,得到该元素在位图中应存放的下标位置;再利用取余运算,确定在该下标处具体要存放的比特位。
通过对数组是否含有负数的情况进行分类,使用 Set 集合和位图两种不同策略,能够高效解决本题。
class Solution {
public boolean containsDuplicate(int[] nums) {
//寻找最大值
int max = 0;
int min = 0;
for(int i: nums){
if(max < i) max = i;
if(min > i) min = i;
}
//根据最小值,如果元素无负数用位图,否则用set
if(min < 0){
Set set = new HashSet<>();
for(int i : nums){
//add方法如果有重复元素会添加失败返回false
boolean flag = set.add(i);
if(flag == false) return true;
}
} else{
//位图的大小用数组中的最大值进行计算,每个byte可以存储8个元素信息
byte[] array = new byte[max/8+1];
for(int i : nums){
int a = i / 8; //计算元素在位图中下标位置
int b = i % 8; //当前下标中的具体存放位置
// 如果不为不为自己说明该处已经被设置过元素
if( (array[a] & (1<
227. 基本计算器 II - 力扣(LeetCode)
在处理表达式求值这类题目时,有一种通用且有效的解法,那就是运用栈这种数据结构来模拟整个计算的过程。在进行模拟的过程中,通常会碰到以下三种不同的情况,针对每种情况都有相应的处理逻辑:
1.遇到操作符:当扫描到表达式中的操作符时,需要对当前所记录的操作符进行更新,以便后续对数字进行正确的运算操作。例如,从之前的“+”操作符更新为“*”操作符,这样后续遇到数字时就能按照新的操作符进行计算。
2.遇到数字:当扫描到表达式中的数字时,需要将这个数字完整地提取出来。提取出数字后,根据当前所记录的操作符(记为op)的不同,分情况进行讨论和处理:
3.遇到空格:当扫描到表达式中的空格时,由于空格在表达式中通常只是起到分隔数字和操作符的作用,并不参与实际的计算,所以直接跳过这个空格,继续扫描后续的字符。
当按照上述逻辑完整地扫描并处理完整个表达式后,此时栈中存储的就是经过部分运算后的中间结果。最后,只需要将栈中的所有元素相加,得到的和就是整个表达式的计算结果,将这个结果返回即可完成表达式求值的任务。
class Solution {
public int calculate(String s) {
char op = '+'; //操作符初始化为+,用于处理第一个字符
Deque q =new ArrayDeque<>();
char[] arr = s.toCharArray();
for(int i=0; i='0'&&arr[i]<='9'){
int tmp = 0;
//如果当前为数字,则解析出完整数字
while(i='0'&&arr[i]<='9'){
tmp = tmp*10+(arr[i++]-'0');
}
//因为在上述while循环中i++当不满足条件时,for循环要自增又向后移动了一位
//会跳过一位字符所以修正i
i--;
//分情况讨论 处理操作数
if(op == '+'){
q.push(tmp);
}else if(op == '-'){
q.push(-tmp);
}else if(op == '*'){
//如果是'*'则说明优先级最高可以直接操作
//直接取出栈顶元素和当前数字相乘
q.push(tmp*q.pop());
}else {
//如果是'/'则说明优先级最高可以直接操作
//直接取出栈顶元素和当前数字相除
q.push(q.pop()/tmp);
}
}else {
//如果当前是操作符 则更新操作符
op = arr[i];
}
}
//将栈中欧所有元素相加 并返回
int ret = 0;
while(!q.isEmpty()) ret+=q.pop();
return ret;
}
}
662. 二叉树最大宽度 - 力扣(LeetCode)
在设计数据结构时,选择使用数组来模拟队列,通过索引计算,精准定位每个节点在数组中的位置。数组中存储的元素不仅包含节点本身,还记录了节点所处的位置信息。采用数组模拟队列,核心优势在于数组能够凭借下标,以O(1)的时间复杂度快速访问目标元素,极大提升数据访问效率。尽管在实际运行过程中,数组下标可能会出现溢出的情况,但这并不会对最终结果的准确性造成影响。这是因为该数据结构本质上属于循环队列,关注点并非单个下标的具体数值,而是队列中最右侧下标与最左侧下标之间的差值。
然而,数组在执行删除队头元素操作时,弊端也较为明显,其时间复杂度达到O(n)。所以每次处理数据时,会创建一个临时数组。先将下一层的数据转移到临时数组中,操作完成后,再将临时数组中的数据拷贝回原数组。经过这样的处理,计算队列宽度变得轻而易举。队列宽度的计算方法为,用最右侧数组下标减去最左侧数组下标,由于数组下标从0开始,为确保结果准确,需在此基础上加1。
class Solution {
public int widthOfBinaryTree(TreeNode root) {
// 使用数组模拟队列来存储节点及其位置索引
// Pair 表示节点和其在当前层的索引位置
List> q = new ArrayList<>();
// 将根节点加入队列,初始索引位置为0
// 题目保证root不为空,所以可以直接入队
q.add(new Pair(root,0));
int ret =0; //返回值最大宽度
//进行层序遍历
while(!q.isEmpty()){
// 获取当前层的第一个节点和最后一个节点
Pair t1 = q.get(0);
Pair t2 = q.get(q.size()-1);
// 更新最大宽度 计算当前层的宽度:最后一个节点的索引 - 第一个节点的索引 + 1
ret = Math.max(ret,t2.getValue()-t1.getValue()+1);
//由于数组头删时间复杂度较高,所以使用一个临时数组,将下一层所有节点入队
//然后把临时数组赋值给q即可
List> tmp = new ArrayList<>();
for(Pair t : q){
TreeNode node = t.getKey();
Integer index = t.getValue();
//如果左节点存在则将入队 并计算出下标编号
if(node.left != null){
tmp.add(new Pair(node.left,index*2));
}
//如果左节点存在则将入队 并计算出下标编号
if(node.right != null){
tmp.add(new Pair(node.right,index*2+1));
}
}
// 将下一层的节点队列设置为当前队列,继续处理下一层
q = tmp;
}
return ret;
}
}