刷题剑指offer

 

目录

1、输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

 

 2、给一个数组,返回它的最大连续子序列的和

3、输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

 4、把数组排成最小的数

 5、丑数

 6、第一个只出现一次的字符

 7、数组中的逆序对

8、两个链表的公共结点。

9、统计一个数字在排序数组中出现的次数。

10、二叉树的深度

11、平衡二叉树

12、数组中只出现一次的数字

13、和为S的连续正数序列

14、和为S的两个数字

15、反转单词顺序列

16、扑克牌顺子

16、孩子们的游戏

17、求1+2+3+...+n

18、不用加减乘除做加法

19、把字符串转换成整数

20、数组中重复的数字

21、构建乘积数组

22、正则表达式匹配

23、表示数值的字符串

24、字符流中第一个不重复的字符

25、链表中环的入口结点


 

1、输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

 

方法一:非递归版

解题思路:

1.核心是中序遍历的非递归算法。

2.修改当前遍历节点与前一遍历节点的指针指向。

 import java.util.Stack;
    public TreeNode ConvertBSTToBiList(TreeNode root) {
        if(root==null)
            return null;
        Stack<TreeNode> stack = new Stack<TreeNode>();
        TreeNode p = root;
        TreeNode pre = null;// 用于保存中序遍历序列的上一节点
        boolean isFirst = true;
        while(p!=null||!stack.isEmpty()){
            while(p!=null){
                stack.push(p);
                p = p.left;
            }
            p = stack.pop();
            if(isFirst){
                root = p;// 将中序遍历序列中的第一个节点记为root
                pre = root;
                isFirst = false;
            }else{
                pre.right = p;
                p.left = pre;
                pre = p;
            }
            p = p.right;
        }
        return root;
    }

 

方法二:递归版

解题思路:

1.将左子树构造成双链表,并返回链表头节点。

2.定位至左子树双链表最后一个节点。

3.如果左子树链表不为空的话,将当前root追加到左子树链表。

4.将右子树构造成双链表,并返回链表头节点。

5.如果右子树链表不为空的话,将该链表追加到root节点之后。

6.根据左子树链表是否为空确定返回的节点。

public TreeNode Convert(TreeNode root) {
        if(root==null)
            return null;
        if(root.left==null&&root.right==null)
            return root;
        // 1.将左子树构造成双链表,并返回链表头节点
        TreeNode left = Convert(root.left);
        TreeNode p = left;
        // 2.定位至左子树双链表最后一个节点
        while(p!=null&&p.right!=null){
            p = p.right;
        }
        // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
        if(left!=null){
            p.right = root;
            root.left = p;
        }
        // 4.将右子树构造成双链表,并返回链表头节点
        TreeNode right = Convert(root.right);
        // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
        if(right!=null){
            right.left = root;
            root.right = right;
        }
        return left!=null?left:root;       
    }

方法三:改进递归版

解题思路:

思路与方法二中的递归版一致,仅对第2点中的定位作了修改,新增一个全局变量记录左子树的最后一个节点。

// 记录子树链表的最后一个节点,终结点只可能为只含左子树的非叶节点与叶节点
    protected TreeNode leftLast = null;
    public TreeNode Convert(TreeNode root) {
        if(root==null)
            return null;
        if(root.left==null&&root.right==null){
            leftLast = root;// 最后的一个节点可能为最右侧的叶节点
            return root;
        }
        // 1.将左子树构造成双链表,并返回链表头节点
        TreeNode left = Convert(root.left);
        // 3.如果左子树链表不为空的话,将当前root追加到左子树链表
        if(left!=null){
            leftLast.right = root;
            root.left = leftLast;
        }
        leftLast = root;// 当根节点只含左子树时,则该根节点为最后一个节点
        // 4.将右子树构造成双链表,并返回链表头节点
        TreeNode right = Convert(root.right);
        // 5.如果右子树链表不为空的话,将该链表追加到root节点之后
        if(right!=null){
            right.left = root;
            root.right = right;
        }
        return left!=null?left:root;       
    }

 2、给一个数组,返回它的最大连续子序列的和

HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

思路

看了几篇关于最大连续子序列的和的博客,发现都是上来给出状态方程:

 max( dp[ i ] ) = getMax( max( dp[ i -1 ] ) + arr[ i ] ,arr[ i ] )

首先我们需要了解dp[i]到底是个啥,经过博主的不懈努力,终于发现dp[i]就是以数组下标为i的数做为结尾的最大子序列和,注意是以i为结尾,比如说现在有一个数组{6,-3,-2,7,-15,1,2,2},为了不搞,我们就下标以1开始,dp[3]就是以-2为结尾的,那么显然dp[3]的最大值就是1咯(6,-3,-2),dp[4]要以7结尾那么以7结尾的子序列最大和就是8(6,-3,-2,7)。

 知道dp[i]是啥后,现在我们开始细细品一下上面这个递推式,求dp[i]的时候是不是有两种可能,要么就是像上面的dp[4]一样,dp[3]求出来是1了,再加上自己array[4]是最大的,那么还有一种可能就是说如果dp[3]我求出来是-100,那如果我也是dp[3]+array[4]的话是-93,这时候dp[3]反而是累赘,最大就是自己(因为前面定义了必须以i为结尾,也就说必须以7结尾)。

下面给出代码:

 public int FindGreatestSumOfSubArray(int[] array) {
        int max= array[0];
        int res=array[0];
        for(int i=1;i<array.length;i++){
           max= Math.max(max+array[i],array[i]);
           res=Math.max(max,res);//
        }
        return res;
    }

3、输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。

 我们以三个字符abc为例来分析一下求字符串排列的过程。首先我们固定第一个字符a,求后面两个字符bc的排列。当两个字符bc的排列求好之后,我们把第一个字符a和后面的b交换,得到bac,接着我们固定第一个字符b,求后面两个字符ac的排列。现在是把c放到第一位置的时候了。记住前面我们已经把原先的第一个字符a和后面的b做了交换,为了保证这次c仍然是和原先处在第一位置的a交换,我们在拿c和第一个字符交换之前,先要把b和a交换回来。在交换b和a之后,再拿c和处在第一位置的a进行交换,得到cba。我们再次固定第一个字符c,求后面两个字符b、a的排列。
 

  public ArrayList<String> Permutation(String str) {
        ArrayList<String> stringList = new ArrayList<String>();
        char[] chars = str.toCharArray();
        Permu(chars,0,stringList);
        Collections.sort(stringList);
        return stringList;
    }
    public void Permu(char[] chars,int n,ArrayList<String> strings){
        if(chars==null){
            return;
        }
        if(n==chars.length-1){
            if(strings.contains(String.valueOf(chars))){
                return;
            }
            strings.add(chars.toString());
        }else {
            for(int i=n;i<chars.length;i++){
                char temp=chars[i];
                chars[i]=chars[n];
                chars[n]=temp;
                Permu(chars,n+1,strings);

                char temp1=chars[n];
                chars[n]=chars[i];
                chars[i]=temp1;
            }
        }
    }

 4、把数组排成最小的数

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。


* 解题思路:

 * 先将整型数组转换成String数组,然后将String数组排序,最后将排好序的字符串数组拼接出来。关键就是制定排序规则。

 * 排序规则如下:

 * 若ab > ba 则 a > b,

 * 若ab < ba 则 a < b,

 * 若ab = ba 则 a = b;

 * 解释说明:

 * 比如 "3" < "31"但是 "331" > "313",所以要将二者拼接起来进行比较

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;

public class Solution {
    public String PrintMinNumber(int [] numbers) {
        String[] arrayStr=new String[numbers.length];
        for(int i=0;i<numbers.length;i++){
            arrayStr[i]=String.valueOf(numbers[i]);
        }
        Arrays.sort(arrayStr, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                String c1=o1+o2;
                String c2=o2+o1;
                return c1.compareTo(c2);
            }
        });
        StringBuffer sb =new StringBuffer();
        for(int i=0;i<arrayStr.length;i++){
           sb.append(arrayStr[i]);
        }
        return sb.toString();
    }
}

 5、丑数

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

该思路: 我们只用比较3个数:用于乘2的最小的数、用于乘3的最小的数,用于乘5的最小的
  public int GetUglyNumber_Solution(int index) {
        if (index <= 0) {
            return 0;
        }
        int count = 0;
        List<Integer> list = new ArrayList<Integer>();
        list.add(1);
        int i2 = 0, i3 = 0, i5 = 0;
        while (list.size() <= index) {
            int m2 = list.get(i2) * 2;
            int m3 = list.get(i3) * 3;
            int m5 = list.get(i5) * 5;
            int min = Math.min(m2, Math.min(m3, m5));
            list.add(min);
            if (min == i2) i2++;
            if (min == i3) i3++;
            if (min == i5) i5++;
        }
        return list.get(list.size() - 1);
    }

 6、第一个只出现一次的字符

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写).


说一下解题思路哈,其实主要还是hash,利用每个字母的ASCII码作hash来作为数组的index。首先用一个58长度的数组来存储每个字母出现的次数,为什么是58呢,主要是由于A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122,而每个字母的index=int(word)-65,比如g=103-65=38,而数组中具体记录的内容是该字母出现的次数,最终遍历一遍字符串,找出第一个数组内容为1的字母就可以了,时间复杂度为O(n)。

  public int FirstNotRepeatingChar(String str) {
        int[] word=new int[58];
        for(int i=0;i<str.length();i++){
            word[(int)str.charAt(i)-65]+=1;
        }
        for(int i=0;i<str.length();i++){
           if(word[(int)str.charAt(i)-65]==1){
                return i;
            }
        }
        return -1;
    }

 7、数组中的逆序对

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

输入描述:

题目保证输入的数组中没有的相同的数字

数据范围:

对于%50的数据,size<=10^4

对于%75的数据,size<=10^5

对于%100的数据,size<=2*10^5

示例1

输入

复制

1,2,3,4,5,6,7,0

输出

复制

7
 int count=0;
    public int InversePairs(int [] array) {
   if (array != null && array.length != 0) {
            mergeUp2Down(array, 0, array.length - 1);
        }
         return count;
    }

     private void mergeUp2Down(int[] array, int low, int high) {
        if(low>=high){
            return;
        }
        int mid = (low + high) >> 1;
        mergeUp2Down(array, low, mid);
        mergeUp2Down(array, mid + 1, high);
        merger(array,low,mid,high);
    }

    private void merger(int[] array, int low, int mid,int high) {
        int i=low,j=mid+1,k=0;
        int[] temp=new int[high-low+1];
        while (i<=mid&&j<=high){
            if(array[i]>array[j]){
                temp[k++]=array[j++];
                count+=mid-i+1;
                count=count>1000000007?count%1000000007:count;
            }else {
                temp[k++]=array[i++];
            }
        }
        while (i<=mid){
            temp[k++]=array[i++];
        }
        while (j<=high){
            temp[k++]=array[j++];
        }
        for(int m=0;m<temp.length;m++){
            array[m+low]=temp[m];
        }
    }

8、两个链表的公共结点。

输入两个链表,找出它们的第一个公共结点。

假定 List1长度: a+n  List2 长度:b+n, 且 a<b 那么 p1 会先到链表尾部, 这时p2 走到 a+n位置,将p1换成List2头部

接着p2 再走b+n-(n+a) =b-a 步到链表尾部,这时p1也走到List2的b-a位置,还差a步就到可能的第一个公共节点。

将p2 换成 List1头部,p2走a步也到可能的第一个公共节点。如果恰好p1==p2,那么p1就是第一个公共节点。  或者p1和p2一起走n步到达列表尾部,二者没有公共节点,退出循环。 同理a>=b. 时间复杂度O(n+a+b)

  public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode p1 = pHead1;
        ListNode p2 = pHead2;
        while(p1!=p2){
            if(p1!=null)p1=p1.next;
             if(p2!=null)p2=p2.next;
            if(p1 != p2){
             if(p1==null)p1=pHead2;
             if(p2==null)p2=pHead1;
            }
        }
        return p1;
    }

9、统计一个数字在排序数组中出现的次数。

因为data中都是整数,所以可以稍微变一下,不是搜索k的两个位置,而是搜索k-0.5和k+0.5
这两个数应该插入的位置,然后相减即可。
  public int GetNumberOfK(int [] array , int k) {
        return biSearch(array, k+0.5)-biSearch(array, k-0.5);
    }

    private int biSearch(int[] array, double v) {
        int start=0,end=array.length-1;
        while (start<=end){
            int mid = (end - start)/2 + start;
            if(array[mid]<v){
                start=mid+1;
            }else if(array[mid]>v){
                end=mid-1;
            }
        }
        return start;
    }

10、二叉树的深度

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

求树的深度,可以从层次遍历出发考虑

层次遍历可以使用队列完成,也可以使用递归完成,所以有两种方法

递归版本:

思路: 从跟节点出发, 查询左子树的深度 , 获取右子树的深度,比较一下,取大的,再加一 。就是整个二叉树的深度 

递归的三个条件

边界条件:当前节点下,是否还有子节点,没有返回,有继续递归

递归前进段:当前节点下,还有子节点

递归返回段:当前节点下,没有子节点

  public int TreeDepth(TreeNode root) {
            if(root==null){
            return 0;
        }
        int left=TreeDepth(root.left);
        int right=TreeDepth(root.right);
        return Math.max(left,right)+1;
    }

非递归版本:

  public int TreeDepth(TreeNode root) {
        if(root==null){
            return 0;
        }
        // depth:当前节点所在的层数,count已经遍历了的节点数,
        // nextCount下层的节点总数;当count==nextCount的时候,代表本层的节点已经遍历完毕。
        int depth=0,count=0,nextCount=1;
        ArrayList<TreeNode> list =new ArrayList<TreeNode>();
        list.add(root);
        while (list.size()>0){
            TreeNode treeNode= list.remove(0);
            count++;
            if(treeNode.left!=null){
                list.add(treeNode.left);
            }
            if(treeNode.right!=null){
                list.add(treeNode.right);
            }
            if(count==nextCount){
                count=0;
                nextCount=list.size();
                depth++;
            }
        }
        return depth;
    }

11、平衡二叉树

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

 

如果二叉树的每个节点的左子树和右子树的深度不大于1,它就是平衡二叉树。

先写一个求深度的函数,再对每一个节点判断,看该节点的左子树的深度和右子树的深度的差是否大于1

 private boolean isBalance=true;
    public boolean IsBalanced_Solution(TreeNode root) {
        if(root!=null){
          getTreeDepth(root);
        }
        return isBalance;
    }

    private int getTreeDepth(TreeNode root) {
        if(root==null){
            return 0;
        }
        int left=getTreeDepth(root.left);
        int right=getTreeDepth(root.right);
        if(Math.abs(left-right)>1){
            isBalance=false;
        }
        return Math.max(left,right)+1;
    }

12、数组中只出现一次的数字

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

第一种解法,是自己想到的,使用HashMap可以把时间复杂度变为O(n);

  public static void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array.length<=1){
            num1[0]=0;
            num2[0]=0;
            return;
        }
        HashMap<Integer,Integer> map= new HashMap<Integer,Integer>();
        for(int i=0;i<array.length;i++){
            if(map.containsKey(array[i])){
                map.put(array[i],1);
            }else {
                map.put(array[i],0);
            }
        }
      boolean isFirst=true;
        for(Integer key:map.keySet()){
            if(map.get(key)==0){
                if(isFirst){
                    num1[0]=key;
                    isFirst=false;
                }else {
                    num2[0]=key;
                }
            }
        }
    }

第二种解法是看别人的优秀写法。

考虑过程:

首先我们考虑这个问题的一个简单版本:一个数组里除了一个数字之外,其他的数字都出现了两次。请写程序找出这个只出现一次的数字。

 这个题目的突破口在哪里?题目为什么要强调有一个数字出现一次,其他的出现两次?我们想到了异或运算的性质:任何一个数字异或它自己都等于0 。也就是说,如果我们从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现一次的数字,因为那些出现两次的数字全部在异或中抵消掉了。

 有了上面简单问题的解决方案之后,我们回到原始的问题。如果能够把原数组分为两个子数组。在每个子数组中,包含一个只出现一次的数字,而其它数字都出现两次。如果能够这样拆分原数组,按照前面的办法就是分别求出这两个只出现一次的数字了。

 我们还是从头到尾依次异或数组中的每一个数字,那么最终得到的结果就是两个只出现一次的数字的异或结果。因为其它数字都出现了两次,在异或中全部抵消掉了。由于这两个数字肯定不一样,那么这个异或结果肯定不为0 ,也就是说在这个结果数字的二进制表示中至少就有一位为1 。我们在结果数字中找到第一个为1 的位的位置,记为第N 位。现在我们以第N 位是不是1 为标准把原数组中的数字分成两个子数组,第一个子数组中每个数字的第N 位都为1 ,而第二个子数组的每个数字的第N 位都为0 。

 现在我们已经把原数组分成了两个子数组,每个子数组都包含一个只出现一次的数字,而其它数字都出现了两次。因此到此为止,所有的问题我们都已经解决。

 public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
        if(array==null ||array.length<2)
            return ;
        int temp = 0;
        for(int i=0;i<array.length;i++)
            temp ^= array[i];

        int indexOf1 = findFirstBitIs(temp);
        for(int i=0;i<array.length;i++){
            if(isBit(array[i], indexOf1))
                num1[0]^=array[i];
            else
                num2[0]^=array[i];
        }
    }
    private int findFirstBitIs(int num){
        int indexBit = 0;
        while(((num & 1)==0) && (indexBit)<8*4){
            num = num >> 1;
            ++indexBit;
        }
        return indexBit;
    }
    private boolean isBit(int num,int indexBit){
        num = num >> indexBit;
        return (num & 1) == 1;
    }

13、和为S的连续正数序列

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列? Good Luck!

输出描述:

输出所有和为S的连续正数序列。序列内按照从小至大的顺序,序列间按照开始数字从小到大的顺序

查看别人的写的优秀代码,总结了四种解法

解法一:

数学知识偏多一点,但也不是难到无法求解,稍微麻烦一点就是了。首先我们需要回答三个问题。

1、n = 2k + 1时,n项连续正数序列的和为S的条件:     n & 1 && S / n == 0 解读 逻辑与的左边要求n为奇数,右边要求整个序列的平均数恰好为中间数。

2、n = 2k时,n项连续正数序列的和为S的条件:  S % n * 2 == n  解读  S % n 的结果是中间两项左边的那项,乘2刚好是项数。举例,现有S = 39,6个连续正数序列和式能不能为S呢?套用公式,39 % 6 * 2 =6 == 6,我们也知道,这次的序列是 4、5、6、7、8、9,取余的结果为3对应着值为6的那一项,也就是中间项左边的那一项。

3、和为S,项数为n,如何写出这个序列?  S / n - (n-1) / 2  解读  执行的除法是地板除法(floor),不管最终结果有无小数都直接舍去。仍使用上述例子,39 / 6 = 6,6恰好是中间项左边的那一项,6 - (6-1)/ 2 = 4,恰好是序列最左端。序列写出来就没问题。
 

 public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<ArrayList<Integer>>();
        if (sum < 3) {
            return listArrayList;
        }
        for (int n = (int) Math.sqrt(sum * 2); n >= 2; n--) {
            if (((n & 1) == 1 && (sum % n) == 0) || ((sum % n) * 2 == n)) {
                ArrayList<Integer> list = new ArrayList<>();
                for (int j = 0, k = sum / n - (n - 1) / 2; j < n; j++, k++) {j用于计数,k用于遍历求值
                    list.add(k);
                }
                listArrayList.add(list);
            }
        }
        return listArrayList;
    }

解法二:

暴力求解,类似于TCP的滑动窗口协议。

 public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
        if (sum < 3) {
            return listArrayList;
        }
        int low = 1, high = 2;
        while (low < high) {
            //终止条件,因为题干要求至少2个数
            //连续且差为1的序列,求和公式可知
            int currentSum = (int) ((low + high) * (high - low + 1)) / 2;//求和
            if (currentSum < sum) {
                high++;
            }
            if (currentSum > sum) {
                low++;
            }
            if (currentSum == sum) {
                ArrayList<Integer> list = new ArrayList<>();
                for (int k = low; k <= high; k++) {
                    list.add(k);
                }
                low++;
                listArrayList.add(list);
            }
        }
        return listArrayList;
    }

解法三:

同样是受到了TCP滑动窗口协议的启发。还有迭代的想法在里面,也可以是说状态转移方程。

用begin和end分别表示序列的左值和右值,首先将begin初始化为1,end初始化为2;

  • 若[begin, end]之和 > S,从序列中去掉第一个值(增大begin);
  • 若和 < S,增大end,和中加入新的end;
  • 等于S,将[begin, end] 纳入到结果集中;
 public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
        if (sum < 3) {
            return listArrayList;
        }
        int mid = (sum + 1) >> 1;
        int begin = 1;
        int end = 2;
        int curSum = 3;
        while (begin < mid) {
            while ((curSum > sum)&&(begin<end)) {//这个循环的隐患在于循环结束时,beign == end而且和等于sum
                curSum -= begin;
                begin++;
            }
            if ((curSum == sum)&&(begin<end)) {
                ArrayList<Integer> list = new ArrayList<>();
                for (int k = begin; k <= end; k++) {
                    list.add(k);
                }
                listArrayList.add(list);
            }
            end++;
            curSum += end;
        }
        return listArrayList;
    }

 

解法四:(换元法)  

根据所学知识,有:

(a + b)(b - a + 1) = 2 * sum;此为等差数列公式。

令i = b - a + 1(项数), j = a + b(首末项之和);现讨论取值范围。i >= 2(序列中至少有2项), j >= 3(序列之和至少为3);隐藏的关系是: j > i同时还有 i * j = 2 * sum,进行放缩之后就有 i * i < 2 * sum,即 i < 根号(2 * sum)。对i进行遍历,找出i,j∈正整数且j - i + 1为偶的取值。

  ArrayList<ArrayList<Integer>> listArrayList = new ArrayList<>();
        if (sum < 3) {
            return listArrayList;
        }
        int twoSum=2*sum;
        int i,j;
        for(i=(int)Math.sqrt(twoSum);i>=2;i--){
            if(twoSum%i==0){//首末项之和为整数
                j=twoSum/i;//求出首末项之和
                int temp=j-i+1;//解方程得到2倍的左值等于j - i + 1;要求左边界为整数
                if(temp%2==0){ //要求temp是偶数
                    int begin=temp>>1;
                    int end=j-begin;//j的实际意义是首末项之和
                    ArrayList<Integer> list = new ArrayList<>();
                    for (int k = begin; k <= end; k++) {
                        list.add(k);
                    }
                    listArrayList.add(list);
                }
            }
        }
        return listArrayList;

14、和为S的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

输出描述:

对应每个测试案例,输出两个数,小的先输出。

假设:若b>a,且存在,

a + b = s;

(a - m ) + (b + m) = s

:(a - m )(b + m)=ab - (b-a)m - m*m < ab;说明外层的乘积更小
也就是说依然是左右夹逼法!!!只需要2个指针

1.left开头right指向结尾
2.如果和小于sum,说明太小了left右移寻找更的数
3.如果和大于sum,说明太大了right左移寻找更的数
4.和相等把left和right的数返回

 public ArrayList<Integer> FindNumbersWithSum(int [] array,int sum) {
        ArrayList<Integer> list=new ArrayList<>();
        if(array.length<2){
            return list;
        }
        int i=0,j=array.length-1;
        while(i<j){
            if((array[i]+array[j])==sum){
                list.add(array[i]);
                list.add(array[j]);
                break;
            }
            if((array[i]+array[j])>sum){
                j--;
            }
            if((array[i]+array[j])<sum){
                i++;
            }
        }
        return list;
    }

15、反转单词顺序列

牛客最近来了一个新员工Fish,每天早晨总是会拿着一本英文杂志,写些句子在本子上。同事Cat对Fish写的内容颇感兴趣,有一天他向Fish借来翻看,但却读不懂它的意思。例如,“student. a am I”。后来才意识到,这家伙原来把句子单词的顺序翻转了,正确的句子应该是“I am a student.”。Cat对一一的翻转这些单词顺序可不在行,你能帮助他么?

剑指offer的思想:两次翻转

 public static String ReverseSentence(String str) {
        if(str==null||str.trim().isEmpty()){
            return str;
        }
        char[] c = str.toCharArray();
        reverse(c,0,c.length-1);//翻转整个句子
        int begin=0,end=0;
        //翻转句子中的每个单词
        while (begin<c.length){
            if(c[begin]==' '){//若起始字符为空格,则begin和end都自加
                begin++;
                end++;
            }else if(c[end]==' '){//遍历到终止字符为空格,就进行翻转
                reverse(c,begin,--end);
                begin=++end;
            }else if(end==c.length-1){//若遍历结束,就进行翻转
                reverse(c,begin,end);
                begin=++end;
            }
            else{
                end++;//没有遍历到空格或者遍历结束,则单独对end自减
            }
        }
        return String.valueOf(c);
    }
    //完成翻转功能
    private static void reverse(char[] c,int begin,int end) {
        while (begin<end){
           char temp= c[begin];
            c[begin]=c[end];
            c[end]=temp;
            begin++;
            end--;
        }
    }

16、扑克牌顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张^_^)...他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子.....LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

思路1:

1、排序 。

2、计算所有最大值和最小值间隔总数。 

3、计算相邻的数字是否有重复。

 public static boolean isContinuous(int [] numbers) {
        if(numbers.length!=5){
            return false;
        }
        int min = 14;
        int max = -1;
        int flag = 0;
        Arrays.sort(numbers);//排序
        for(int i=0;i<numbers.length;i++){
            if(numbers[i]==0){
                continue;
            }
            if((flag^numbers[i]) ==0)return false;//相邻的两个数异或运算,若为0则存在相等的
            flag =  numbers[i];
            if(numbers[i]>max){
                max=numbers[i];
            }
            if(numbers[i]<min){
                min=numbers[i];
            }
            if (max - min >=5) {
                return false;
            }
        }
        return true;
    }

看到别人还有一种写法,这个没有排序,有一点不太懂:

  public boolean isContinuous(int [] numbers) {
        if(numbers.length!=5){
            return false;
        }
        int min = 14;
        int max = -1;
        int flag = 0;
        for(int i=0;i<numbers.length;i++){
            if(numbers[i]==0){
                continue;
            }
            if(((flag >> numbers[i]) & 1) == 1) return false;//没看懂这里
            flag = (1 << numbers[i]);//没看懂这里
            if(numbers[i]>max){
                max=numbers[i];
            }
            if(numbers[i]<min){
                min=numbers[i];
            }
            if (max - min >=5) {
                return false;
            }
        }
        return true;
    }

16、孩子们的游戏

每年六一儿童节,牛客都会准备一些小礼物去看望孤儿院的小朋友,今年亦是如此。HF作为牛客的资深元老,自然也准备了一些小游戏。其中,有个游戏是这样的:首先,让小朋友们围成一个大圈。然后,他随机指定一个数m,让编号为0的小朋友开始报数。每次喊到m-1的那个小朋友要出列唱首歌,然后可以在礼品箱中任意的挑选礼物,并且不再回到圈中,从他的下一个小朋友开始,继续0...m-1报数....这样下去....直到剩下最后一个小朋友,可以不用表演,并且拿到牛客名贵的“名侦探柯南”典藏版(名额有限哦!!^_^)。请你试着想下,哪个小朋友会得到这份礼品呢?(注:小朋友的编号是从0到n-1)

如果没有小朋友,请返回-1

 /*

 *这道题我用数组来模拟环,思路还是比较简单,但是各种下标要理清

    */

 public int LastRemaining_Solution(int n, int m) {
        if(n<1||m<1){
            return -1;
        }
        int[] array =new int[n];
        int i = -1,step = 0, count = n;
        while (count>0) {//跳出循环时将最后一个元素也设置为了-1
            i++;//指向上一个被删除对象的下一个元素。
            if(i>=n){ //模拟环。
                i=0;
            }
            if(array[i]==-1){//跳过被删除的对象。
                continue;
            }
            step++;//记录已走过的。
            if(step==m){  //找到待删除的对象。
                array[i]=-1;
                step=0;
                count--;
            }
        }
        return i;//返回跳出循环时的i,即最后一个被设置为-1的元素
    }

17、求1+2+3+...+n

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

总结大牛们的方法,提供java的两种阶梯思路:

共同点:一,利用利用短路 && 来实现 if的功能;二,利用递归来实现循环while的功能

不同点:方法一:递归实现1+2+..+n;方法二:n(n+1)/2,递归实现n(n+1);方法三,利用Math实现n(n+1)

关于如何递归实现a*b,有大佬总结过,我搬下来:利用位运算来做,快速幂,快速模乘,

原理是把a拆成2的幂的和,a = 2^e0 + 2^e1 + 2^e2.... 
     那么 a * b = (2^e0 + 2^e1 + 2^e2+...) * b 

                      = b * 2^e0 + b * 2^e1 + b * 2^e2 + ...
                      = (b << e0) + (b << e1) + ....
接下来看代码:

方法一:递归实现1+2+..+n;

 public int Sum_Solution(int n) {
        int ans=n;
        boolean flag=(ans>0)&&((ans+=Sum_Solution(n-1))>0);
        return ans;
    }

方法三,利用Math实现n(n+1)

 public int Sum_Solution(int n) {
        return (int)(Math.pow(n,2)+n)/2;
    }

 

方法二:n(n+1)/2,递归实现n(n+1);

先参考使用while的例子,再转换

原理是把a拆成2的幂的和,a = 2^e0 + 2^e1 + 2^e2.... 

 那么 a * b = (2^e0 + 2^e1 + 2^e2+...) * b 

                      = b * 2^e0 + b * 2^e1 + b * 2^e2 + ...
                      = (b << e0) + (b << e1) + ....

public static int Sum_Solution2(int n) {
        int res = 0;
        int a = n;//若a=2=10
        int b = n + 1;//b=3=11
        while (a != 0) {
            if ((a & 1) == 1)//a在第二位==1的时候才更新res=0+110=6
                res += b;
            a >>= 1;//a右移1位 1
            b <<= 1;//b左移动1位 110
        }
        return res>>=1;//n(n+1)/2     }

接下来,用(a & 1) == 1和(a != 0)来代替判断语句

public int Sum(int n) {
        int res = multi(n, n + 1);//n*(n-1)
        return res>>=1;//n*(n-1)/2
    }
     
    private int multi(int a, int b) {
        int res = 0;
        //循环体内部, if ((a & 1) == 1), res += b;
        boolean flag1 = ((a & 1) == 1) && (res += b) > 0;
        a >>= 1;
        b <<= 1;
        // while (a != 0) {}循环条件
        boolean flag2 = (a != 0) && (res += multi(a,b)) > 0 ;
        return res;
    }

18、不用加减乘除做加法

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


首先看十进制是如何做的: 5+7=12,三步走 第一步:相加各位的值,不算进位,得到2。 第二步:计算进位值,得到10. 如果这一步的进位值为0,那么第一步得到的值就是最终结果。 第三步:重复上述两步,只是相加的值变成上述两步的得到的结果2和10,得到12。 同样我们可以用三步走的方式计算二进制值相加: 5-101,7-111 第一步:相加各位的值,不算进位,得到010,二进制每位相加就相当于各位做异或操作,101^111。 第二步:计算进位值,得到1010,相当于各位做与操作得到101,再向左移一位得到1010,(101&111)<<1。 第三步重复上述两步, 各位相加 010^1010=1000,进位值为100=(010&1010)<<1。 继续重复上述两步:1000^100 = 1100,进位值为0,跳出循环,1100为最终结果。

 public int Add(int num1,int num2) {
        while (num2!=0){
            int temp=num1^num2;
            num2=(num1&num2)<<1;
            num1 =temp;
        }
        return num1;
    }

19、把字符串转换成整数

将一个字符串转换成一个整数(实现Integer.valueOf(string)的功能,但是string不符合数字要求时返回0),要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0。

输入描述:

输入一个字符串,包括数字字母符号,可以为空

输出描述:

如果是合法的数值表达则返回该数字,否则返回0

示例1

输入

+2147483647
    1a33

输出

2147483647
    0
 public int StrToInt(String str) {
          if(str==null||str.isEmpty()){
            return 0;
        }
        char[] arr=str.toCharArray();
        boolean isNec=false;
        int sum=0;
        if(arr[0]=='-'){
            isNec=true;
        }
        for(int i=0;i<arr.length;i++){
            if(i==0&&(arr[i]=='-'||arr[i]=='+')){
                    continue;
            }
            if(arr[i]>'9'||arr[i]<'0'){
                return 0;
            }
            sum=sum*10+(arr[i]-'0');
        }
        return isNec?(0-sum):sum;
    }

20、数组中重复的数字

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

思路:使用HashMap或者一个数组来存储数组中数出现的次数。

  public static boolean duplicate(int numbers[],int length,int [] duplication) {
        if(length<2||numbers.length<2){
            return false;
        }
        boolean isReturn=false;
        HashMap<Integer,Integer> map=new HashMap<>();
        for(int i=0;i<numbers.length;i++){
            if(map.containsKey(numbers[i])){
                int value =map.get(numbers[i]);
                map.put(numbers[i],++value);
            }else {
                map.put(numbers[i],0);
            }
        }
        for (int key : map.keySet()) {
            if(map.get(key)!=0){
                duplication[0]=key;
                isReturn=true;
                break;
            }
        }
        return isReturn;
    }

另一种:

/**

 * 数组中重复的数字

 *在一个长度为n的数组里的所有数字都在0到n-1的范围内。

 * 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。

 * 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

 * 思路:

 * 数组中的数字都在0到n-1的数字范围内。如果数组中没有重复出现的数字,那么当数组排序后数字i就出现在数组中下标为i的元素处。那么数组中如果存在重复数字的话,有些位置的对应的数字就没有出现,而有些位置可能存在多个数字。数组用numbers表示

 那么我们重排这个数组。从第0个元素开始。

 1、比较numbers[i]和i的值,如果i与numbers[i]相等,也就是对数组排序后,numbers[i]就应该在对应的数组的第i个位置处,那么继续判断下一个位置。

 2、如果i和numbers[i]的值不相等,那么判断以numbers[i]为下标的数组元素是什么。

 2.1、如果numbers[numbers[i]]等于numbers[i]的话,那么就是说有两个相同的值了,重复了。找到了重复的数字

 2.2、如果numbers[numbers[i]]不等于numbers[i]的话,那么就将numbers[numbers[i]]和numbers[i]互换。继续进行1的判断。

 3、循环退出的条件是直至数组最后一个元素,仍没有找到重复的数字,数组中不存在重复的数字。

 */

 public boolean duplicate(int numbers[],int length,int [] duplication) {
     if(length<=0||numbers==null){
            return false;
        }
        //判断数组数据是否合法
        for(int i=0;i<length;i++){
            if(numbers[i]<0||numbers[i]>length-1){
                return false;
            }
        }
 
        for(int i=0;i<length;i++){
            while(numbers[i]!=i){
                if(numbers[i]==numbers[numbers[i]]){
                    duplication[0] = numbers[i];
                    return true;
                }else{
                    //交换numbers[i]和numbers[numbers[i]]
                    int temp = numbers[i];
                    numbers[i] = numbers[temp];
                    numbers[temp] = temp;
                }
            }
        }
        return false;
    }

21、构建乘积数组

给定一个数组A[0,1,...,n-1],请构建一个数组B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。

剑指的思路:

B[i]的值可以看作下图的矩阵中每行的乘积。

下三角用连乘可以很容求得,上三角,从下向上也是连乘。

因此我们的思路就很清晰了,先算下三角中的连乘,即我们先算出B[i]中的一部分,然后倒过来按上三角中的分布规律,把另一部分也乘进去。

  public int[] multiply(int[] A) {
        int[] B = new int[A.length];
        B[0]=1;
        for(int i=1;i<A.length;i++){
            B[i]=B[i-1]*A[i-1];
        }
        int temp=1;
        for(int i=A.length-2;i>=0;i--){
            temp=temp*A[i+1];
            B[i]=B[i]*temp;
        }
        return B;
    } B;

22、正则表达式匹配

请实现一个函数用来匹配包括'.'和'*'的正则表达式。模式中的字符'.'表示任意一个字符,而'*'表示它前面的字符可以出现任意次(包含0次)。 在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"ab*ac*a"匹配,但是与"aa.a"和"ab*a"均不匹配

 public boolean match(char[] str, char[] pattern)
    {
        if (str == null || pattern == null) {
            return false;
        }
        int strIndex = 0;
        int patternIndex = 0;
        return matchCore(str, strIndex, pattern, patternIndex);
    }

    private boolean matchCore(char[] str, int strIndex, char[] pattern, int patternIndex) {
        //str到尾,pattern到尾,匹配成功
        if (strIndex == str.length && patternIndex == pattern.length) {
            return true;
        }
        //str未到尾,pattern到尾,匹配失败
        if (strIndex != str.length && patternIndex == pattern.length) {
            return false;
        }
        //str到尾,pattern未到尾(不一定匹配失败,因为a*可以匹配0个字符)
        if (strIndex == str.length && patternIndex != pattern.length) {
            //只有pattern剩下的部分类似a*b*c*的形式,才匹配成功
            if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
                return matchCore(str, strIndex, pattern, patternIndex + 2);
            }
            return false;
        }

        //str未到尾,pattern未到尾
        if (patternIndex + 1 < pattern.length && pattern[patternIndex + 1] == '*') {
            if (pattern[patternIndex] == str[strIndex] || (pattern[patternIndex] == '.' && strIndex != str.length)) {
                return matchCore(str, strIndex, pattern, patternIndex + 2)//*匹配0个,跳过
                        || matchCore(str, strIndex + 1, pattern, patternIndex + 2)//*匹配1个,跳过
                        || matchCore(str, strIndex + 1, pattern, patternIndex);//*匹配1个,再匹配str中的下一个
            } else {
                //直接跳过*(*匹配到0个)
                return matchCore(str, strIndex, pattern, patternIndex + 2);
            }
        }

        if (pattern[patternIndex] == str[strIndex] || (pattern[patternIndex] == '.' && strIndex != str.length)) {
            return matchCore(str, strIndex + 1, pattern, patternIndex + 1);
        }

        return false;
    }

23、表示数值的字符串

请实现一个函数用来判断字符串是否表示数值(包括整数和小数)。例如,字符串"+100","5e2","-123","3.1416"和"-1E-16"都表示数值。 但是"12e","1a3.14","1.2.3","+-5"和"12e+4.3"都不是。

1、看到一个大神的暴力解法,十分浅显易懂。

  public boolean isNumeric(char[] str) {
        try {
          double d=  Double.parseDouble(new String(str));
        }catch (Exception e){
            return false;
        }
        return true;
    }

2、一般解题思路:

  • 12e说明e的后面必须有数字,不能有两个e
  • +-5说明符号位要么出现一次在首位,要么出现一次在e的后一位,其他地方都不能有
  • 12e4.3说明e的后面不能有小数,1.2.3说明不能有两个小数点
  • 1a3.14说明不能有其他的非法字符,比如这里的a
public boolean isNumeric(char[] str) {
//signal表示符号,decimal表示小树点,hasE表示含有符号e
        boolean signal = false,decimal = false,hasE = false;
        for(int i=0;i<str.length;i++){
            if(str[i] == 'E' || str[i] == 'e'){
                //e后面必须有数字,所以是最后一位肯定不通过
                if(i==str.length-1){
                    return false;
                }
                //不能有两个e
                if(hasE){
                    return false;
                }
                hasE = true;
            }else if(str[i] == '+' || str[i] == '-'){
                //不是第一次出现,那么后面能出现符合的地方只有紧贴着e的后面一位,不是则不通过
                if(signal && str[i-1] != 'E' && str[i-1] != 'e'){
                    return false;
                }
                //第一次出现,如果不是出现在第一位,那么还是判断一下是不是出现在e的后面一位
                if(!signal && i>0 && str[i-1] != 'E' && str[i-1] != 'e'){
                    return false;
                }
                signal = true;
            }else if(str[i] == '.'){
                //如果存在e并且e后面为小数则不通过
                if(hasE){
                    for(;i>=0;i--){
                        if(str[i] == 'e' || str[i] == 'E'){
                            return false;
                        }
                    }
                }
                //不能有两个小数点
                if(decimal){
                    return false;
                }
                decimal = true;
            }else if(str[i] < '0' || str[i] > '9'){
                //不是e也不是+-符号也不是小数点,那么只能是数字,不是数字就是非法的字符
                return false;
            }
        }
 
        return true;
    }

3、正则表达式解法

/*

以下对正则进行解释:

[\\+\\-]?            -> 正或负符号出现与否

\\d*                 -> 整数部分是否出现,如-.34 或 +3.34均符合

(\\.\\d+)?           -> 如果出现小数点,那么小数点后面必须有数字;

                        否则一起不出现

([eE][\\+\\-]?\\d+)? -> 如果存在指数部分,那么e或E肯定出现,+或-可以不出现,

                        紧接着必须跟着整数;或者整个部分都不出现

*/

 public boolean isNumeric(char[] str) {
     String string = String.valueOf(str);
        return string.matches("[\\+\\-]?\\d*(\\.\\d+)?([eE][\\+\\-]?\\d+)?");
    }

24、字符流中第一个不重复的字符

常规的解法是用一个map来存储,这样空间复杂度为O(n),然后每次都遍历map获取第一个不重复的字符,时间复杂度也为O(n)。下面显示代码:

 Map<Character,Integer> map =new LinkedHashMap<>();
    public void Insert(char ch)
    {
        if(map.containsKey(ch)){
           int m= map.get(ch);
            map.put(ch,++m);
        }else {
            map.put(ch,0);
        }
    }
    //return the first appearence once char in current stringstream
    public char FirstAppearingOnce()
    {
        for(char ch:map.keySet()){
            int k =  map.get(ch);
            if(k==0){
                return ch;
            }
        }
        return '#';
    }

25、链表中环的入口结点

//先说个定理:两个指针一个fast、一个slow同时从一个链表的头部出发

//fast一次走2步,slow一次走一步,如果该链表有环,两个指针必然在环内相遇

//此时只需要把其中的一个指针重新指向链表头部,另一个不变(还在环内),

//这次两个指针一次走一步,相遇的地方就是入口节点。

//这个定理可以自己去网上看看证明。

 public ListNode deleteDuplication(ListNode pHead) {
        ListNode fast = pHead;
        ListNode slow = pHead;
        while (fast!=null&&fast.next!=null){
            fast =  fast.next.next;
            slow= slow.next;
            if(fast.val==slow.val){
                break;
            }
        }
        if(fast==null||fast.next==null){
            return null;
        }
        fast=pHead;
        while (fast!=slow){
            fast =  fast.next;
            slow= slow.next;
        }
        return fast;
    }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值