前言
Leetcode202/1/454/15/18
一、202题(欢乐数)
题目描述:
编写一个算法来判断一个数 n 是不是快乐数。
快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
如果 n 是 快乐数 就返回 true ;不是,则返回 false 。
示例 1:
输入:n = 19 输出:true
解释:
12 + 92 = 82;82 + 22 = 68;62 + 82 = 100;12 + 02 + 02 = 1
示例 2:
输入:n = 2 输出:false
提示:1 <= n <= 231 - 1
题解1
class Solution {
public boolean isHappy(int n) {
//快乐数:各位置平方和为1
//无论是什么数字,过程都是一个循环单链表,类似环形链表那题
//如果各位数字之和为1,则循环点就是1
//如果各位数字之和永远不为1,则循环点是非1的数字
int slow = n;
int fast = step(n);
while(slow!=fast){
slow = step(slow);//每次走一格
fast = step(step(fast));//每次走两格
}
return slow==1;
}
// 写一个方法返回n的各位数字之和
public int step(int n){
int ans = 0;
int x;
while(n!=0){
x = n%10;
ans += x*x;
n = n/10;
}
return ans;
}
}
算法思路:
-
2
31
−
1
2^{31}-1
231−1是一个10位数,如果n是10个9,各位值的平方和数最大是81*10=810,也就是说,平方数一定位于1~810,所以最多操作810次,之后就会进入一个循环点。那么快乐数和非快乐数的区别在于,循环点是否为1。
解释:快乐数的循环一定为1,比如19这个数变成100之后,就会一直是1,平方和一直是1,所以循环点是1
- 非快乐数的循环点是非1数,比如2:2->4->16->37->58->89->145->42->20->4,循环点是4。这一连串的数字可以看成是一个循环单链表,那么这题分析到这里,就和第142题环形链表思路相同了。
- 使用双指针,fast比slow指针多走一格。所以先要实现一个方法,step(n),表示下一步的节点值。那么slow走一格就是slow=step(slow),而fast走两步就是fast=step(step(fast))。剩下的就和第142题一样了。
题解2(哈希表)
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了(来自卡哥语录)
class Solution {
public boolean isHappy(int n) {
Set<Integer> record = new HashSet<>();
while (n != 1 && !record.contains(n)) {
record.add(n);
n = getNextNumber(n);
}
return n == 1;
}
private int getNextNumber(int n) {
int res = 0;
while (n > 0) {
int temp = n % 10;
res += temp * temp;
n = n / 10;
}
return res;
}
}
算法思路:这个就是利用“不断进行平方和替换原有数操作”必然出现循环节点,用set来存这个过程中的平方数。当出现某个数在set中已经包含时,退出循环。如果该循环节点是1,则是快乐数,否则不是。
二、1题(两数之和)
题目描述:
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
mine:击败5.06%用户,,ԾㅂԾ,,
该解法问题比较大,已在本题结尾的注释中讲明原因,请移步解法2
class Solution {
public int[] twoSum(int[] nums, int target) {
// 先把数组中的元素变成map
HashMap<Integer,Integer> result = new HashMap<>();
int minnum = Integer.MAX_VALUE;
int maxnum = Integer.MIN_VALUE;
if(target%2!=0){
for(int i=0;i<nums.length;i++){
minnum = minnum<nums[i]?minnum:nums[i];
maxnum = maxnum>nums[i]?maxnum:nums[i];
result.put(nums[i], i);
}
}
else{
//特殊判断target为偶数且数组中含两个target/2的情况
for(int i=0;i<nums.length;i++){
if(nums[i]==target/2&&result.containsKey(nums[i])){
return new int[]{result.get(target/2),i};
}
minnum = minnum<nums[i]?minnum:nums[i];
maxnum = maxnum>nums[i]?maxnum:nums[i];
result.put(nums[i], i);
}
}
// 然后分解target
for(int i=minnum;i<=maxnum&&i<=target/2;i++){
if(result.containsKey(i)&&result.containsKey(target-i)){
return new int[]{result.get(i),result.get(target-i)};
}
}
return null;
}
}
算法思路:把数组变成<value,index>的HashMap,并找到数组的最小值和最大值。在最小值到max(最大值,target/2)之间遍历i,同时查询HashMap是否同时含有(i,target-i),如果同时含有就返回他们的键码值即可。
易错点:如果target为偶数,可能会出现两个值为target/2的数,比如[3,3],target为6,应该返回[0,1],却返回了[1,1]。所以需要特数处理,如果target是偶数,如果map中已经有了一个target/2,则再次出现target/2时,直接返回[result.get(target/2),current_i]。
题解2
讲解
题解1思路的优化版本
public int[] twoSum(int[] nums, int target) {
int[] res = new int[2];
if(nums == null || nums.length == 0){
return res;
}
Map<Integer, Integer> result = new HashMap<>();
for(int i = 0; i < nums.length; i++){
int temp = target - nums[i]; // 遍历当前元素,并在map中寻找是否有匹配的key
if(result.containsKey(temp)){
res[1] = i;
res[0] = result.get(temp);
break;
}
result.put(nums[i], i); // 如果没找到匹配对,就把访问过的元素和下标加入到map中
}
return res;
}
算法思路:同样也是使用HashMap的数据结构。遍历数组,边遍历边讲数组以<value,index>的格式加入map中。假设当前正在遍历数组的第i个元素,需要判断:是否存在key值为(target-i)的元素,如果存在则返回ans=[result.get(target-i),i]。
注释:这个思路就更为清晰,省去了判断两个target/2在数组中的情况。没想到这个思路的原因在于,一看到这个题我的第一反应是O(
n
2
n^2
n2)的暴力解法,之后才思考用map求解,但还是带了一些暴力解法的思想,虽然复杂度比O(
n
2
n^2
n2)要低,但是如果数组是[-1e9,1e9],target是0,我的复杂度是2e9,暴力解法是4,这样看来解法1问题很大。
三、454题(四数相加)
题目描述:
给你四个整数数组 nums1、nums2、nums3 和 nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:
0 <= i, j, k, l < n
nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
int res = 0;
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
//统计两个数组中的元素之和,同时统计出现的次数,放入map
for (int a : nums1) {
for (int b : nums2) {
map.put(a+b, map.getOrDefault(a+b, 0) + 1);
}
}
//统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数
for (int c : nums3) {
for (int d : nums4) {
res += map.getOrDefault(-(c+d), 0);
}
}
return res;
}
}
算法思路:
- a+b+c+d,即a+b=-(c+d)。由于只需要统计出现的次数,所以可以遍历(a,b),用map<key,value>记录,其中key存(a+b),value存key出现的次数。
- 再遍历(c,d),在map中找是否存在-(c+d)即可。
四、15题(三数之和)
题目描述:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
双指针法
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
// 找出a + b + c = 0
// a = nums[i], b = nums[left], c = nums[right]
for (int i = 0; i < nums.length; i++) {
// 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
if (nums[i] > 0) {
return result;
}
if (i > 0 && nums[i] == nums[i - 1]) { // 去重a
continue;
}
int left = i + 1;
int right = nums.length - 1;
while (right > left) {
int sum = nums[i] + nums[left] + nums[right];
if (sum > 0) {
right--;
} else if (sum < 0) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 去重逻辑应该放在找到一个三元组之后,对b 和 c去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
right--;
left++;
}
}
}
return result;
}
}
算法思路:双指针
- [i,left,right]是三元组,i用来遍历数组,固定为三元组的第一个元素。[left,right]是双指针,寻找三元组的第二、三位元素。
- 对数组从小到大排序,left=i+1,right=nums.length-1。当[i,left,right]和小于0时,left++,大于0时right–,直到left==right。
- 由于题目要求三元组不能重复,如nums[] = [-1,0,1,2,-1,-4],可以是[-1,0,1],也可以是[0,1,-1],但这两个重复了。所以这题最重要的,也是最难的点在于如何去除重复三元组
去重:
- i遍历的去重:如果nums[i] == nums[i+1],则continue循环。
解释:如[-1,-1,2,3],当i=0时,我们有这些三元组[-1,-1,2],[-1,-1,3],[-1,2,3]
当i=1时,我们有三元组[-1,2,3]
所以如果nums[i] = nums[i-1],那么这次找到的三元组一定是包含在上一次找到的三元组里的。
- [left,right]去重:如果nums[left+1] = nums[left]或者nums[right] = nums[right-1]就要continue循环。
解释:因为想找nums[left]+nums[right] = -nums[i]的[left,right]二元组,
只要有一个元素相同,那么就会出现重复三元组,
因为另一个元素必定也相同才满足和为-nums[i]。
比如[-5,2,2,3,3],nums[0]=5,left=1,right=4。
left要移到2,right要移到3才能做到完全去重。
五、18题(四数之和)
题目描述:
给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):
0 <= a, b, c, d < n
a、b、c 和 d 互不相同
nums[a] + nums[b] + nums[c] + nums[d] == target
你可以按 任意顺序 返回答案 。
和第15题思路相同
题解1
mine
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> ans = new ArrayList<List<Integer>>();
//[i,j,left,right] 循环遍历(i,j) //双指针[left,right]
int left,right;
for(int i=0;i<=nums.length-4;i++){
//是i-1>=0,因为i从0开始
if(i-1>=0&&nums[i]==nums[i-1]){
continue;
}
//注意是j=i+1,不是j=1
for(int j=i+1;j<=nums.length-3;j++){
if(j-1>i&&nums[j]==nums[j-1]){
continue;
}
left = j+1;
right = nums.length-1;
while(left<right){
if(left-1>j&&nums[left]==nums[left-1]){
left++;
continue;
}
if(right+1<nums.length&&nums[right]==nums[right+1]){
right--;
continue;
}
if((long)nums[i]+nums[j]+nums[left]+nums[right]==target){
List<Integer> list = new ArrayList<Integer>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[left]);
list.add(nums[right]);
ans.add(list);
left++;
right--;
}else if((long)nums[i]+nums[j]+nums[left]+nums[right]<target){
left++;
}else{
right--;
}
}
}
}
return ans;
}
}
易错点:
- 去重:跳出条件,比前不比后。即当前index=j,是比较nums[j]==nums[j-1],而不是nums[j]==nums[j+1]。不然[2,2,2,2,2]target=8时会返回空。
- 去重前提条件:比较nums[j]==nums[j-1],前提条件是要判断j-1是否合法。
- 数字和范围:4个int型数字相加,很有可能大于Integer.MAX_VALUE或小于Integer.MIN_VALUE,所以要转为long类型,如下所示:
(long)nums[i]+nums[j]+nums[left]+nums[right]==target
题解2
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
for (int i = 0; i < nums.length; i++) {
// nums[i] > target 直接返回, 剪枝操作
if (nums[i] > 0 && nums[i] > target) {
return result;
}
if (i > 0 && nums[i - 1] == nums[i]) { // 对nums[i]去重
continue;
}
for (int j = i + 1; j < nums.length; j++) {
if (j > i + 1 && nums[j - 1] == nums[j]) { // 对nums[j]去重
continue;
}
int left = j + 1;
int right = nums.length - 1;
while (right > left) {
// nums[k] + nums[i] + nums[left] + nums[right] > target int会溢出
long sum = (long) nums[i] + nums[j] + nums[left] + nums[right];
if (sum > target) {
right--;
} else if (sum < target) {
left++;
} else {
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
// 对nums[left]和nums[right]去重
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
left++;
right--;
}
}
}
}
return result;
}
}
注释:算法是一样的,只是细节上的区别。
result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
总结
202的关键点是想到这个操作一定有循环。1并不难。454简单。15有难度。掌握第15题后18也就掌握了。