写写《剑指offer》里面比较新奇的解法(第一次刷题见过的)
Map
48.最长不含重复字符的子字符串
HashMap解法:
- 重点解释这一句:i=Math.max(i,map.get(s.charAt(j))+1); 为什么要和i比较max
- 防止i指针左移
- 考虑 abba 这种情况,遍历到第二个a的时候map中 a:0,b:2,i=2,map.get(‘a’)+1=1,而此时i明显需要大于等于2,所以用一个max函限制一下i指针不可以左移
public int lengthOfLongestSubstring(String s) {
int res=0;
Map<Character,Integer> map=new HashMap<>();
for(int j=0,i=0;j<s.length();j++){
if(map.containsKey(s.charAt(j))){
i=Math.max(i,map.get(s.charAt(j))+1);
//map.containsKey(s.charAt(j))不知道包含的s.charAt(j)是在i指针前面还是后面
}
map.put(s.charAt(j),j);
res=Math.max(res,j-i+1);
}
return res;
}
找规律
43. 1~n整数中1出现的次数
思路:递归
- 如n=1234,high=1, pow=1000, last=234,可以将数字范围分成两部分:1-999和1000-1234
- 1-999这个范围1的个数是f(pow-1)
- 1000~1234这个范围1的个数需要分为两部分:
- 千分位是1的个数:千分位为1的个数刚好就是(last+1),注意,这儿只看千分位,不看其他位
- 其他位是1的个数:即是234中出现1的个数,为f(last)
- 所以全部加起来是f(pow-1) + last + 1 + f(last);
- 例子如3234,high=3, pow=1000, last=234,可以将数字范围分成两部分1-999,1000-1999,2000-2999和3000-3234
- 1-999这个范围1的个数是f(pow-1)
- 1000~1999这个范围1的个数需要分为两部分:
- 千分位是1的个数:千分位为1的个数刚好就是pow,注意,这儿只看千分位,不看其他位
- 其他位是1的个数:即是999中出现1的个数,为f(pow-1)
- 2000~2999这个范围1的个数是f(pow-1)
- 3000~3234这个范围1的个数是f(last)
- 所以全部加起来是pow + highf(pow-1) + f(last);
public int countDigitOne(int n) {
return count(n);
}
private int count(int n){
//n=high*pow+low
if(n<1) return 0;
String s=String.valueOf(n);
int high=s.charAt(0)-'0';//最高位,分为是1和不是1的情况
int pow=(int)Math.pow(10,s.length()-1);
int low=n-high*pow;
if(high==1){
return count(pow-1)+count(low)+1+low;
}else{
return count(pow-1)*high+pow+count(low);
}
}
44. 数字序列中某一位的数字
思路:
- 确定 n 所在 数字 的 位数 ,记为 digit;
- 确定 n 所在的 数字 ,记为 num ;
- 确定 n 是 num 中的哪一数位,并返回结果。
public int findNthDigit(int n){
int digit=1;
long start=1;
long count=9;
while(n>count){
n-=count;
digit+=1;
start*=10;
count=digit*start*9;
}
//此时n是从start开始计数的,位数是digit
long num=start+(n-1)/dight;
return Long.toString(num).charAt((n-1)%digit)-'0';
}
61.扑克牌中的顺子
思路:
- 不能有重复
- 将5个数排序,统计0的个数,且第五张数字的值减去第一张非0的牌数字的值小于5,因为大于等于5无论怎么用0填补也无法凑成顺子
- 一个for搞定
public boolean isStraight(int[] nums) {
Arrays.sort(nums);
int joker=0;
for(int i=0;i<=3;i++){
if(nums[i]==0){
joker++;
}else if(nums[i]==nums[i+1]){
return false;
}
}
return nums[4]-nums[joker]<5;
}
约瑟夫环
62. 圆圈中最后剩下的数字
public int lastRemaining(int n, int m) {
//(0+m)%上一轮长度
int ans=0;
for (int i=2;i<=n;i++){
ans=(ans+m)%i;
}
return ans;
}
状态机
56 - I. 数组中数字出现的次数
题目:一个整型数组 nums 里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
思路:
- 数组所有数字做异或操作,其结果等于这两个不同的数字做异或操作,且结果一定不为0,故我们只要找到这两个数在某一位的不同就可以区分开来
- 找最后一位1的操作是mask=x&(-x)
public int[] singleNumbers(int[] nums) {
int xor = nums[0];
for(int i=1;i<nums.length;i++){
xor ^= nums[i];
}
int lastOnePosition = xor & (-xor);
int ans1 = 0,ans2 = 0;
for(int num :nums){
if((num&lastOnePosition) == lastOnePosition){
ans1 ^= num;
}else{
ans2 ^= num;
}
}
return new int[]{ans1^0 ,ans2^0};
}
56 - II. 数组中数字出现的次数(难)
Krahets大神解法
题目:在一个数组 nums 中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
思路:有限状态自动机,由于二进制只能表示 0, 1 ,因此需要使用两个二进制位来表示 3 个状态。设此两位分别为 two , one,状态转变为 00->01->10->00…
public int singleNumber(int[] nums) {
int ones = 0, twos = 0;
for(int num : nums){
ones = ones ^ num & ~twos;
twos = twos ^ num & ~ones;
}
return ones;
}
贪心算法
14- II. 剪绳子 II
public int cuttingRope(int n) {
if (n < 4) {
return n - 1;
}
int mod = (int) 1e9 + 7;
long res = 1;
while (n > 4) {
res *= 3;
res %= mod;
n -= 3;
}
return (int) (res * n % mod);
}
位运算
16.二进制中1的个数
n&(n−1) 解析: 二进制数字 n最右边的 1 变成 0 ,其余不变
public int hammingWeight_best(int n) {
int res = 0;
while(n!=0){
n&=(n-1);
res++;
}
return res;
}
栈
31. 栈的压入、弹出序列
不断用popped数组的值和栈顶比较即可,就提一个点:stack在pop的时候一定要检查是否为空!
public boolean validateStackSequences(int[] pushed, int[] popped) {
Stack<Integer> stack = new Stack<>();
int index = 0;
for (int i = 0; i < pushed.length; i++) {
stack.push(pushed[i]);
//pop一定要判断stack非空
while (!stack.isEmpty() && index < popped.length && stack.peek() == popped[index]) {
stack.pop();
index++;
}
}
return stack.isEmpty();
}
33. 判断数组是否为二叉搜索树的后序遍历序列(难)
思路:辅助单调栈(借助后序遍历倒序)
- 后序遍历顺序为 “左、右、根”
- 后序遍历倒序类似先序遍历的镜像 ,即先序遍历为 “根、左、右” 的顺序,而后序遍历的倒序为 “根、右、左” 顺序。
public boolean verifyPostorder(int[] postorder) {
Stack<Integer> stack=new Stack<>();//单调栈,存放递增序列
int root=Integer.MAX_VALUE;//把root看成MAX_VALUE的左子树
for(int i=postorder.length-1;i>=0;i--){
if(postorder[i]>root) return false;
while (!stack.isEmpty()&&stack.peek()>postorder[i]){
root=stack.pop();
//这里是查找postorder[i]的父节点root,因为root是单调栈中大于postorder[i]的最近的一个值
}
stack.push(postorder[i]);
}
return true;
}
快速幂
16.数值的整数次方
public static double myPow(double x, int n){
if(n==0) return 0;
long b = n;//防止取n=-n溢出
if(b<0){
b=-b;
x=1/x;
}
double res=1.0;
while(b>0){
//判断奇数
if((b&1)==1){
res*=x;
}
x*=x;
b>>=1;
}
return res;
}
17.打印从1到最大的n位数(重点)
法一:不考虑大数问题的版本,自己实现一个快速幂
public int[] printNumbers(int n) {
int size=mypow(10,n)-1;
int[] res=new int[size];
for(int i=0;i<size;i++){
res[i]=i+1;
}
return res;
}
public int mypow(int base,int n){
if(base==0) return 0;
int res=1;
while(n>0){
if((n&1)==1)
res*=base;
base*=base;
n>>=1;
}
return res;
}
法二:考虑大数问题,用字符串来做(推荐)
//n可能非常大,需要char数组来表示
public void print1ToMaxOfNDigits(int n) {
if (n < 0) return;
char[] number = new char[n];
print1ToMaxOfNDigits(number, 0);
}
private void print1ToMaxOfNDigits(char[] number, int n) {
if (n == number.length) {
printNumber(number);
return;
}
for (int i = 0; i < 10; i++) {
number[n] = (char) (i + '0');
print1ToMaxOfNDigits(number, n + 1);
}
}
private void printNumber(char[] number) {
String a = String.valueOf(number);
int n = Integer.valueOf(a);
System.out.println(n);
/*
int index = 0;
while (index < number.length && number[index] == '0') {
index++;
}
while (index < number.length) {
System.out.print(number[index++]);
}
System.out.println();
*/
}
数值字符串正则问题
20. 表示数值的字符串
标志位判断法,推荐
public boolean isNumber(String s) {
if(s==null || s.length()==0) return false;
boolean numSeen=false;
boolean dotSeen=false;
boolean eSeen=false;
char[] c=s.trim().toCharArray();
for (int i = 0; i < c.length; i++) {
if(c[i]>='0' && c[i]<='9'){
numSeen=true;
}else if(c[i]=='.'){
if(dotSeen || eSeen){
return false;
}
dotSeen=true;
}else if(c[i]=='e'||c[i]=='E'){
if(eSeen||!numSeen){
return false;
}
eSeen=true;
numSeen=false;//重置,确保e之后出现数字
}else if(c[i]=='+'||c[i]=='-'){
if(i!=0 && c[i-1]!='e'&&c[i-1]!='E'){
return false;
}
}else {
return false;
}
}
return numSeen;
}
67. 把字符串转换成整数
- 符号位: 三种情况,即 ‘’++’’ , ‘’-−’’ , ''无符号" ;新建一个变量保存符号位,返回前判断正负即可。
- 非数字字符: 遇到首个非数字的字符时,应立即返回。
- 在每轮数字拼接前,判断 res 在此轮拼接后是否超过 2147483647,若超过则加上符号位直接返回。设数字拼接边界 bndry = 2147483647 // 10 = 214748364,则以下两种情况越界:
- res>bndry ,执行拼接10×res≥2147483650越界
- res=bndry,x>7
public static int strToInt(String str) {
char[] c = str.trim().toCharArray();
if (c.length == 0) return 0;
int sign = 1;//正负标志位
int res = 0;
int i = 1;
int boundary = Integer.MAX_VALUE / 10;
if (c[0] == '-') sign = -1;
else if (c[0] != '+') i = 0;
for (int j = i; j < c.length; j++) {
if (c[j] < '0' || c[j] > '9') break;
if (res > boundary || res == boundary && c[j] > '7') {//对于正数
return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
}
res = res * 10 + c[j] - '0';
}
return sign * res;
}
摩尔投票法
39. 数组中出现次数超过一半的数字
public int majorityElement(int[] nums) {
/*摩尔投票法,时间O(N),空间O(1)*/
int x = 0;//假设的众数
int votes = 0;
for(int num : nums){
if(votes == 0) x = num;
votes += num == x ? 1 : -1;
}
return x;
}
快排
40. 最小的k个数
快排或者大根堆(每次弹出最大的数)
在面试中,另一个常常问的问题就是这两种方法有何优劣。看起来分治法的快速选择算法的时间、空间复杂度都优于使用堆的方法,但是要注意到快速选择算法的几点局限性:
第一,算法需要修改原数组,如果原数组不能修改的话,还需要拷贝一份数组,空间复杂度就上去了。
第二,算法需要保存所有的数据。如果把数据看成输入流的话,使用堆的方法是来一个处理一个,不需要保存数据,只需要保存 k 个元素的最大堆。而快速选择的方法需要先保存下来所有的数据,再运行算法。当数据量非常大的时候,甚至内存都放不下的时候,就麻烦了。所以当数据量大的时候还是用基于堆的方法比较好。
//快排变形
public int[] getLeastNumbers(int[] arr, int k) {
if(arr.length==0||k<1) return new int[0];
return quickSearch(arr,0,arr.length-1,k-1);
}
private int[] quickSearch(int[] arr,int lo,int hi,int k){
int m=partition(arr,lo,hi);
if(m==k){
return Arrays.copyOf(arr,k+1);
}else if(m>k){
return quickSearch(arr,lo,m-1,k);
}else{
return quickSearch(arr,m+1,hi,k);
}
}
private int partition(int[] arr,int lo,int hi){
int i=lo,j=hi+1;
int v=arr[lo];
while(true){
while(++i<hi&&arr[i]<v);
while(--j>lo&&arr[j]>v);
if(i>=j) break;
int temp=arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
arr[lo]=arr[j];
arr[j]=v;
return j;
}
public int[] getLeastNumbers(int[] arr, int k){
if(arr.length==0 || k==0){
return new int[0];
}
Queue<Integer> queue = new PriorityQueue<>((v1,v2)->v2-v1);//改成大根堆需要重写一下比较器
for(int num:arr){
if(queue.size()<k){
queue.offer(num);
}else {
if(num<queue.peek()){
queue.poll();
queue.offer(num);
}
}
}
int[] res = new int[queue.size()];
int index=0;
for(int num:queue){
res[index++]=num;
}
return res;
}
45. 把数组排成最小的数
数组转化为String数组,通过String的拼接来排序