剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer
目录
题目描述:小明很喜欢数学,有一天他在做数学作业时,要求计算出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+1;
3、注意,对于任意正整数,他们只能作为满足条件的序列的起点或者终点一次!!比如对于满足条件的序列: 起点为 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
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,即从curSumsum,说明以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
{
end++;//右移end以增大序列和
}
}
return ret;
}
题目描述:输入一个递增排序的数组和一个数字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
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
}
题目描述:汇编语言中有一种移位指令叫做循环左移(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
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;
}
题目描述:牛客最近来了一个新员工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
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
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;
}
题目描述: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]
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;
}