目录
快乐数
这道题的解题思路与判断链表是否有环的思路一致。我们定义两个指针,其中快指针在初始化时先走一步,慢指针则初始化为n。根据鸽巢原理,这两个指针最终必然会相遇。
例如,当n取9999999999时,按照题设规则运行一次后得到的结果为810 。这表明,当循环次数超过810次时,在[0, 810]这个范围内必然会出现重复的数。而我们要做的,就是判断这个重复的数是否为1。
class Solution {
public boolean isHappy(int n) {
//快慢指针 注意fast开始不能等于n,否则循环进不去
int fast = bitSum(n), slow = n;
while(fast != slow){
//快指针走两步,慢指针走一步
fast = bitSum(bitSum(fast));
slow = bitSum(slow);
}
//如果相遇点为1返回true 否则返回false
return fast == 1;
}
public int bitSum(int n){
int sum = 0;
while(n != 0){
int t = n%10;
sum += t * t;
n /= 10;
}
return sum;
}
}
无重复字符的最长子串
使用滑动窗口解决,滑动窗口就是在字符串上设定一个由左右边界所界定的窗口,通过不断地移动这个窗口来遍历字符串。
具体的实现过程为:当窗口向右移动时,也就是进入新的字符,将新字符放入一个类似哈希表的数据结构中,以此来记录字符的出现情况。一旦发现哈希表中出现了重复的字符,这就意味着当前窗口内的子字符串不再满足无重复字符的条件。通过移动窗口的左边界来“出窗口”,即移除窗口中最早出现的字符,直到窗口内不再有重复字符为止。
在这个过程中,每当窗口调整完毕,根据当前窗口的大小和之前记录的大小来判断是否更新长度。
由于字符串中可能出现的字符种类通常是有限的,ASCII字符集中,字符的数量是128个。因此,可以使用一个数组来充当哈希表,数组的索引对应字符的ASCII码值,数组中的元素则记录相应字符出现的次数。
class Solution {
public int lengthOfLongestSubstring(String s) {
int left = 0, right = 0;
int length = 0;
//用数组充当hash表,用里面的数组表示出现的次数
int[] hash = new int[128];
while(right < s.length()){
//表示right位置的字符出现次数+1
char ch =s.charAt(right++);
hash[ch]++; //进窗口
while(hash[ch] > 1){
//出窗口
hash[s.charAt(left++)]--;
}
//判断条件
length = Math.max(length,right-left);
}
return length;
}
}
在排序数组中查找元素的第一个和最后一位置
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
使用二分查找算法分别查找左端点和右端点。
1. 查找左端点元素:在查找过程中,设定左区间为无效区间(left指向无效值),右边为有效区间(right指向有效值)。当中间元素小于目标值时,由于左区间属于无效值,可直接跳过。当中间元素大于或等于目标值时, right 不能执行 +1 操作,因为该元素有可能就是左端点值。完成左端点查找后,如果目标值存在,再继续查找右端点;若不存在,则直接返回。
2. 查找右端点元素:此时 mid 必须向上取整,否则可能陷入死循环,例如:
left = 2, right = 3;
//不加+1 下一次进入还是2
mid = left + (right - left) / 2 = 2 + (3 - 2) / 2 = 2
//加1
mid = left + (right - left + 1) / 2 = 2 + (3 - 2 + 1) / 2 = 3
在查找时,设定左边为有效区间(left为有效值),右边为无效区间(right为无效值)。当中间元素小于等于目标值时,左区间进行更新,不能跳过,因为该元素有可能就是右端点值;当中间元素大于目标值, right进行跳过。
class Solution {
public int[] searchRange(int[] nums, int target) {
int size =nums.length;
int[] ret = new int[2];
int mid = 0; //中间下标
//返回数组默认值为-1
ret[0] = ret[1] = -1;
int left = 0, right = size-1;
if(size == 0) return ret;
while(left < right){
//要找到左端点,就需要左区间都是不合法的值,右区间第一个为合法值
mid = left + (right-left) / 2;
if(nums[mid] < target) left = mid + 1; //由于都是不合法的值所以直接跳过
else right = mid; //nums[mid]可能是target所以不能+1
}
//查看该值是否存在
if(nums[right] != target) return ret;
ret[0] = left;
left = 0;
right = size-1;
while(left < right){
// 为了寻找右端点,避免死循环,让 mid 向右偏(向上取整),所以 +1
mid =left + (right - left + 1) / 2;
//当求右面最后一个值时,左区间元素都是有效区间,
if(nums[mid] <= target) left = mid;
else right = mid - 1; // 目标值的右端点一定在 mid 的左侧
}
ret[1] = right;
return ret;
}
}
[模板]二维前缀和
该题使用前缀和思想,首先预处理一个前缀和矩阵,然后进行使用前缀和矩阵
预处理前缀和矩阵

当使用前缀和矩阵时求[x1,y1]到[x2,y2]就是可以表示为:

import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int m = in.nextInt();
int q = in.nextInt();
//因为下标是从1开始所以访问时要包含n和m
int[][] arr = new int[n+1][m+1];
long[][] tmp = new long[n+1][m+1];
//接受参数
for(int i=1; i <= n; i++){
for(int j=1; j <= m; j++){
arr[i][j] = in.nextInt();
}
}
//预处理前缀和矩阵
for(int i=1; i <= n; i++){
for(int j=1; j <= m; j++){
tmp[i][j] = tmp[i-1][j]+tmp[i][j-1]+arr[i][j]-tmp[i-1][j-1];
}
}
for(int i = 0; i < q; i++){
int x1 = in.nextInt();
int y1 = in.nextInt();
int x2 = in.nextInt();
int y2 = in.nextInt();
long sum = tmp[x2][y2]-tmp[x1-1][y2]-tmp[x2][y1-1]+tmp[x1-1][y1-1];
System.out.println(sum);
}
}
}
丢失的数字
解法很多,排序、高斯求和、哈希和位运算等,这里只实现两种,排序和位运算
解法1 排序:先对数组进行排序,然后创建一个新数组。通过依次对比两个数组中的元素,就能找出原数组中缺失的值并返回。需要特别留意的是,当第一个循环结束后,这表明原数组前面的数字都没有缺失,那么缺失的就只会是最后一位数字。
class Solution {
public int missingNumber(int[] nums) {
Arrays.sort(nums);
int[] tmp = new int[nums.length+1];
for(int i=0; i<nums.length; i++){
tmp[i] = i;
//不相等即表示找到该数 直接返回
if(tmp[i] != nums[i]) return i;
}
//当到达这步,说明丢失的是最后一位数字,直接返回nums最后元素+1;
return nums[nums.length-1]+1;
}
}
第二种解法运用位运算的特性。由于位运算具备交换律,我们能够先将原数组中的所有元素进行异或操作。接着,构建一个循环,对从0到n(n为数组长度)的数字逐一遍历并执行异或运算。在异或运算规则下,相同的数字相互异或后结果为0,也就是会相互抵消 。经过这两轮异或操作后,最终得到的结果便是原数组中缺失的那个数字。
class Solution {
public int missingNumber(int[] nums) {
int size = nums.length;
int ret = 0;
//先拿ret异或数组中的所有元素
for(int i = 0; i < size;i++){
ret ^= nums[i];
}
//拿ret异或0到n之间所有的元素,异或满足交换律
for(int i = 0; i <= size;i++){
ret ^= i;
}
return ret;
}
}
提莫攻击
在处理中毒时间的计算时,采用模拟的思路即可。当判断当前中毒时间与下一次中毒时间的间隔小于中毒持续时间时,表明中毒时间会被重置,此时只需将当前间隔的差值累加起来。而当这个间隔大于中毒持续时间,意味着这段时间内中毒时间不会重置,那就需要把完整的中毒持续时间累加进去。在完成整个循环的遍历后,不要忘记将最后一次中毒的持续时间也全部加上,以此确保计算结果的准确性。
class Solution {
public int findPoisonedDuration(int[] timeSeries, int duration) {
int ret = 0; //返回值
for(int i=0; i < timeSeries.length-1; i++){
int d = timeSeries[i+1] - timeSeries[i];
//当前和后一次攻击的差值小于中毒时间时说明后面会重置,所以只累加差值
if(d < duration){
ret += d;
} else {
//当前和后一次攻击的差值大于中毒时间,不会重置应该把中毒时间全都计算上
ret += duration;
}
}
//最后一个中毒时间没有重置机会了,所以要全都累加上中毒时间
return ret + duration;
}
}
排序数组
采用传统的快排发现如果有大量重复元素时,时间复杂度会退化成0(N^2),会超时:
class Solution {
public int[] sortArray(int[] nums) {
sort(0,nums.length-1,nums);
return nums;
}
//排序方法
public void sort(int left,int right,int[] nums){
if(left >= right) return;
int prev = left, cur = left + 1;
//采用快排,左边的数比关键值小,右边都是大于等于关键值的
while(cur <= right){
if(nums[cur] < nums[left] && ++prev != cur){
swap(cur,prev,nums);
}
cur++;
}
swap(left,prev,nums);
sort(left,prev-1,nums);
sort(prev+1,right,nums);
}
//交换方法
public void swap(int a,int b,int[] nums){
int tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}
}
在排序过程中,我们采用三个指针来实现高效的数组分区。首先定义指针 i ,用于扫描待排序区间。通过 i 的移动,整个区间划分为四个部分:
- 区间 [l, left] :此区间内的所有元素均小于基准值 key 。
- 区间 [left + 1, i - 1] :该区间存放的元素都等于基准值 key 。
- 区间 [i, right - 1] :此为尚未扫描的区域,元素的大小关系暂未确定。
- 区间 [right, r] :这个区间里的元素都大于基准值 key 。
在 i 扫描过程中,根据当前元素与 key 的大小关系执行不同操作:
- 若扫描到的元素小于 key ,则将其与 left 指针指向的下一个元素交换。因为交换过来的是已扫描过的元素,所以无需再次扫描,直接让 i 后移一位(即 i++ )。
- 扫描到的元素等于 key ,说明该元素已在正确位置,直接让 i 后移一位(即 i++ )。
- 若扫描到的元素大于 key ,则将其与 right 指针指向的前一个未扫描元素交换。由于交换过来的是未扫描元素,因此 i 不能后移,需继续扫描当前位置 。
当 i 完成对整个区间的扫描后, [left + 1, right - 1] 区间内的元素都等于 key ,已然有序,无需递归处理。此时,只需分别针对 [l, left] 和 [right, r] 这两个区间,重复上述的扫描和分区操作,直至整个数组有序。
做该题时候可以先看一下基础算法题1的颜色划分,再进行做题
class Solution {
public int[] sortArray(int[] nums) {
sort(0,nums.length-1,nums);
return nums;
}
public void sort(int l,int r,int[] nums){
if(l >= r) return;
//将数组分成三块 选择基准值
int key = nums[new Random().nextInt(r - l + 1) + l];
//[l,left]都为小于key [left+1,right-1]都为未扫描的 [right,r]都为大于key的
int left = l - 1, right = r + 1 ,i = l;
//使用i遍历未扫描过的元素
while(i < right){
//如果当前元素小于key则和left下一个交换,由于交换的是扫描过的,所以i++
//如果为等于key则直接i++
//如果为大于key的则和未扫描的元素进行交换,i不进行+1
if(nums[i] < key) swap(i++,++left,nums);
else if(nums[i] == key) i++;
else swap(--right,i,nums);
}
//当走到这里时数组被分成了三组 中间都是等于key的则不用继续递归
//[l,left],[left+1,right-1],[right,r]
sort(l,left,nums);
sort(right,r,nums);
}
public void swap(int a,int b,int[] nums){
int tmp = nums[a];
nums[a] = nums[b];
nums[b] = tmp;
}
}
交易逆序对的总数
LCR 170. 交易逆序对的总数 - 力扣(LeetCode)
交易逆序对的总数就是寻找数组中后面的数字比当前的数字小的数量,后面只要有元素比当前元素小就可以组成一个逆序对,直接暴力解法两个循环直接求就好,但是超时
计算数组中逆序对的总数,本质上是要找出数组里满足这样条件的组合数量:对于数组中的每一个数字,在它后面只要存在比它数值小的数字,那么这两个数字就能构成一个逆序对。如果采用暴力求解的方法,只需使用两层循环遍历数组,外层循环选定一个数字,内层循环遍历该数字后面的所有数字并进行比较,从而统计出逆序对的数量。但是会超时
class Solution {
public int reversePairs(int[] record) {
int count = 0;
for(int i = 0; i < record.length; i++){
for(int j = i+1; j < record.length; j++){
if(record[i] > record[j]) count++;
}
}
return count;
}
}
解法二:将一个数组分成左半部分和右半部分,首先把左半部分的逆序对挑出,再把右半部分的逆序对挑出,然后从左右分别挑出,这个肯定是可行的,因为这里就和暴力解法是一样的 ;
那也就可以挑出左半部分,再挑出右半部分,然后挑完之后分别排序,再进行一左一右的挑,这种也肯定是可行的,因为本身就不影响结果,我只需要知道我从左半部分挑出这个数,在右半部分有没有比我小的就好,根本不管你右本部分有没有序,如果固定右半部分的数,只需要知道在左半部分有多少个比我大既可以,所以就可以使用归并排序;
用归并排序的好处就是当两个有序区间进行归并时,如果按照升序,我cur1区间只要比cur2区间的某个数大,那就找到了在某个数之前有多少个数比他大的,只要拿边界减去当前cur1再加一就好,然后让cur2++,再进行寻找下一个数之前比较大的数,然后把cur2放到临时数组中,如果cur1比cur2小就直接把cur1直接放到临时数组中,循环完要把临时数组拷贝回去原数组,也可以采用降序寻找在该数之后有多少个比较小的
通过将一个数组划分为左半部分和右半部分,来有效计算数组中的逆序对数量。具体思路如下:
首先,分别处理左半部分和右半部分数组,找出各自内部的逆序对。这一过程本质上与暴力解法类似,只不过是将数组拆分成两个较小的子数组分别处理,从而降低了计算量。
接着,对左右两部分数组分别进行排序,然后再进行一左一右的配对比较。因为在计算逆序对时,我们只关注对于左半部分数组中的任意一个数,在右半部分数组中是否存在比它小的数,而并不关心右半部分数组自身的顺序。同样地,对于右半部分数组中的数,我们只需确定在左半部分数组中有多少个数比它大即可。基于这一原理,我们可以采用归并排序来实现这一过程。
归并排序的优势在于,当对两个有序区间进行归并时:
- 升序情况:若左区间( cur1 所指向的区间)中的某个数大于右区间( cur2 所指向的区间)中的某个数,那么我们就找到了在 cur2 所指的这个数之前,左区间中有多少个数比它大。此时,只需用左区间的边界索引减去当前 cur1 的索引,再加 1,就可得到相应的逆序对数量。然后,将 cur2 向后移动一位( cur2++ ),继续寻找下一个数之前比它大的数,并将 cur2 指向的数存入临时数组中。如果 cur1 指向的数小于 cur2 指向的数,则直接将 cur1 指向的数存入临时数组。循环结束后,将临时数组的内容拷贝回原数组。
- 降序情况:当采用降序归并排序时,若 cur1 区间的某个数小于 cur2 区间的某个数,那么就意味着找到了在 cur2 所指的这个数之后,左区间中有多少个数比它小。此时,同样可以通过特定的计算方式(比如用左区间当前索引减去起始索引)得到逆序对数量。接着,将 cur2 向后移动一位( cur2++ ),继续寻找下一个数之后比它小的数,并将 cur2 指向的数存入临时数组中。如果 cur1 指向的数大于 cur2 指向的数,则直接将 cur1 指向的数存入临时数组。最终,在循环结束后,也要将临时数组的内容拷贝回原数组。
策略一:找出该数之前,有多少个数比较大,采用归并升序
class Solution {
int[] tmp;
public int reversePairs(int[] record) {
tmp = new int[record.length];
return mergeSore(record,0,record.length-1);
}
public int mergeSore(int[] nums,int left, int right){
//如果区间只有一共一个元素,或者一个都没有肯定没有逆序对
if(left>=right) return 0;
int mid = left + (right - left) / 2;
int ret = 0; //记录本层得到多少逆序对
//左区间逆序对个数 + 排序 + 右区间逆序对个数 + 排序
ret += mergeSore(nums,left,mid);
ret += mergeSore(nums,mid+1,right);
//当求完左区间和右区间 再进行求一左一右的个数
int cur1 = left, cur2 = mid+1,index=0;
while(cur1 <= mid && cur2 <= right){
//升序
if(nums[cur1] <= nums[cur2]){
tmp[index++] = nums[cur1++];
}else {
//当nums[cur1]>nums[cur2]时 找到一批逆序对 因为curl本来就是升序,我都比你大
// 之后的数也肯定比你大,所以只需要统计curl到mid之间的元素即可
ret += mid - cur1 + 1;
tmp[index++] = nums[cur2++];
}
}
while(cur1 <= mid) tmp[index++] = nums[cur1++];
while(cur2 <= right) tmp[index++] = nums[cur2++];
//将tmp中的数拷贝回原数组
for(int i = left; i <= right; i++){
nums[i] = tmp[i - left];
}
return ret;
}
}
策略二:找出该数之后,有多少个数比较小,采用归并逆序
//只有while循环中和升序不一样,其他代码都相同
while(cur1 <= mid && cur2 <= right){
//逆序
if(nums[cur1] <= nums[cur2]){
tmp[index++] = nums[cur2++];
}else {
//当nums[cur1]>nums[cur2]时 因为是逆序,所以下标curl处的值都比cur2处的值大了,那肯定也比cur2后面的值大,统计一下cur2后面有多少元素即可
ret += right - cur2 + 1;
tmp[index++] = nums[cur1++];
}
}
两两交换链表中的节点
解法一:利用循环,首先为了1不进行判断为空情况创建一个头节点,然后在进循环之前创建一个指针,指向要当前要进行交换的两个指针第一个指针
为了避免对链表头节点进行特殊的空值判断,首先创建一个头节点。这个头节点就像是链表的“守护者”,它不存储实际的数据,简化后续的逻辑处理。
在进入循环之前,创建一个指针。这个指针的作用非常关键,它将指向当前要进行交换的两个指针中的第一个指针。可以把这个指针想象成一个“定位器”,帮助我们精准地找到需要交换的节点位置。
当进入循环后,分别获取这两个待交换的指针,也就是交换的第一个指针和第二个指针。
在交换过程中,有一个关键的步骤千万不能忽略,那就是一定要把prev指向交换后的第一个指针,即 prev.next = end 这一步。。例如,假设有一个链表,它的结构是 1 -> 2 -> 3 -> 4 ,现在我们要交换 2 和 1 。如果我们没有执行 prev.next = second 这一步,那么 prev.next 仍然会指向 1 。交换完成后,链表在表面上看起来好像变成了 1 -> 3 -> 2 -> 4 ,但实际上,由于 prev.next 没有正确更新, prev (也就是虚拟头节点)并没有正确地指向交换后的新链表结构。这样一来,链表实际上从 1 开始,丢失了 2 ,最终的结果就变成了 1 -> 3 -> 4 ,链表发生了断裂,节点 2 就这样“消失”了。
在完成指针的获取和检查之后,按照正确的顺序进行节点交换即可。
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode parent = new ListNode();
parent.next = head;
//prev下一个都是要修改的指针
ListNode prev = parent;
while(prev.next != null && prev.next.next != null){
//first指向第一个要交换的节点
ListNode first = prev.next;
//指向第二个要交换的节点
ListNode end = first.next;
//进行交换
//前一个指针必须先指向修改后的第一个指针
prev.next = end;
first.next = end.next;
end.next = first;
//修正prev
prev = first;
}
return parent.next;
}
}
解法二:在链表节点交换的递归实现里,关键的第一步是进行有效性判断,确认待交换的两个指针的状态,只要其中任何一个指针为空,就意味着没有足够的节点可供交换,此时直接返回当前链表即可。避免因空指针而引发错误。
确认可以进行交换后,通过递归调用获取交换完成后的链表的第一个指针。这个递归过程会深入链表,不断地对后续节点进行交换操作,直到到达链表的末端或者不满足交换条件的位置。递归返回的指针,是后续已经完成交换的链表的起始点。
最后,基于获取到的指针,按照既定的逻辑进行当前这两个节点的交换操作。调整指针指向,完成节点顺序的互换,让链表的结构按照预期发生改变。
class Solution {
public ListNode swapPairs(ListNode head) {
//要交换指针 只要有一个为空就进行返回
if(head == null || head.next == null){
return head;
}
//获取后面交换指针的第一个位置
ListNode tmp = swapPairs(head.next.next);
//进行交换逻辑
ListNode ret = head.next;
ret.next = head;
head.next = tmp;
return ret;
}
}
判断是否互为字符重排
面试题 01.02. 判定是否互为字符重排 - 力扣(LeetCode)
在解决这道题时,由于枚举法会导致时间复杂度极高,出现超时问题,所以需要转换思路。实际上,这道题的关键在于判断字符串 s1 和 s2 中各字符的出现次数是否一致。
可以借助哈希表来实现这个判断过程。虽然使用常规的哈希容器能够解决问题,但在比较和操作上可能会较为繁琐。所以选择使用数组来模拟哈希表,这种方式更为简洁高效。具体实现步骤如下:
1. 首先,创建一个数组来模拟哈希表。在遍历字符串 s1 时,统计每个字符的出现次数。数组的下标用于表示字符,数组元素则记录对应字符的出现次数。例如,若字符 a 出现了3次,那么数组中对应下标的元素值就为3。
2. 接着,在遍历第二个字符串 s2 时,对于 s2 中的每个字符,将数组中对应下标的元素值减1。每次减1后,都要检查该元素值是否变为负数。一旦出现负数,就表明这个字符在字符串 s1 中出现的次数小于在 s2 中出现的次数,也就意味着 s1 中不存在足够数量的该字符,这两个字符串不匹配。
3. 如果顺利遍历完整个字符串 s2 ,且数组中所有元素值都未出现负数,那就说明两个字符串中各字符的出现次数完全相同,它们是匹配的,此时直接返回 true 。
class Solution {
public boolean CheckPermutation(String s1, String s2) {
//如果长度不相同直接返回false
if(s1.length() != s2.length()) return false;
//使用数组模拟哈希表
int[] hash = new int[26];
//先把第一个字符串信息统计到哈希表中
for(int i=0; i < s1.length(); i++){
hash[s1.charAt(i) - 97]++;
}
//遍历第二个字符串
for(int i=0; i < s1.length(); i++){
//如果hash中的值为负数说明第一个字符串中没有这样的元素 直接false
if(--hash[s2.charAt(i) - 97] < 0) return false;
}
return true;
}
}

3441

被折叠的 条评论
为什么被折叠?



