剑指offer刷题详细分析:part9:41题——45题

  • 剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer

  • 目录

  1. Number41:和为S的连续正整数序列
  2. Number42:和为S的2个数字
  3. Number43:左旋转字符串
  4. Number44:翻转单词顺序
  5. Number45:扑克牌顺子

题目41 和为S的连续正整数序列

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

  分析:可以使用2种方法解决

方法1:暴力穷举
  思路如下:

1、从1开始穷举连续和的值,大于sum则终止本次循环;

2、序列的終点为最大值,必须小于:sum/2 + 1,因为对于任意正奇数或者偶数,有 (sum/2+1+sum/2) > sum,即对于终点是 sum/2+1 的连续正整数序列,它的值一定大于sum,那么这种正整数序列的终点一定小于 sum/2+1,即序列中数的最大值必须小于 sum/2+13、注意,对于任意正整数,他们只能作为满足条件的序列的起点或者终点一次!!比如对于满足条件的序列: 起点为 start,终点为 end,其他的满足条件的序列必然不会以start开头,也不会以end结尾。如果另外的序列以start开头,它向满足条件,其终点必然得是end,这与前面的序列相同!!
总结:对于不同的满足条件的序列,他们的start与end必然不同!

  这种方法需要双层遍历,时间复杂度是 O(n^2).
  代码如下:

//1、暴力穷举法
    public ArrayList<ArrayList<Integer>> FindContinuousSequence(int sum) {
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();

        //当sum<3的时候,sum=1或者sum=2,正数序列最少包括2个数,不存在满足 和=1或2 的正数序列!
        if(sum<3)
            return ret;

        int limitNum = sum/2+1;
        /*
        我们的序列从1开始,序列中数的最大值必然小于 sum/2+1。
        那么我们用 i 来表示序列的数,当i<sum/2+1的时候,我们可以持续寻找序列
         */
        for (int i = 1; i < limitNum ; i++)
        {
            //再次回到这里,又创建一个新的ArrayList,序列和重新赋值为0,起始值加1,重新开始查找起始值不同的新的序列
            ArrayList<Integer> arrayList = new ArrayList<>();
            int curSum = 0;//首先,开始的时候序列和为0
            int curNum= i;//开始的时候,序列的初始值为i,i从1开始(对于所有满足条件的序列,每个i只能作为其中唯一一个序列的起始值)
            while(curSum < sum)
            {
                curSum += curNum;
                arrayList.add(curNum);
                curNum++;

                /*
                如果以i为起始值的序列,有一个序列满足 序列和=sum,将这个序列 arrayList添加到ret
                如果找到 curSum>sum,即从curSum<sum的时候,再加一个curNum,此时curSum>sum,说明以i为起始值的序列没有满足的,
                那么我们跳出内层循环,继续将i+1,遍历以i+1为起始值的序列。
                 */
                if(curSum == sum)//当找到,curSum=sum,下一次自然会跳出循环,不需要我们手动跳出
                    ret.add(arrayList);
            }
        }
        return ret;
    }

方法2:双指针
  思路

1、双指针技术,就是相当于有一个窗口,窗口的左右两边就是两个指针;

2、根据窗口内值之和来确定窗口的位置和宽度;

3、同样,对于不同的满足条件的序列,他们的start与end必然不同。我们定义2个指针:start()end(),start从1开始,end从2开始。当start<end 的时候,我们持续寻找序列。
首先计算以start开头,以end结尾的序列和 tempsum:
1)tempSum<sum,说明以1为起始的序列和太小,将end右移;
2)tempSum>sum,说明以1为起始的序列没有满足和为sum的子序列,那么我们将start右移,将起始值换为2,寻找以2位起始的序列中,是否有满足和为sum的子序列。
3)tempSum=sum,说明以1为起始的序列存在满足和为sum的子序列,将子序列添加到ret。注意,对于所有满足条件的序列,他们的头尾必然不同,因此,此时我们必须将start右移,此时tempSum<sum,又可以开始右移end将tempSum变大,寻找其他序列。
当然也可以先右移end使得tempSum>sum,再右移start使得temSum变小来匹配sum。但是注意,不能左移start或者end,因为前面的数字作为序列的start或者end,之前已经遍历过,这些序列要么不满足,要么满足已经添加到ret中,不需要再次遍历。
也就是说,我们2个指针start与end,是从左到右指向数字的每一个数来寻找满足条件的序列,不应该将他们左移(脑子里面可以想象一下这个过程!)

  这种方法的时间复杂度是 O(n)
  代码如下:

public ArrayList<ArrayList<Integer> > FindContinuousSequence(int sum)
    {
        ArrayList<ArrayList<Integer>> ret = new ArrayList<>();

        if(sum<3)
            return ret;

        int start = 1;
        int end = 2;
        while (start<end)//start不能等于end,因为序列要求最少2个数字
        {
            ArrayList<Integer> arrayList = new ArrayList<>();
            int tempSum = (start+end)*(end-start+1)/2;//求 start->end 的序列和

            if(tempSum == sum)
            {
                //将序列的数字添加到arrayList
                for (int i = start; i <=end ; i++)
                {
                    arrayList.add(i);
                }
                ret.add(arrayList);//记得将找到的序列添加到ret
                //此时我们可以将start或者end右移,来寻找下一个满足条件的序列
                // 当前的start或者end不可能为下一个满足条件序列的其实或者终点,因此必须改变这两个指针的值(改变一个另外的也会跟着改变)
                start++;
            }
            else if(tempSum > sum)
            {
                start++;//右移start以减少序列和
            }
            else //tempSum<sum
            {
                end++;//右移end以增大序列和
            }
        }
        return ret;
    }

题目42 和为S的2个数字

  题目描述:输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。
  输出描述:对应每个测试案例,输出两个数,小的先输出。

  分析:可以使用暴力遍历法(O(n^2)),不推荐。
  使用双指针,一个指针指向元素较小的值,一个指针指向元素较大的值。指向较小元素的指针从头向尾遍历,指向较大元素的指针从尾向头遍历。

如果两个指针指向元素的和 sum == target,那么得到要求的结果;
如果 sum > target,移动较大的元素,使 sum 变小一些;
如果 sum < target,移动较小的元素,使 sum 变大一些。

  这种方法时间复杂度为O(n),需要明确一点,可能数组中有多组数字满足条件,当两个数字离的越远,他们的乘积就越小。因此,我们才从数组的两边开始往中间查找,这样可以保证查找到的2个数字在所有满足条件的组合之间,离得最远!

public ArrayList<Integer> FindNumbersWithSum(int [] array, int sum)
    {
        ArrayList<Integer> arrayList = new ArrayList<>();

        int i = 0;
        int j = array.length-1;

        //i与j不可以相等,当遍历到i与j相等,数字内所有可能的数字组合全部查找完,说明没有满足条件的组合
        //当 i<j 的时候,持续遍历
        while(i < j)
        {
            int cur = array[i]+array[j];
            if(cur == sum)
            {
                arrayList.add(array[i]);
                arrayList.add(array[j]);
                //注意!!!如果找到,我们必须将数添加到ArrayList,返回ArrayList,否则会一直循环遍历卡在这个地方,产生死循环!
                return arrayList;
            }
            else if(cur > sum)
            {
                j--;//将大数左移变小
            }
            else
            {
                i++;//将小数右移变大
            }
        }

        return arrayList;//没有找到就return null
    }

题目43 左旋转字符串

  题目描述:汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。是不是很简单?OK,搞定它!

  分析:先将 “abc” 和 “XYZdef” 分别翻转,得到 “cbafedZYX”,然后再把整个字符串翻转得到 “XYZdefabc”。
  这种方法的好处是不需要使用额外的数据结构辅助,空间复杂度为O(1)。另外,也不需要使用字符串的其他操作(只有一个 复杂度为O(n)的toCharArray()操作),时间复杂度是O(n)。

  代码如下:

public String LeftRotateString(String str,int n)
    {
        //下面这个时候才是真正的不需要翻转。
        //注意,这里的判断应该放在 n>=str.length() 之前,都在下面的取余预算出现 java.lang.ArithmeticException: / by zero 异常
        if(str == null || str.length()==0 || n<=0)
            return str;

        /**
         * 很多答案忽略了一点,他们都是当 n >= str.length() 的时候,不翻转直接返回str,其实这样是有问题的,因为这个操作是循环左移,
         * 当 n >= str.length() 的时候,应该用 n除以字符串的长度,求得的余数就是真正的要左移的位数
         */
        if(n>=str.length())
        {
            //n对数组长度取余
            n = n%str.length();
        }


        char[] arr = str.toCharArray();

        //转换0到n-1位置的字符
        reverse(arr , 0 , n-1);
        //转换n到arr.length-1位置的字符
        reverse(arr , n , arr.length-1);
        //转换0到arr.length-1位置的字符
        reverse(arr , 0 , arr.length-1);

        //得到转换后的数组
        return new String(arr);
    }

    //翻转字符数组 start到end 位置的字符
    private void reverse(char arr[] , int start , int end)
    {
        //当start<end的时候,持续交换,直到start=end
        for (; start < end ; start++,end--)
        {
            swap(arr , start , end);
        }
    }
    //交换字符数组 arr的n位置与m位置的租房
    private void swap(char arr[] , int n , int m)
    {
        char temp = arr[n];
        arr[n] = arr[m];
        arr[m] = temp;
    }

题目44 翻转单词顺序

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

  分析题目应该有一个隐含条件,就是不能用额外的空间。虽然 Java 的题目输入参数为 String 类型,需要先创建一个字符数组使得空间复杂度为 O(N),但是正确的参数类型应该和原书一样,为字符数组,并且只能使用该字符数组的空间。任何使用了额外空间的解法在面试时都会大打折扣,包括递归解法。
  正确的解法应该是和书上一样,先旋转每个单词,再旋转整个字符串。做法与43题的做法大同小异,但是在细节处有一些地方需要注意!
  代码如下:

public String ReverseSentence(String str)
    {
        if(str == null || str.length() == 0)
            return str;

        int length = str.length();
        char[] chars = str.toCharArray();

        //使用2个指针来指向字符串的首尾
        int start = 0;
        int end = 0;//end指向字符串尾部的后一个元素
        /*
        当 字符串尾部 end<length-1 的时候,我们持续寻找单词.
         但是,如果如下这样寻找,对于最后一个单词,str.charAt(end) 永远不会指向空格字符,也就是说,最后一个字符还没有翻转,遍历便结束。
         因此,我们应该遍历到 length位置,判断到length位置的时候,同样将start到end-1位置的字符,此时end=length
        while(end<length)
        {
            if(str.charAt(end) == ' ')
            {
                //当end的位置是空格字符的时候,start位置到end-1位置是一个字符串,将其字符翻转
                reverse(chars , start , end-1);
                start = end+1;//此时,将start指向下一个字符的起始位置
            }
            end++;//不管当前end位置是不是空格字符,每一次循环都要将end+1
        }
        */

        while(end<=length)
        {
            //end会加到length(到达数组尾部),此时同样将start到end-1位置的字符翻转。
            //另外,str.charAt(end) == ' ',同样将start到end-1位置的字符翻转。
            //注意,end == length 必须在 str.charAt(end) == ' ' 之前,否则 str.charAt(end)数组越界
            if(end == length || str.charAt(end) == ' ')
            {
                //当end的位置是空格字符的时候,start位置到end-1位置是一个字符串,将其字符翻转
                reverse(chars , start , end-1);
                start = end+1;//此时,将start指向下一个字符的起始位置
            }
            end++;//不管当前end位置是不是空格字符,每一次循环都要将end+1
        }
        //最后,将整个数组翻转
        reverse(chars ,0 , length-1);

        return new String(chars);
    }

    //翻转字符数组 start到end 位置的字符
    private void reverse(char arr[] , int start , int end)
    {
        //当start<end的时候,持续交换,直到start=end
        for (; start < end ; start++,end--)
        {
            swap(arr , start , end);
        }
    }
    //交换字符数组 arr的n位置与m位置的租房
    private void swap(char arr[] , int n , int m)
    {
        char temp = arr[n];
        arr[n] = arr[m];
        arr[m] = temp;
    }

题目45 扑克牌顺子

  题目描述: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。

  题目解析:传入一个数组,数组内元素值可能为:0-13 的任意一个数,其中0代表“癞子”,可以代表任意数,判断传入的数组内的元素是否是连续的?

  分析
1)将数组内元素排序;
2)找出数组内 0 的个数 cnt;
3)从第一个非0元素开始遍历,如果有2个元素相同,说明不是顺子,直接返回false;否则,当相邻的2个元素之间差大于1的时候,假设大的值是 n,那么 从 cnt 中取出 n-1 个癞子来补充2个元素之间的“空隙”。
4)最后,遍历完所有元素,如果 cnt>=0,说明癞子够补充序列之间的“空隙”,那么返回true,否则返回false。

public boolean isContinuous(int [] numbers)
    {
        //首先,顺子的元素个数最少为5,当数组长度小于5,直接返回false,不是顺子!
        if(numbers.length < 5)
            return false;

        Arrays.sort(numbers);//堆数组元素进行排序
        int cnt = 0;//用于统计0的个数

        for (int i = 0; i < numbers.length ; i++)
        {
            if(numbers[i] == 0)
                cnt++;
            else
                break;
        }

        // 使用癞子去补全不连续的顺子
        /**
         * 从第一个不是0的元素开始查找,就是从数组 cnt 位置开始查找.
         * 1)如果有2个元素相同,说明不是顺子,直接返回false;
         * 2)当 numbers[i+1]>numbers[i]的时候,统计2个元素之间的“间隙”,用0补充。(数组已经排序,不会有 numbers[i+1]<numbers[i] 的情况)
         */
        for (int i = cnt; i < numbers.length-1 ; i++)
        {
            if(numbers[i+1] == numbers[i])
                return false;
            //numbers[i+1]与numbers[i]之间差值为1,连续;大于1(n,n=numbers[i+1]-numbers[i]),则cnt需要取n-1个0补充“间隙”,cnt-(n-1)
            //即cnt = cnt - (numbers[i+1]-numbers[i]-1)
            cnt -= numbers[i+1]-numbers[i]-1;
        }

        //当剩余癞子(0)的数目大于等于0,说明癞子够用,多余的部分可以补充在数组头部或者尾部,返回true
        return cnt>=0;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值