徒手挖地球十周目
NO.28 实现strStr() 简单
吐槽一下:这道题的难度标识实在是令人纠结,虽然练习题目才是核心,但是题目的难度标识对于我这样的初学者也是不可缺少的参考标识。
题还没读完,脑海里跳出的第一个想法居然是直接用indexOf()。。。还好立刻就否决了这个想法,但是还是在好奇心的驱使下leetcode提交了一次这个"算法"。。。0ms 100%。。。那就等刷完这道题,读一下indexOf()的源码吧!^_^
思路一:双指针暴力法 1. 用i和j分别指向haystack字符串和needle字符串的开头。2. 如果haystack的i号字符等于needle的j号字符,则j和i都向后移动。3. 如果haystack的i号字符不等于needle的j号字符,则j回到needle字符串的开头,i也回溯之后继续比较。4. 循环直至haystack遍历完毕或者needle遍历完毕。5. 最后如果j指针没有遍历我能needle则说明haystack串不包含needle串,返回-1;反之则返回i-j。
public int strStr(String haystack, String needle) {
if (haystack==null||needle==null)throw new NullPointerException();
int i=0,j=0;
for (;i<haystack.length()&&j<needle.length();i++){
if (haystack.charAt(i)==needle.charAt(j)){
j++;
}else {
i=i-j;
j=0;
}
}
return j<needle.length()?-1:i-j;
}
时间复杂度:O(m*n)
思路二:Sunday法 该算法的思路相较于KMP十分容易理解,1. 构建一张偏移表,该表主要记录了模式串中的每一个字符,以及每个字符在模式串中出现的最右位置到尾部的距离+1,未在模式串中出现的字符对应的偏移距离都是"模式串长度+1"。2. 有了偏移表之后开始比较,用idx作为当前查询索引,每次截取目标字符串的[idx,idx+模式串长度]子串和模式串比较,如果相等则返回idx。3. 如果不相等,查看子串在目标串中的后一个字符c是否存在于偏移表中,如果存在则idx=idx+偏移表[c];如果不存在idx=idx+模式串长度+1。循环直至idx+模式串长度>目标字符串长度。
public int strStr(String haystack,String needle){
if (needle.equals(""))return 0;
int hLen=haystack.length(),nLen=needle.length();
if (hLen<nLen)return -1;
// 创建偏移表
Map<Character,Integer> offsetMap=new HashMap<>();
for (int i=0;i<nLen;i++){
offsetMap.put(needle.charAt(i),nLen-i);
}
// 开始查找模式串
int idx=0;
// 循环直至idx+模式串长度>目标字符串长度
while (idx+nLen<=hLen){
// 截取目标字符串
String cutHay = haystack.substring(idx, idx + nLen);
// 如果子串和模式串相等,则返回idx
if (cutHay.equals(needle)){
return idx;
}else {
// 边界处理,如果子串后面已经没有字符,即已经是最后一组子串,则搜索失败
if(idx+nLen>=hLen)return -1;
// 如果子串在目标串中的后一个字符c是否存在于偏移表中
if (offsetMap.containsKey(haystack.charAt(idx+nLen))){
idx+=offsetMap.get(haystack.charAt(idx+nLen));
}else {
idx+=nLen+1;
}
}
}
return -1;
}
时间复杂度:O(m*n), 但是该算法的平均情况也可以达到O(n)。
思路三:KMP法 数据结构课的时候没学透彻,趁这次机会好好学习一下。作为一只弱鸡,就不瞎扯KMP了,直接找个"巨人肩膀"窥探一下KMP的原理。经过多方查找,最终通过阮一峰的一篇文章艰难入门KMP算法。
public int strStr(String haystack, String needle) {
int tarsize = needle.length(); //短字符串
int scrsize = haystack.length(); //长字符串
if(tarsize == 0) //短字符串是0
return 0;
if(tarsize > scrsize) //短字符串 比 长字符串长
return -1;
if(tarsize == scrsize && needle.equals(haystack)) //两个字符串相同
return 0;
int start = 0; //长字符串的和短字符串比较的第一个字符
int i = 0; //长字符串的和短字符串正在比较的相对第一个位置
int[] next = getNext(needle); //得到next数组
while (start <= scrsize - tarsize)
{
if(haystack.charAt(start + i) == needle.charAt(i))
{
i++;
if(i == tarsize)
return start;
}
else
{
start = start + i - next[i];
i = i > 0 ? next[i] : 0;
}
}
return -1;
}
public int[] getNext(String needle)
{
int tarsize = needle.length();
int[] next = new int[tarsize];
next[0] = -1;
if(tarsize > 1)
next[1] = 0;
int i = 2;
int j = 0;
while(i < tarsize)
{
if(needle.charAt(i-1) == needle.charAt(j)) //
{
next[i] = j+1;
j++;
i++;
}
else if(j > 0)
{
j = next[j];
}
else
{
next[i] = 0;
i++;
}
}
return next;
}
时间复杂度:O(m+n)
NO.29 两数相除 中等
看了很多人的题解,学习到了很多。但是有些题解我不敢苟同,例如用long存储变量的题解,题目明确说明:我们环境只能存储32位有符号整数;需要用乘法改变正负号的题解,第一句就说了不能用乘法。。等等
思路一:二分法除数翻倍 被除数中有N个除数,那么商就是N(用减法来实现除法,新被除数=被除数-除数&商+=1)。如果每次被除数只减一个除数,虽然可以实现除法,但是效率太低,在leetcode上也会TLE。所以采用每次除数翻倍(商也不再是每次+1)的方法。
这道题的思路并不难,但是本题有很多细节需要注意和学习:
-
商的范围需要注意,小心溢出。这里可以采用先将除数和被除数转换成负数并且用负数商来进行运算,运算结束再根据除数和被除数原本的符号决定商的符号(负号直接返回,正号需要判断符号转变后是否溢出)。
-
如何得到商的符号:判断除数和被除数异或之后的符号即可。
-
如何获得相反数:反码+1=补码。分享一篇文章,对这里有疑惑的同学可以看看——补码(为什么按位取反再加一)
举个栗子,17/3,除数和被除数都转换为负数(反码+1=补码),即-17/-3,先用-17-(-3)=-14,商+=-1;
除数翻倍-14-(-6)=-8,商+=-2;
除数翻倍,此时的除数-12<被除数-8,所以除数重置为-3;
继续-8-(-3)=-5,商+=-1;
除数翻倍,此时的除数-6<被除数-5,所以除数重置为-3;
继续-5-(-3)=-2,商+=-1;
除数翻倍,此时的除数-6<被除数-2,所以除数重置为-3;
但是初始的除数-3<被除数-2,所以计算结束。
最后根据除数和被除数原本的符号决定商的符号,结果应该是"正正得正",判断此时的负数商符号转变后是否溢出,负数商不等于32位有符号整形最小值-2147483648,所以可以直接转换为正数,返回负数商的相反数。
public int divide(int dividend, int divisor) {
if (divisor==0)throw new IllegalArgumentException();
//将除数和被除数异或之后,得到商的符号
boolean isPositive=(dividend^divisor)>=0;
//将除数和被除数都转化为负数
if (dividend>0)dividend=opposite(dividend);
if (divisor>0)divisor=opposite(divisor);
// 商用负数来表示,这样可以处理Integer.MIN_VALUE的情况
int ans=0;
while (dividend<=divisor){
int tempDivisor=divisor;
int count=-1;
//这里注意需要对tempDivisor是否为负数做判断,因为tempDivisor有可能会溢出
while (tempDivisor<0&÷nd<=tempDivisor){
//被除数-除数
dividend-=tempDivisor;
ans+=count;
//除数翻倍
tempDivisor+=tempDivisor;
count+=count;
}
}
//对返回值进行处理,这里也可以使用三目运算符完成
if (isPositive){
if (ans==Integer.MIN_VALUE){
return Integer.MAX_VALUE;
}else{
return opposite(ans);
}
}else {
return ans;
}
}
//x的反码+1,得到x的相反数
private int opposite(int x){
return ~x+1;
}
时间复杂度:O(logN),除数是 1,每次减一个除数,我们将减 n 次,但因为每次除数都翻倍了,所以共减了log(n)次。
NO.31 下一个排列 中等
思路一:一次遍历法 经过观察发现,降序序列没有更大的排序,例如[9,4,3,2,1]、[7,5,4,2,]等等。
- 从数组nums的最后一个元素开始,逆序遍历寻找第一个非递增元素nums[i]。例如[1,5,4,7,6,5,3,1],逆序遍历到第一个非递增元素是4。
- 此时nums[i]元素的右边是一个递减序列,逆序遍历这个递减序列找到大于nums[i]的元素中最小的一个nums[j](此时右边这个序列是有序的,利用这一性质)。例如,上例中4右边的序列中找到5。
- 交换nums[i]和nums[j],此时序列变为[1,5,5,7,6,4,3,1]。然而这个序列并不是我们需要的"下一个排列",将i之后的元素序列翻转后得到[1,5,5,1,3,4,6,7]才是最终结果。
public void nextPermutation(int[] nums) {
// 逆序找到第一个非递增元素nums[i]
int i=nums.length-2;
while (i>=0&&nums[i]>=nums[i+1]){
i--;
}
// 如果nums本身非递减序列,逆序找到nums[i]元素右边的递减序列中第一个大于nums[i]的元素
if (i>=0){
int j=nums.length-1;
while (j>=0&&nums[j]<=nums[i]){
j--;
}
// 交换nums[i]和nums[j]
swap(nums,i,j);
}
// 最后翻转nums[i]右边的元素序列,得到参数数组nums的下一个更大的排列
reverse(nums,i+1);
}
// 从start号索引开始翻转nums数组元素
public void reverse(int[] nums,int start){
int i=start,j=nums.length-1;
while (i<j){
swap(nums,i,j);
i++;
j--;
}
}
// 交换nums数组的i和j号索引元素
private void swap(int[] nums, int i, int j) {
int temp=nums[i];
nums[i]=nums[j];
nums[j]=temp;
}
时间复杂度:O(n),在最坏的情况下,只需要对整个数组进行两次扫描