【剑指Offer】剑指offer题目
本文为《剑指Offer》刷题笔记的总结篇,计划将牛客网上《剑指Offer》的66道题刷了一遍,以博客的形式整理了一遍,这66道题属于相对基础的算法题目,对于刷题练手是很好的实践,接下来会继续回到LeetCode,争取每天拿出一个小时,刷一到两道题。
如果有什么问题,请多多指教。
文章目录
前言
主要对这66道题做一个总结,整体来看,这66道题,涉及到了常用的数据结构:数组、字符串、链表、树、二叉树、栈、队列,还有在编程中经常用到的数据操作和算法:循环、递归、查找、排序、回溯、动态规划、位运算。以下对这66道题做一个归类。 从这些题目中,我们可以学习到以下一些解题经验: 首先要真正理解题意,对于一些不确定的情况,比如输入输出格式,可以追问。 思考要全面,对于特殊情况,比如指针为null,输入为空等等情况,要充分考虑,保证代码的完整性和鲁棒性。 当遇到一个比较复杂的问题时,可以通过画图、举例或者分解来考虑,从具体的实例中总结普遍规律。 注重效率的考量,这66道题用蛮力法解决可能都不是很难,但是我们需要考虑的是时间效率和空间效率的平衡,以空间换时间有时候是一个不错的选择。 常用数据结构和数据操作是基础,要重点掌握,如树的遍历,排序,查找,递归等操作,在题目中反复用到,要深刻理解算法思想。一、字符串(共9道题目)
【剑指Offer】2、替换空格
解题思路:
对于这个题目,我们首先想到原来的一个空格替换为三个字符,字符串长度会增加,因此,存在以下两种不同的情况:
(1)允许创建新的字符串来完成替换。
(2)不允许创建新的字符串,在原地完成替换。(这种看过别人通过双指针的形式展示。)
第一种情况比较简单。对于第二种情况,有以下两种解法:
(1)时间复杂度为O(n^2)的解法。
从头到尾遍历字符串,当遇到空格时,后面所有的字符都后移2个。
(2)时间复杂度为O(n)的解法。
可以先遍历一次字符串,这样可以统计出字符串中空格的总数,由此计算出替换之后字符串的长度,每替换一个空格,长度增加2,即替换之后的字符串长度为原来的长度+2*空格数目。接下来从字符串的尾部开始复制和替换,用两个指针P1和P2分别指向原始字符串和新字符串的末尾,然后向前移动P1,若指向的不是空格,则将其复制到P2位置,P2向前一步;若P1指向的是空格,则P1向前一步,P2之前插入%20,P2向前三步。这样,便可以完成替换,时间复杂度为O(n)。
看下别人的图:
解题一:
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param s string字符串
# @return string字符串
#
class Solution:
def replaceSpace(self , s ):
n=len(s)
s1=list(s)
i=0
while(i<n):
if s1[i]==" ":
s1[i]="%20"
i+=1
else:
i+=1
continue
return "".join(s1)
当然这种也很慢。
或者调包求解:
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param s string字符串
# @return string字符串
#
class Solution:
def replaceSpace(self , s ):
return s.replace(" ","%20")
解法二:
不允许创建新的字符串,在原地完成替换。
//第一种情况:创建新的字符串实现:
public String replaceSpace(StringBuffer str) {
String res="";
for(int i=0;i<str.length();i++){
char c=str.charAt(i);
if(c==' ')
res += "%20";
else
res += c;
}
return res;
}
//第二种情况:原地替换,O(n)的解法
public String replaceSpace(StringBuffer str) {
if(str==null)
return null;
int numOfblank = 0;//空格数量
int len=str.length();
for(int i=0;i<len;i++){ //计算空格数量
if(str.charAt(i)==' ')
numOfblank++;
}
str.setLength(len+2*numOfblank); //设置长度
int oldIndex=len-1; //两个指针
int newIndex=(len+2*numOfblank)-1;
while(oldIndex>=0 && newIndex>oldIndex){
char c=str.charAt(oldIndex);
if(c==' '){
oldIndex--;
str.setCharAt(newIndex--,'0');
str.setCharAt(newIndex--,'2');
str.setCharAt(newIndex--,'%');
}else{
str.setCharAt(newIndex,c);
oldIndex--;
newIndex--;
}
}
return str.toString();
}
解法三:
python 实现(但是python双指针感觉就是个累赘,记得以前jieba分词用的拼接字符串使用的方法):
#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
#
# @param s string字符串
# @return string字符串
#
class Solution:
def replaceSpace(self , s ):
res =[]
for c in s:
if c==" ":
res.append("%20")
else:
res.append(c)
return "".join(res)
总结
本题有实际的现实意义,在网络编程中,如果URL参数中含有特殊字符,则可能导致服务器端无法获得正确的参数值,因此可以通过替换的方法将其转换为可以识别的字符。转换规则就是%后面加上该字符ASCII码的16进制表示,如这里的空格,ASCII码是0x20(十进制的32),所以可以替换为%20,其实还可以使用正则匹配进行替换。后续在补充。
【剑指Offer】27、字符串的排列
解法一:递归法
我尝试第一个方法是把所有的可能都写出来,然后去重。
首先:
但是会有重复的排序:
python实现逻辑:
class Solution:
##查找字符串的全排列
def allstring(self, ss):
l=[]
for i in range(len(ss)):
temp1 = ss[i]
temp2 = ss[:i] + ss[i+1:]
tempt3 = self.allstring(temp2)
if tempt3:
for x in tempt3:
l.append(temp1+x)
else:
l.append(temp1)
return l
def Permutation(self, ss):
result=self.allstring(ss)
result=list(set(result))
return result
if __name__=="__main__":
s=Solution()
res=s.Permutation('abc')
print(res)
解法二:java 实现:
对于这个问题,我们同样可以采用分解的办法。要求整个字符串的排列,可以看成两步:第一步:求所有可能出现在第一个位置的字符,即把第一个字符与后面的字符依次交换。第二步:固定一个字符,求后面所有字符的排列。
很明显,求后面所有字符的排列,我们仍然可以把所有的字符分成两个部分:后面的字符的第一个字符以及这个字符之后的所有字符,然后把第一个字符逐一与其后的字符交换。因此,这是典型的递归思路。
import java.util.*;
public class Solution {
ArrayList<String> res=new ArrayList<>();
public ArrayList<String> Permutation(String str) {
if(str==null||str.length()==0)
return res;
Permutation(str.toCharArray(),0);
Collections.sort(res);
return res;
}
public void Permutation(char[] strArr,int begin){
if(begin==strArr.length-1){
String s=String.valueOf(strArr);
if(!res.contains(s))
res.add(s);
}else{
for(int i=begin;i<strArr.length;i++){ //依次将后面的与begin交换
swap(strArr,begin,i);
Permutation(strArr,begin+1);
swap(strArr,begin,i);
}
}
}
public void swap(char[] strArr,int a,int b){
char temp=strArr[a];
strArr[a]=strArr[b];
strArr[b]=temp;
}
}
解法三:看到牛客网大神写的思路:
输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
首先读题我们发现,这种从固定的数组/String 里的取出元素进行排列组合,是经典的permutation题目。然后我们发现,这题的要求是 需要我们返回所有不重复的排列组合,而不是返回有多少种排列组合,所以我们需要建一个String的Arraylist来存储所有排列出的不重复的字符串。
除此之外,我们新建一个辅助method,用于不停的循环向下寻找所有可能的排列组合。在这个辅助method里,有三个arguments。第一个是str,即当前我们剩下可以取的string,第二个是cur,即我们当前所拥有的字符串,第三个是result,是我们符合条件的字符串的合集。
在这个辅助method中,我们首先需要检测,我们还有没有能取的string,如果没有,我们便可以开始确认我们当前所拥有的字符串有没有被放进最后的Arraylist result中,如果没有,我们便将他加进去。接下来,当我们还有能取的string时,我们就可以开始进行循环。对于现在所剩下的string其中的每一个字符串,我们都将他尝试与我们现有的string cur进行组合,所以循环的argument的cur 此时变成cur+Str.charAt(i)。然后因为我们已经把当前位于i的字符取过了,我们的第一个argument需要被更新为除这个的剩下的string,我们这里运用了substring来取i之前的string和i之后的string进行相加。
如此循环,直到我们所有的可能性都被取完。
这个方法同样也适用于我们需要排列组合的时一个数组而非string的情况。
具体代码如下:
import java.util.ArrayList;
public class Solution {
public ArrayList<String> Permutation(String str) {
ArrayList<String> result = new ArrayList<>();
if(str.length() == 0){
return result;
}
recur(str,"",result);
return result;
}
public void recur(String str,String cur,ArrayList<String> result){
if(str.length() == 0){
if(!result.contains(cur)){
result.add(cur);
}
}
for(int i = 0; i < str.length();i++){
recur(str.substring(0,i)+str.substring(i+1,str.length()),cur+str.charAt(i),result);
}
}
}
【剑指Offer】34、第一个只出现一次的字符
解法一:
# -*- coding:utf-8 -*-
class Solution:
def FirstNotRepeatingChar(self, s):
# write code here
s=list(s)
d={}
for i ,value in enumerate(s):
if s.count(value)==1:
return i
else:
continue
return -1
if __name__ == "__main__":
s = Solution()
res = s.FirstNotRepeatingChar("google")
print(res)
解法二:
参考博客大神的答案,
对于本题,这里给出以下三种解法:(1)用 HashMap 建立每个字符与其出现次数的映射,然后再依次遍历字符串时,找到第一个出现次数为1的字符,返回其位置即可。
(2)更进一步,因为该字符串全部是字母,所以可以用一个数组代替哈希表,数组下标就代表该字母。
(3)使用模式匹配从前(indexOf)和从后(lastIndexOf)匹配每一个字符,相等即为唯一。
//方法一:哈希表
public int FirstNotRepeatingChar(String s) {
Map<Character,Integer> map = new HashMap<Character,Integer>();
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
map.put(c,map.getOrDefault(c,0)+1);
}
for(int i=0;i<s.length();i++)
if(map.get(s.charAt(i))==1)
return i;
return -1;
}
//方法二:数组代替哈希表
public int FirstNotRepeatingChar(String str) {
if(str==null || str.length()==0)
return -1;
// A-Z对应的ASCII码为65-90,a-z对应的ASCII码值为97-122
int len=str.length();
int[] count=new int[58]; //122-65+1
for(int i=0;i<len;i++){
char c=str.charAt(i);
count[c-'A']++;
}
for(int i=0;i<len;i++){
char c=str.charAt(i);
if(count[c-'A']==1)
return i;
}
return -1;
}
//方法三:模式匹配
public int firstUniqChar(String s) {
for(int i=0;i<s.length();i++){
char c=s.charAt(i);
if(s.indexOf(c)==s.lastIndexOf(c))
return i;
}
return -1;
}
【剑指Offer】43、左旋转字符串
方法一:使用标准库
# -*- coding:utf-8 -*-
class Solution:
def LeftRotateString(self, s, n):
# write code here
s_list = list(s)
str1 = s_list[:n]
str2 = s_list[n:]
t = "".join(str2)
s1 = "".join(str1)
return t + s1
if __name__ == "__main__":
s = Solution()
res = s.LeftRotateString("abcXYZdef", 3)
print(res)
解法二:
对于本题,从最直观的角度我们首先可以想到暴力解法:每次移动一位,移动k次为止。对于每一次移动,其实就是将字符串第一个字符放到字符串末尾,而为了实现这一目标,需要将字符串其他位置的元素依次前移,因此,暴力解法时间复杂度为O(n^2)。
还是那句话:最容易想到的解法往往不是最优的。
进一步考虑,字符串左移k位,就相当于将字符串分为两部分,第一部分是前k位,另一部分是剩余的其他位,然后将这两部分交换顺序即可得到最后结果。因此,我们可以得到以下的三次反转算法:
将字符串分为两部分,即前k个字符和剩余的其他字符,然后分别对这两部分进行反转,然后再对整个字符串进行一次反转,这样得到的结果就是我们想要的循环左移之后的字符串。事实上,这并不难理解,前后两部分各自经历了两次反转,因此每一部分的顺序并没有改变,只是将前后两部分进行了交换。对字符串进行一次反转,需要一次扫描,因此次算法时间复杂度为O(n)。
举例:
输入字符串"abcdefg"和数字2,该函数将返回左旋转2位得到的结果"cdefgab";
第一步:翻转字符串“ab”,得到"ba";
第二步:翻转字符串"cdefg",得到"gfedc";
第三步:翻转字符串"bagfedc",得到"cdefgab";
//方法一,依次左移,每次移动一位
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
for(int i=0;i<n;i++){ //只控制循环次数
char c=strArr[0];
for(int j=0;j<len-1;j++) //拿出第一个,后面依次前移,复杂度O(n^2)
strArr[j]=strArr[j+1];
strArr[len-1]=c;
}
return new String(strArr);
}
//方法二:三次反转
public String LeftRotateString(String str,int n) {
char[] strArr=str.toCharArray();
int len=strArr.length;
if(len<=0)
return str;
n=n%len;
reverseStr(strArr,0,n-1);
reverseStr(strArr,n,len-1);
reverseStr(strArr,0,len-1);
return new String(strArr);
}
public void reverseStr(char[] array,int begin,int end){ //反转字符串,前后指针
for(;begin<end;begin++,end--){
char c=array[begin];
array[begin]=array[end];
array[end]=c;
}
}
参考思路,编写python程序实现:
模拟字符串左旋的写法,其实还可以用入栈出栈的想法去实现。
# -*- coding:utf-8 -*-
class Solution:
def LeftRotateString(self, s, n):
# write code here
if len(s) <= 0:
return str
str_list=list(s)
# 同时
for i in range(n):
t=str_list[0]
str_list.append(t)
str_list.pop(0)
return "".join(str_list)
if __name__ == "__main__":
s = Solution()
res = s.LeftRotateString("abcXYZdef", 3)
print(res)
【剑指Offer】44、翻转单词序列
解法一:
将字符串反过来输出: