剑指OFFER数组和字符串相关
注:顺序是先筛选分类再按LeeCode上的通过率排的,每题最后的总结代码都是在LeeCode上跑过的,应该没啥问题。但是思路中的代码都是直接在优快云编辑器里徒手敲的,若有笔误还烦请告知,蟹蟹~
03 数组中的重复数字
第一反应是用HashMap做,反正啥玩意都HashMap一把梭,顺便说一个优雅的往HashMap里放key来计数的操作:
map.put(i, map.getOrDefault(i, 0) + 1);
getOrDefault()
呢就是要是有你说的key
就返回value
,没有就返回你设置的默认值,我们这样设置在用HashMap计数的时候就超方便了。
class Solution {
public int findRepeatNumber(int[] nums) {
HashMap<Integer,Integer> map = new HashMap<>();
for(int num:nums){
map.put(num, map.getOrDefault(num, 0) + 1);
}
for(int key:map.keySet()){
if(map.get(key) > 1){
return key;
}
}
return -1;
}
}
然后发现这玩意时间跑过了7%的人…这玩个锤子,继续想方法优化吧。看到题目里这个条件:所有数字的范围都在0到n-1之间,这说明了我们可以一个数对应一个下标,要是有冲突就说明这个数是重复的。
class Solution {
public int findRepeatNumber(int[] nums) {
for(int i = 0; i < nums.length; i++){
if(nums[i] == i){ //若我这个数就在我这个坑,则继续
continue;
}else if(nums[i] == nums[nums[i]]){ //若我这个数与我想占得坑冲突了,那就是重复
return nums[i];
}else{ //我想占得坑里是别的数,就和他换一下,占好自己的坑
int tmp = nums[i];
nums[i] = nums[tmp];
nums[tmp] = tmp;
}
}
//找不到重复,返回-1
return -1;
}
}
速度变成了92%,这个优化还行。
53-I 在排序数组中查找数字
这种题基本上是要写二分查找的。二分查找这玩意是简单的思路,细节的魔鬼,恐怖的一匹,借着这题好好探究一下。
二分查找的等号在哪很关键,代表着我们要考虑在nums[mid] == target
的时候怎么做,在这题里,我们在nums[mid] == target
时,需要right = mid
,即把right指针往前压,导致其二分的范围越来越往前压,找左边界。
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
int count = 0;
while(left < right){
int mid = left + (right - left) / 2;
if(nums[mid] >= target){
right = mid;
}
if(nums[mid] < target){
left = mid + 1;
}
}
while(left < nums.length && nums[left++] == target){
count++;
}
return count;
}
}
再贴一个左右边界法来研究一下二分查找,此解答源自这里:
public class Solution {
public int search(int[] nums, int target) {
int len = nums.length;
if (len == 0) {
return 0;
}
int firstPosition = findFirstPosition(nums, target);
if (firstPosition == -1) {
return 0;
}
int lastPosition = findLastPosition(nums, target);
return lastPosition - firstPosition + 1;
}
private int findLastPosition(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len - 1;
while (left < right) {
// 注意,以下 nums[mid] < target 以及 nums[mid] == target 的情况可以合并
// 边界是 left = mid ,取中间数的时候必须 + 1
int mid = (left + right + 1) >>> 1;
if (nums[mid] < target) {
// mid 以及 mid 左边都不是,下一轮搜索区间在 [mid + 1, right]
left = mid + 1;
} else if (nums[mid] == target) {
// mid 可能是,mid 左边一定不是,下一轮搜索区间在 [mid, right]
left = mid;
} else {
// 此时 nums[mid] > target
// mid 以及 mid 右边都不是,下一轮搜索区间在 [left, mid - 1]
right = mid - 1;
}
}
return left;
}
private int findFirstPosition(int[] nums, int target) {
int len = nums.length;
int left = 0;
int right = len - 1;
while (left < right) {
int mid = (left + right) >>> 1;
if (nums[mid] < target) {
// mid 以及 mid 左边都不是,下一轮搜索区间在 [mid + 1, right]
left = mid + 1;
} else if (nums[mid] == target) {
// mid 可能是,mid 右边一定不是,下一轮搜索区间在 [left, mid]
right = mid;
} else {
// 此时 nums[mid] > target
// mid 以及 mid 右边都不是,下一轮搜索区间在 [left, mid - 1]
right = mid - 1;
}
}
if (nums[left] == target) {
return left;
}
return -1;
}
}
总结一下,在相等的情况下,你left = mid
就是把下一轮的空间压在右边了,找的是右边界;你right = mid
就是把下一轮空间压在左边了,找的是左边界。感觉我讲的不是很清楚,你还可以多找些专业的分析文章看看,我这里只是提醒你注意这块罢了。
53-II 0到n-1中缺失的数字
我的思路是按这个序列缺失了一个数走的,所有这个序列本来的长度应该是0~length,不用减一,这样最后出的时候发现0~n-1有序,那就直接指到n上去。
class Solution {
public int missingNumber(int[] nums) {
//缺失了一个数,所以本来应该有0-nums.length个数
int left = 0,right = nums.length;
while(left < right){
int mid = left + (right - left) / 2;
//如果出现错位了,那么首个错位的数字肯定在它左边,我们要压缩右边界
if(nums[mid] != mid){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
}
29 顺时针打印矩阵
这题也是个细节题,一看就会一写就废,先列个表:
主要解释一下判停那块,就是我做完从左向右了之后,下一步需要做从上到下了,在做从上到下之前我需要把t
加一,看一下它有没有超过b
,要是超过了就说明可以结束了,其余同理。这题主要玩的就是++i
和i++
,++i
是不多BB先加上1,然后再干活;i++
是先干活,干完之后等这句话结束了再加1。
class Solution {
public int[] spiralOrder(int[][] matrix) {
if(matrix.length == 0){
return new int[0];
}
int t = 0, b = matrix.length - 1, l = 0, r = matrix[0].length - 1;
int index = 0;
int[] res = new int[(b+1)*(r+1)];
while(true){
for(int i = l; i <= r; i++){
//我先把res[index] =matrix[t][i]给干完了,等这句话结束的分号时我再加一
res[index++] = matrix[t][i];
}
//我不管你是个啥我先加上1,然后再跟你b去比一比
if(++t > b) break;
for(int i = t; i <= b; i++){
res[index++] = matrix[i][r];
}
if(--r < l) break;
for(int i = r; i >= l; i--){
res[index++] = matrix[b][i];
}
if(--b < t) break;
for(int i = b; i >= t; i--){
res[index++] = matrix[i][l];
}
if(++l > r) break;
}
return res;
}
}
04 二维数组中的查找
兄弟萌这题不要从matrix[0][0]
开始走啊,你看matrix[0][0]
往右是比他大的往下还是比他大的,你说你是走右边还是走下边?他就是个弟中弟。兄弟萌matrix[0][n]
了解一下啊,你看这个15,往左走11比他小,往下走19比他大,绝对不会选择恐惧症,完美啊。对了,二维数组做之前先把行列想清楚,不然很容易写反,蛋疼。
class Solution {
public boolean findNumberIn2DArray(int[][] matrix, int target) {
if(matrix.length == 0 || matrix == null){
return false;
}
//咱们从matrix[0][n]开始走
int n = matrix[0].length - 1, m = 0;
while(n >= 0 && m < matrix.length){
if(matrix[m][n] > target){
//刚说了,目标值比他小,那就往左找
n--;
}else if(matrix[m][n] < target){
//目标值比他大,就往下找
m++;
}else{
//这可不就找到了
return true;
}
}
return false;
}
}
下面开始是三题字符串了,三题的通过率断崖式下跌,我开始慌了
58-II 左旋转字符串
class Solution {
public String reverseLeftWords(String s, int n) {
return s.substring(n) + s.substring(0, n);
}
}
双百,小朋友你是否有很多问号?先唠一下substring
是左闭右开,我在树的07 重建二叉树那题里唠过,那题用到了Arrays.copyOfRange
还记得不?反正写的时候实在不行你找个例子写在纸上,然后念叨左闭右开左闭右开截就完事了。我看了下大佬们的其他做法,好像都差不多,那就直接下一题吧。
58-I 翻转单词顺序
这题面经里好像也经常见哎,我用了正则表达式来解决多个空格的问题,其他的好像没啥好说的,记得几个常用的String方法吧:
//帮你把最前面和最后面的空格都去掉的无情杀手
str.trim();
//按照你给的内容把字符串拦腰截断的无情杀手二号
str.split("balabala");
//我们的老朋友,左闭右开的无情杀手三号
str.substring(1,2);
//可以变长变短的String的好基友sb同学
StringBuilder sb = new StringBuilder();
//我们想到sb同学就会想到他的特有技能:把东西加在他屁股后面
sb.append("ass");
//sb不做人啦,他要变回String不然八成没法return
sb.toString();
对了,所有对字符串的操作必须赋值回去,不然是不生效的,因为字符串是不可改变的。
//可以
s = s.trim();
//不可以
s.trim();
正则表达式可以看这里,下面上代码:
class Solution {
public String reverseWords(String s) {
s = s.trim();
//" +"可以匹配一个或多个空格
String[] strs = s.split(" +");
StringBuilder sb = new StringBuilder();
for(int i = strs.length - 1; i >= 0; i--){
sb.append(strs[i]);
sb.append(" ");
}
//这里注意我们最后还加了个空格,所以再trim一下把最后那个空格给它去掉
return sb.toString().trim();
}
}
67 把字符串转化为整数
后端想打死测试工程师系列。我中了一个" "
这个用例,想来我一上来就判空了,应该先trim()
再判空的,卑微。其他还好,思路对了就不难,倒杯咖啡慢慢做呗。先说几个这题的知识点吧:
首先是char转int记得减个0,因为大家都是ASCII码做运算的:
int i = ch[index] - '0';
其次是怎么去掉数组的第一位,我没出息的用了copyOfRange()
,还行吧,可能有好方法,评论区call我哦~
最后是个小细节,在while()
条件里的:
while(index < ch.length && Character.isDigit(ch[index]))
你把这两个先后顺序一掉,就会报空指针了,因为&&
叫短路与,它判断第一个条件不满足就滚蛋了,不会判第二个了,你index
超了直接滚蛋,第二个ch[index]
就不管了,超不超边界无所谓。你要反过来的话就先判ch[index]
,你这时候超了就空指针了。如果你想两个都判可以用&
就完事了。||
同理。
我直接把思路写在注释里了,先上个没注释的版本,再上个注释版,不然太乱了:
class Solution {
public int strToInt(String str) {
str = str.trim();
if(str.length() == 0){
return 0;
}
boolean isMinus = false;
char[] ch = str.toCharArray();
if(ch[0] == '+' || ch[0] == '-' || Character.isDigit(ch[0])){
if(ch[0] == '+' || ch[0] == '-'){
if(ch[0] == '-'){
isMinus = true;
}
ch = Arrays.copyOfRange(ch,1,ch.length);
}
int index = 0;
long res = 0;
while(index < ch.length && Character.isDigit(ch[index])){
res *= 10;
res += ch[index] - '0';
if(res > Integer.MAX_VALUE){
return isMinus ? Integer.MIN_VALUE : Integer.MAX_VALUE;
}
index++;
}
return isMinus ? -(int)res : (int)res;
}
return 0;
}
}
注释版:
class Solution {
public int strToInt(String str) {
//先去空格再判空,血的教训
str = str.trim();
if(str.length() == 0){
return 0;
}
//然后我想啊,下面要判断首位了,首位合格的无非就'+'或'-'或数字三种情况,其他的一概滚蛋
//'+''-'肯定是要把它去掉的,这样三种情况就统一了
//当然了,'-abc'这种有可能出现,不过只看首位它是没毛病的,让它进来,反正后面很容易解决
//既然要去掉正负号,那肯定要出个boolean记一下是不是负数
boolean isMinus = false;
char[] ch = str.toCharArray();
//首位是不是正负号或者数字啊
if(ch[0] == '+' || ch[0] == '-' || Character.isDigit(ch[0])){
//是不是正负号啊
if(ch[0] == '+' || ch[0] == '-'){
//是不是负号啊
if(ch[0] == '-'){
isMinus = true;
}
//删除首位
ch = Arrays.copyOfRange(ch,1,ch.length);
}
//首位搞定了就看后面是不是数字了,直到不是数字的地方或者倒底结束
int index = 0;
//结果可能超int范围,拿个long接一下
//'-abc'这种情况返回的也是0,舒服,一箭双雕
long res = 0;
//短路与助您远离空指针喔,铁汁们,先后顺序关注一下
while(index < ch.length && Character.isDigit(ch[index])){
//一位一位往上算
res *= 10;
res += ch[index] - '0';
//及时止损,一看到res超int范围立马return
//你要是想着最后一起算,那肯定会有超long范围的测试用例等着你,你就哭去吧
if(res > Integer.MAX_VALUE){
//正负号看是正数负数,返回最大值
return isMinus ? Integer.MIN_VALUE : Integer.MAX_VALUE;
}
//别忘了往后走一位
index++;
}
//long转int就是这么朴实无华
return isMinus ? -(int)res : (int)res;
}
//兄弟首位都不对想啥呢,回去吧您
return 0;
}
}
到此为止数组和字符串的部分就结束啦~求个关注啵啵啵(●´З`●)