牛客剑指offer:题解(41-50)

欢迎指正

题解(01-10):link

题解(11-20):link

题解(21-30):link

题解(31-40):link

题解(41-50):link

题解(51-60): link

题解(61-67): link


41.和为S的连续正数序列

描述见链接

1.解法一:数学方程

  1. 假设第一个开始的元素是x ,后面有n个连续正数,且满足条件,则可以得到式子x + (x + 1) + (x + 2) +...+(x + n) = sum ,化简一下就是(n + 1)(n + 2 * x) = 2 * sum ,且由于至少包括两个数,那么我们可以得到x <= sum / 2 (例子:9 = 4 + 5) ,即第一个开始的元素应该小于等于目标元素的一半,否则后面就没办法再增加比x大的数了,由此可以减少运算次数。
  2. sum <= 3 包含特殊情况,提前处理
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if (sum <= 2) return res;
        if (sum == 3) {
            ArrayList<Integer> tmp = new ArrayList<>();
            tmp.add(1);
            tmp.add(2);
            res.add(tmp);
            return res;
        }
        for (int x = 1;x <= sum / 2;x ++) {
            for (int n = 1;n < sum / 2;n ++) {
                // 如果满足条件,则加入结果集中
                if (isAns(sum, x, n)) {
                    ArrayList<Integer> tmp = new ArrayList<>();
                    for (int i = 0;i <= n;i ++)
                        tmp.add(x + i);
                    res.add(tmp);
                }
            }
        }
        return res;
    }
    // 根据判断公式判断x,n 是否能够满足条件
    private boolean isAns(int sum, int x, int n) {
        int tmp = (n + 1) * (n + 2 * x);
        return tmp == sum * 2;
    }
}

2.解法二:双指针

  1. 使用双指针,一个指向i,一个指向j = i + 1 开始
  2. j <= (sum + 1) / 2 满足条件。为什么?因为 99 = 44 + 45,保证i = 44,j = (99 + 1) / 2 = 45
  3. i -> j的临时和tmpSum 可以使用高斯公式计算:(首项 + 尾项) * 项数 / 2
  4. 如果tmpSum < sum,那么不够大,于是增加 j
  5. 如果tmpSum > sum ,那么不够小,于是增加i
  6. 如果tmpSum = sum,说明满足条件,那么把i -> j 的数存入结果集,且将j++ 进入下一个可能的结果集的判断
public class Solution {
    public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> res = new ArrayList<>();
        if (sum <= 2) return res;
        int i = 1, j = 2;
        while (j <= (sum + 1) / 2) {
            int tmpSum = (i + j) * (j - i + 1) / 2;
            if (tmpSum < sum) j ++;
            else if (tmpSum > sum) i ++;
            else {
                ArrayList<Integer> tmp = new ArrayList<>();
                for (int k = i;k <= j;k ++) {
                    tmp.add(k);
                }
                res.add(tmp);
                j ++;
            }
        }
        return res;
    } 
}

42.和为S的两个数字

描述见链接

1.解法一:双指针

关键在于证明相隔越远的两个数乘积还最小!

证明如下:

  1. 已知 i < j < k < m,且有 i + m = j + k;
  2. 不妨将几个数换一下改为, i < i + n1 < i + n1 + n2 < i + n1 + n2 + n3,其中n为正数
    则可以得到 n1 + n2 + n1 = n1 + n2 + n3 -> 2*n1 + n2 = n1 + n2 + n3
  3. 下面来求证 i * m < j * k
证明:由题意可得
	(i * m) - (j * k)
=	{i * (i + n1 + n2 + n3)} - {(i + n1) * (i + n1 + n2)} // 中间自己化简一下,注意上面的条件2
=	-(n1 + n2) < 0
所以:i * m < j * k

public class Solution {
    public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        // 双指针,距离隔得最远的两个元素的乘积最小,所以一找到一对满足的数就可以返回了
        // 需要考虑是否有重复元素,这儿是递增,就没考虑相同元素
        ArrayList<Integer> res = new ArrayList<>();
        if (array == null || array.length <= 1) return res;
        int i = 0, j = array.length - 1;
        while (i < j) {
            if (array[i] + array[j] == sum) {
                res.add(array[i]);
                res.add(array[j]);
                return res;
            }
            while (i < j && array[i] + array[j] < sum) i ++;
            while (i < j && array[i] + array[j] > sum) j --;
        }
        return res;
    }
}

43.左旋转字符串

描述见链接

1.解法一:使用库函数 substring(int start, int end) 取得 [start, end)的字符串

public class Solution {
    public String LeftRotateString(String str,int n) {
        int N = str.length();
        if (n <= 0 || str == null || N <= 1) return str;
        n = n % N;
        // 取得左边的n位加到右边的末尾
        String s1 = str.substring(0, n);
        String s2 = str.substring(n, N);
        return s2 + s1;
    }
}

2.解法二:

public class Solution {
    public String LeftRotateString(String str,int n) {
        int N = str.length();
        if (n <= 0 || str == null || N <= 1) return str;
        n = n % N;
        // 1 2 3 4 5 6 7 循环左移3位应该是 4 5 6 7 1 2 3
        // 相当于先把整个反转 7 6 5 4 3 2 1
        // 再反转前N - n位   4 5 6 7 3 2 1 反转0 -> (N - n - 1)
        // 再反转后n位       4 5 6 7 1 2 3 反转(N - n) -> (N - 1)
        StringBuilder sb = new StringBuilder(str);
        reverse(sb, 0, N - 1);
        reverse(sb, 0, N - n - 1);
        reverse(sb, N - n, N - 1);
        return sb.toString();
    }
    private void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char tmp = sb.charAt(left);
            sb.setCharAt(left, sb.charAt(right));
            sb.setCharAt(right, tmp);
            left ++;
            right --;
        }
    }
}

44.翻转单词顺序列

描述见链接

1.解法一

import java.lang.StringBuilder;
public class Solution {
    public String ReverseSentence(String str) {
        // 去除字符串前后的空格,要是最后为空,说明整个字符串是一个空字符串
        if (str == null || str.trim().equals("")) return str;
        String[] arr = str.split(" ");
        // 先交换位置,理解起来的话使用StringBuilder也可以从后往前遍历,不用交换
        for (int i = 0;i < arr.length / 2;i ++) {
            String tmp = arr[i];
            arr[i] = arr[arr.length - 1 - i];
            arr[arr.length - 1 - i] = tmp;
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0;i < arr.length;i ++) {
            sb.append(arr[i]);
            if (i != arr.length - 1)
                sb.append(" ");
        }
        return sb.toString();
    }
}

2.解法二:优化解法一,无需反转,只需从后往前遍历即可

import java.lang.StringBuilder;
public class Solution {
    public String ReverseSentence(String str) {
        if (str == null || str.trim().equals("")) return str;
        String[] arr = str.split(" ");
        StringBuilder sb = new StringBuilder();
        // 从后往前遍历,避免交换位置
        for (int i = arr.length - 1;i >= 0;i --) {
            sb.append(arr[i]);
            if (i != 0)
                sb.append(" ");
        }
        return sb.toString();
    }
}

45.扑克牌顺子

描述见链接

1.解法一

public class Solution {
    public boolean isContinuous(int [] numbers) {
        // 一副牌中最多四个0,即四个癞子牌,默认输入的数组长度应该就是5个
        if (numbers == null || numbers.length == 0) return false;
        Arrays.sort(numbers);
        // countZero 记录0的个数,countGap 记录可以插入0的位置
        int countZero = 0, countGap = 0;
        for (int i = 0;i < numbers.length - 1;i ++) {
            if (countZero == 4) return true;
            if (numbers[i] == 0) countZero ++;
            // 如果出现重复不为0的数字,说明不可能排成一个5连顺子
            else if (numbers[i] == numbers[i + 1]) return false;
            else {
                // 记录两个数字之间可以插入0的地方
                countGap += (numbers[i + 1] - numbers[i] - 1);
            }
        }
        // 只有当 countGap <= countZero ,才能够完全插满中间的空位置,形成顺子
        if (countGap <= countZero) return true;
        return false;
    }
}

46.孩子们的游戏(圆圈中最后剩下的数)

描述见链接

1.解法一:使用链表来,模拟

  1. 使用一个链表存储所有的小孩子,然后记录一个需要删除的小孩子的位置记为 removeIndex
  2. 因为每报数m 个小孩子就删掉一个,所以更新removeIndex的公式,还要考虑越界问题,则removeIndex=(removeIndex + m - 1) % list.size() 。删掉一个小孩子后,整个圈就少了一个人,为了避免越界,就要模上list.size()
  3. 当最后链表中只剩一个人的时候,这个人就是最终赢家
import java.util.LinkedList;
public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if (n <= 0 || m <= 0) return -1;
        LinkedList<Integer> list = new LinkedList<>();
        for (int i = 0; i < n; i ++) 
            list.add(i);
        int removeIndex = 0;
        while (list.size() != 1) {
            // 关键在于对这个removeIndex的更新,好好体会一下
            removeIndex = (removeIndex + m - 1) % list.size();
            list.remove(removeIndex);
        }
        return list.get(0);
    }
}

2.解法二:公式法(约瑟夫环问题)

约瑟夫环公式f(n, m) = (f(n - 1, m) + m) % n

n :一共有多少人

m :报数到m就退出

public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if (n <= 0 || m <= 0) return -1;
        int last = 0; // 一个人时的答案
        // 自底向上的递推,先计算出2个人的答案,再往上一直计算n个人的答案
        // i = 1即只有一个人时,就是这个人就是赢家,返回下标为0
        for (int i = 2;i <= n;i ++) {
            last = (last + m) % i;
        }
        return last;
    }
}

3.解法三:约瑟夫环,从上到下递归

f(n, m) = (f(n - 1, m) + m) % n 记住这个转化公式!!!

public class Solution {
    public int LastRemaining_Solution(int n, int m) {
        if (n <= 0 || m <= 0)
            return -1;
        else if (n == 1)	// 如果 n = 1,说明只有一个人,返回这个人的下标 0
            return 0;
        else			   // 公式 f(n, m) = ( f(n - 1, m) + m ) % n
            return (LastRemaining_Solution(n - 1, m) + m) % n;
    }
}

47.求1+2+3+。。。+n

题目描述: 求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

1.解法一:显然可以使用高斯公式(但是题目要求不能使用乘除法)

2.解法二:递归,短路与操作

public class Solution {
    public int Sum_Solution(int n) {
        int sum = n;
        // 前后都应该是返回boolean值
        // sum += Sum_Solution(--n) 每次先将--n后再计算,最后n=-1的时候,由前面短路与操作,不会执行叠加		// 操作。所以不会把-1加进去
        boolean flag = (sum > 0) && ( (sum += Sum_Solution(--n)) > 0 );
        return sum;
    }
}

48.不用加减乘除做加法

题目描述: 写一个函数,求两个整数之和,要求在函数体内不得使用 +、-、*、/ 四则运算符号。

1.解法一:迭代

  1. 只能使用位运算
  2. 分三步来解答:
    1. 不考虑进位对每一位相加,即0+0=0,1+1=0,1+0=1,0+1=1 ,也就是做异或 运算
    2. 考虑进位,只有1 + 1 会产生进位,因此求每一位的进位可以先将两个数做与运算 ,然后再左移一位
    3. 将前两个结果相加,直到进位为0
public class Solution {
    public int Add(int num1,int num2) {
        int sum = 0, carry = 1;
        while (carry != 0) {
            // 无进位相加
            sum = num1 ^ num2;
            // 对产生的进位进行计算,与运算得到的结果为两个数都有1的位置
            carry = (num1 & num2) << 1;
            // 迭代运算
            num1 = sum;
            num2 = carry;
        }
        return sum;
    }
}

2.解法二:递归

public class Solution {
    public int Add(int num1,int num2) {
        if (num2 == 0)
            return num1;
        int sum = num1 ^ num2; // 不考虑进位加
        int carry = (num1 & num2) << 1; // 考虑进位
        return Add(sum, carry);
    }
}

3.注意:交换两个变量值的方法

// 基于加减法
a = a + b;
b = a - b;
a = a - b;
// 基于位运算
a = a ^ b;
b = a ^ b;
a = a ^ b;

49.把字符串转换成整数

描述见链接

1.解法一

需要注意的问题包括:空指针、空字符串、正负数、整型溢出

注意点:

  1. 字符串是否为 null 或者字符串是否为空字符串。
  2. 字符串对于正负号的处理,特别是正号,可能有也可能没有,但都代表正数
  3. 输入值是否合法,判断除首位可能为正负号外,其他位是否都是数字
  4. int 为 32 位,最大的整数是刚刚超过 21亿,也就是10位十进制数
  5. 使用标志位记录正负号
public class Solution {
    public int StrToInt(String str) {
        if (str == null || str.length() == 0) return 0;
        // boolean isValid = false; // 记录这个数是否合法,好像没啥用
        int flag = 0;// 如果是正数为1,如果为负数为-1
        char c = str.charAt(0);
        if (c == '+') flag = 1; // 正数
        else if (c == '-') flag = -1;  // 负数
        else if (c >= '0' && c <= '9') { // 正数
            flag = 1;
            str = "+" + str; // 便于统一处理
        } else    return 0; // 不是数
        
        int len = str.length();
        long res = 0; // 记录结果,因为整型可能溢出,所以就使用长整型了
        if (len > 11) return 0; // int 数的最大值小于10^10,加上一个符号位最多11位
        for (int i = 1;i < len;i ++) {
            char digit = str.charAt(i);
            if (digit < '0' || digit > '9') return 0;
            // 下面这个公式也比较重要
            res = res * 10 + (digit - '0');
        }
        // 在整型范围内是合法的
        if (flag == 1 && res <= Integer.MAX_VALUE)
            return (int)res;
        if (flag == -1 && - res >= Integer.MIN_VALUE)
            return (int)(-res);
        return 0;
    }
}

50.数组中重复的数字

题目描述: 在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为 7 的数组 {2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字 2。

1.解法一:额外空间存储

public class Solution {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        if (numbers == null || length < 2) return false;
        HashSet<Integer> set = new HashSet<>(); // 使用HashSet存储,遇到重复的就赋值返回了
        for (int i = 0;i < length;i ++) {
            if (set.contains(numbers[i])) {
                duplication[0] = numbers[i];
                return true;
            } else {
                set.add(numbers[i]);
            }
        }
        return false;
    }
}

2.解法二:不使用额外空间的办法 ——> 数组重排(非系统函数排序)

根据题意:

  1. 所有的数字都在0n-1的范围内,因此如果没有重复,那么所存储的值也正好是0n-1n个数字,我们把原数组重新排列为一个元素和对应下标值相同的数组。即对于一个没有重复元素的数组,有

    index : 0 1 2 3 4 5 6 7
    eleme : 0 1 2 3 4 5 6 7
    
    
  2. 具体思路:

    1. 从头到尾扫描这个数组中的元素,当扫描到下标为i 的数字num[i]时,首先比较num[i] == i 成立与否,如果成立,则接着比较下一个元素;
    2. 如果不成立,则判断num[i] == num[num[i]] 成立与否,如果成立,说明是一个重复元素,如果不成立,说明num[i] 应该放到num[num[i]] 这个他的最终归属地去,因此交换这两个元素,重复过程,直到找到结果;
    3. 因为每一次判断无非就是等或不等,所以每一个元素最多比较两次,所以最终的时间复杂度还是O(n)
  3. 示例arr = {2,3,1,0,2,5,3}

    1. arr[0] != 0
      1. arr[0] != arr[arr[0]],所以交换一下得到{1,3,2,0,2,5,3}
      2. arr[0] != arr[arr[0]],所以交换一下得到{3,1,2,0,2,5,3}
      3. arr[0] != arr[arr[0]],所以交换一下得到{0,1,2,3,2,5,3}
      4. arr[0] == 0,退出进行下一次循环,以此类推
    2. 最终找到目标答案2
public class Solution {
    public boolean duplicate(int numbers[],int length,int [] duplication) {
        if (numbers == null || length < 2) return false;
        for (int i = 0;i < length;i ++) {
            while (numbers[i] != i) {
                if (numbers[numbers[i]] == numbers[i]) {
                    duplication[0] = numbers[i];
                    return true;
                } else {
                    int tmp = numbers[numbers[i]];
                    numbers[numbers[i]] = numbers[i];
                    numbers[i] = tmp;
                }
            }
        }
        return false;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值