- 快乐数
- 四数相加
- 三数之和
- 赎金信
- 四数之和
快乐数
关于一些题目的疑问:每位数求平方和到底会不会导致数越来越大。答案是不会的。一开始我个人还总感觉会。
直接看到这个图。就懂了。
所以说,当你的数越来越大。求平方和反而会变小。所以这也是有可能出现循环的原因。
所以快乐数,给你一个数不断的求其平方和,是有可能出现下图中的环的可能。
思路:
按题目的思路进行计算,但是最关键的还是题目中的四个字:无限循环。
按题中的意思得到算法思路:
解法1:
输入的数字为n。
从n中,取出每一位求平方和,得到一个新数值。现在就相当于n转变成了这个新数值。然后继续对这个数字进行相同的处理。重复这个过程,直到n=1或者判断出有循环出现的数。也就是这个图:
如何快速判断出有循环出现的数?用哈希表。在每次得到新数值的时候,就把这个数字加入哈希表。之后每次得到新数字的时候进行判断即可,判断用containsKey()就行。
写的技巧:
1.之前每得到一个新数字我老不想改变原输入数字,以后在这种循环迭代,有值变换迁移的,就可以直接用这个输入的数字进行迭代,这个n我纠结了好久。
2.以题目的条件作为终止循环条件也是一种技巧。
class Solution {
public boolean isHappy(int n) {
HashMap<Integer,Boolean> hmap = new HashMap<>(); //创建哈希表
while(n!=1 && !hmap.containsKey(n)){//终止条件:要么n最终变为了1,要么n变为已经出现过的数字,此时判断有无限循环。
hmap.put(n,true);//新元素加入哈希表,记录出现过
int sum = 0; //计算每一位的求和
while(n!=0){
int digit = n%10; //取最后一位
sum += digit * digit; //求平方然后加起来
n/=10; //把最后一位舍掉
}
n = sum; //迭代新的n的值
}
//跳出循环的时候n要么是重复数字,要么是1.所以是1就是true,重复数字就是false。
return n == 1;
}
}
解法2:
看到环,难免就会想到一个做法:判断环的方法:快慢指针。
这个解法容易想,但是难就难在这不是链表。所以想完成个快慢指针,一定要模拟出链表的感觉。
这个解法我推荐看代码来理解:本质上就是跑步套圈的原理。用的类似快慢指针的思想。慢的一次走一步,快的一次走两步。慢的起点从n开始。快的起点从n的下一个状态。就是getNext(n)开始。
通过二者不断的在循环中调用getNext(n)一直往后面走。最终要么当fast为1,要么slow为1,要么slow的值等于fast的值然后停下来。
class Solution {
public int getNext(int n) { //这个函数平方和。也就是n的下一个状态。
int totalSum = 0;
while (n > 0) { //当n不为0时,表示每位数还没有取完。
int d = n % 10; //取最后一位
n = n / 10; //除10舍掉最后一位
totalSum += d * d; //计算最后一位的平方和进行求和
}
return totalSum; //最后返回所有位的平方和
}
public boolean isHappy(int n) {
int slowRunner = n; //slow起始在n
int fastRunner = getNext(n); //fast起始在n的下一个状态。
while (fastRunner != 1 && slowRunner!=1 && slowRunner != fastRunner) { //开始往后面走,由于fast一次走两步也可能错过1。所以fast和slow都要进行判断。
slowRunner = getNext(slowRunner); //slow每次走一步
fastRunner = getNext(getNext(fastRunner)); //fast每次走两步,这里就是往后计算两次状态,然后赋值,就完成了走两步
}
return fastRunner == 1; //一旦上面的终止条件之一出现,那么就进行判断,等于1那就返回true,不等于1那就说明碰到环了,返回false。
}
}
四数相加
自己做的版本:
思路:四个数组转两个数组,优化到O(n^2)。并且和两数之和的思想一样,用Hash进行查找优化。先二重for循环并用hash表统计前两个数组中的所有相加的结果。后面两个数组,也是二重for循环计算后两个数组中相加的所有结果,在这个过程中,取相加结果的负数,去哈希表中找有没有满足相加为0的组合。有就把对应的hash的value值累加了。
并且题目的意思是统计个数,所以哈希表:
HashMap<Integer,Integer> key为前两个数相加后的值,value为该值出现的次数。后面查找哈希表的时候,只需把value加上就完成了次数的累加。
class Solution {
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
HashMap<Integer,Integer> hmap1 = new HashMap<>();
//哈希表,用于存储前两个数组构建的相加后的数值统计。
int count = 0; //存储结果。
for(int i = 0;i<nums1.length;i++){
for(int j = 0;j<nums2.length;j++){
int sum1 = nums1[i]+nums2[j]; //目标就是构建相加后的数的哈希表。
if(hmap1.containsKey(sum1)){//先判断这个元素本身哈希表有没有
int value = hmap1.get(sum1);//有就把原来的值取出来。然后再put一个value+1进去。
hmap1.put(sum1,value+1);
}else{
hmap1.put(sum1,1);//如果本身这个哈希表就没有,那就put一个1进去。
}
}
}
//后两个数组元素进行相加组合,然后求哈希表中有没有与之相加后为0的元素
for(int i = 0;i<nums3.length;i++){
for(int j = 0;j<nums4.length;j++){
int sum = nums3[i]+nums4[j];
int target = -sum; //找相加后为0的目标元素
if(hmap1.containsKey(target)){ //去哈希表里面找有没有
count += hmap1.get(target); //有就取出来,然后统计次数累加。没有就无事发生。
}
}
}
return count; //返回结果。
}
}
这个题相当于练习哈希表的使用了。
1.注意HashMap没有默认初始化,所以如果有一个key,它本身不在哈希表中,直接访问就报空指针异常。这个时候想处理就只能提前用containsKey判断元素是否在表中,然后再进行后续的处理。比如把这个值put进去。
2.HashMap想改变value的值。只能够通过put操作,put操作是覆盖。要想实现value值+1,那就先通过get操作得到value,然后把value+1再put进去。
三数之和
这个题,难点在判重。所以重点学会判重。
解法:双指针。
具体看这个图,就懂怎么做了。
首先明确一个点,这些操作都是利用排序好的基础上完成的。所以第一步应该是排序。
i在最外层循环,然后每轮循环中先固定i,然后后面进行双指针操作。left指向i+1,right指向nums.length-1。三数之和是三个数字相加等于0,这个时候就可以模拟出一个结果。当求和结果过大的时候,right应该往右移动一格,这样结果就能减小。当求和结果过小的时候,left往右移动一格,这样结果就能够增大。这样就不断能向求和结果等于0靠近。
剪枝操作:由于是排过序了,所以当nums[i]>0的时候,后面就不存在结果集了。后面的都比nums[i]大。
去重操作:最怕的就是数组越界问题。
所以对于i的判重:必须要i>0,即在第一格的时候不用判重,所以就可以放心的写,nums[i]==nums[i-1]的时候,就可以continue进入下一轮。
left的判重,left和left+1位置的元素比较,这样比较简单,主要也是担心数组越界的问题,所以left判重就要多一个条件,left<right。
right的判重同理,right与right-1进行比较。这样比较简单,加一个left<right就可以解决数组越界的问题。
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);//排序
for(int i = 0;i<nums.length-2;i++){//右边不足两个元素的时候就可以停了
if(nums[i] > 0){ //剪枝,一旦第一个元素都大于0了,那后面的也必要进行了,因为是有序数组
return result;
}
if(i > 0 && nums[i]==nums[i-1]){
continue; //剪枝。第一个位置就没必要剪枝,从第二个位置起才需要进行剪枝。而且这样和前面比也不用担心空指针异常。
}
//双指针逻辑
int left = i+1; //left从i+1开始
int right = nums.length-1; //right从数组末尾开始。
while(left < right){ //left和right不碰到一起即可。注意千万不能取等,取等可能会导致多出一组结果,比如-4,2,2。如果left和right同时指向同一个元素,而且值为2,那就有可能把这个错误的结果加入结果集。
int sum = nums[i]+nums[left]+nums[right]; //求和
if(sum<0){
left++; //值小了,left右移,使得值大一点
}else if(sum >0){
right--; //值大了,right往左,使得值小一点。
}else{
//接入结果集
result.add(Arrays.asList(nums[i],nums[left],nums[right]));
while(left<right && nums[right]==nums[right-1]){ //left判重
right--;
}
while(left<right && nums[left]==nums[left+1]){ //right判重
left++;
}
//由于left是和右边一个比较,所以上面不断移动的后果是left移动到最右边的一个重复数字,但这个数字仍然是收集过的,所以就应该left再往右边移动一格。这个++完全不用担心越界,因为一旦越界根本下一个循环都进不去。下面的right同理。
right--;
left++;
}
}
}
return result;
}
}
赎金信
还是比较简单,数组模拟哈希直接解决战斗。
class Solution {
public boolean canConstruct(String ransomNote, String magazine) {
int[] num = new int[26]; //哈希,用于统计magazine里每个字符的个数
for(char x : magazine.toCharArray()){//增强for遍历字符串。但是这里用toCharArray()将字符串转为字符数组。
num[x-'a']++; //统计字符
}
for(char x : ransomNote.toCharArray()){//增强for遍历同理
num[x-'a']--; //用哈希表去做减法。
if(num[x-'a']<0){ //一旦有某个字符的值小于0,说明字符不够用了
return false; //所以返回false。
}
}
return true; //如果能走到这,那就返回true。
}
}
四数之和
这个题就是在三数之和的基础上边一下
在外面再给他套一层k,但是本题也有本题的特点,由于题意改成四数相加之和为target。所以本题的剪枝和就会产生变化。我觉得这是本题最大的变化。
这里重点就看看剪枝:
为什么剪枝会是nums[k]>target && target>=0。首先target有正有负。光写nums[k]>target就忘记考虑一些情况:看这个例子,-4 -2 0 0 ,target =-6。如果k指向-4大于-6这里直接return了,不就少了这组结果。
所以这是忽略了负数+负数会变得更小 。所以之前这种情况,是在满足target>0的情况才能这样剪枝。
class Solution {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>(); //存结果集
Arrays.sort(nums); //排序
for(int k = 0;k<nums.length-3;k++){ //这里到后面不足三个就可以停了
if(nums[k]>target && target>=0){ //剪枝。上面解释过了
return result;
}
if(k>0 && nums[k]==nums[k-1]){ //这里是k的判重,和三数之和的i的判重逻辑逻辑一样
continue; //这里千万别去在k++了,因为k是最外层,默认有++
}
for(int i=k+1;i<nums.length-2;i++){ //这里到后面不足两个就停止。
if(i>k+1 && nums[i]==nums[i-1]){//i的判重。这里能想到i>k+1很重要,这个和上面k>0性质是一样,不能在这一轮i的起点,才能进行判重。
continue;
}
//下面的逻辑纯粹就是三数之和
int left = i+1; //left取在i+1
int right = nums.length-1; //right取在数组末尾
while(left<right){ //这里就是个双指针的流程
long sum = (long)nums[k]+nums[i]+nums[left]+nums[right]; //这里必须要弄个long。因为四个int相加有可能发生上移除。所以要用long来转,这里只要第一个转long即可,后面的计算从左到右都会进行自动类型转换。
if(sum >target){ //值大了。那就right往左走,调小一点
right--;
}else if(sum<target){ //值小了,left往右边走,调大一点
left++;
}else{//相等的逻辑
result.add(Arrays.asList(nums[k],nums[i],nums[left],nums[right])); //先结果收集
while(left<right && nums[left]==nums[left+1]){ //left判重
left++;
}
while(left<right && nums[right]==nums[right-1]){ //right判重
right--;
}
left++;//由于left是和右边一个比较,所以上面不断移动的后果是left移动到最右边的一个重复数字,但这个数字仍然是收集过的,所以就应该left再往右边移动一格。这个++完全不用担心越界,因为一旦越界根本下一个循环都进不去。下面的right同理。
right--;
}
}
}
}
return result; //返回结果集。
}
}