剑指Offer算法题
小明想要换工作,但是又懒得去整理算法题,于是他找到了我。我这次打算将剑指offer中的算法题来进行一次梳理,主要还顺便可以复习和巩固。之后等全部更新完会整理成gitbook。
字符串
- 替换空格
请实现一个函数,把字符串中的每个空格替换成"%20"。例如输入“We are happy.”,则输出“We%20are%20happy.”。
这个题主要有两种思路,一种是正则写法,还有一种就是replace方法。
public class 替换空格 {
public static void main(String[] args) {
String str = "We are happy.";
System.out.println(getResult(str));
}
private static String getResult(String str) {
String res;
if (str == null) {
return null;
}
// res = str.replaceAll(" ", "20%");
// 正则替换
res = str.replaceAll("\\s+?", "20%");
return res;
}
}
- 字符串全排列
输入一个字符串,打印出该字符串中字符的所有排列。例如输入字符串 abc,则打印出由字符 a、b、c 所能排列出来的所有字符串 abc、acb、bac、bca、cab 和 cba。
这个题主要思路是将该字符串划分为两部分,第一部分是第一个元素,剩下的作为第二部分。所有的字符串都要与当前集合的第一个元素进行交换,这也就是一种情况。比如abc/acb和bac/bca等。然后进行了一次交换后,又可以分为两个部分,这接下来就是递归的思想了。
public class 字符串全排列 {
public static void main(String[] args) {
String str = "ab";
String str1 = "abc";
int index = 0;
char[] cArr = str.toCharArray();
getResult(cArr, index);
}
private static void getResult(char[] arr, int index) {
if (arr == null || arr.length == index) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]);
if ((i + 1) % arr.length == 0) {
System.out.println();
}
}
return;
}
for (int i = index; i < arr.length; i++) {
if (!checkSame(arr, index, i)) {
swap(arr, index, i);
getResult(arr, index + 1);
swap(arr, index, i);
}
}
}
private static void swap(char[] arr, int index, int i) {
char temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
}
/**
* 查询是否有和arr[i]相等的字符
*/
private static boolean checkSame(char[] arr, int index, int i) {
for (int j = index; j < i; j++) {
if (arr[i] == arr[index]) {
return true;
}
}
return false;
}
}
- 反转单词顺序
输入一个英文句子,翻转句子中单词的顺序,但单词内字符的顺序不变。例如输入字符串"I am a student.",则输出"student. a am I"。注:标点符号和普通字母一样处理。
这个题只需要将句子按空格切分,然后再倒序输出即可。
public class 反转单词顺序 {
public static void main(String[] args) {
String str = "I am a student.";
System.out.println(getResult(str));
}
private static String getResult(String str) {
if (str == null || str.length() <= 0) {
return null;
}
String[] arr = str.split(" ");
StringBuilder res = new StringBuilder();
for (int i = arr.length - 1; i > 0; i--) {
res.append(arr[i]).append(" ");
}
res.append(arr[0]);
return res.toString();
}
}
- 字符串转整数
请你来实现一个 atoi 函数,使其能将字符串转换成整数
这个题不是很复杂,只需要把问题考虑全面即可。
- 找出第一个非空字符,判断是不是正负号或者数字
- 如果是正负号,那么先判断正负号
- 再判断符号后面是否是数字,如果不是则非法,返回 -1
- 确定连续数字字符的长度
- 计算数字字符的代表的数字大小,并且判断是否越界
public class 字符串转整数 {
public static void main(String[] args) {
String str = "-1234";
String str1 = "-1a234";
System.out.println(getResult(str));
System.out.println(getResult(str1));
}
private static int getResult(String str) {
if (str == null || str.length() == 0) {
return -1;
}
int result = 0;
char[] chs = str.toCharArray();
int len = chs.length;
for (int i = len - 1, j = 0; i > 0; i--, j++) {
int c = (int) chs[i];
if (c < 48 || c > 57) {
return -1;
} else {
result += (c - 48) * Math.pow(10, j);
}
}
int c = (int) chs[0];
if (c <= 57 && c >= 48) {
result += (c - 48) * Math.pow(10, len - 1);
}
if (result < Integer.MIN_VALUE || result > Integer.MAX_VALUE) {
return -1; //越界,如果真的越界,直接会报错,result本身没办法越界
} else if (str.equals("2147483648")) {
if (c == 45) {
result = -2147483648; //边界值
}
} else if (str.equals("-2147483648")) {
result = -2147483648; //边界值
} else {
if (c == 45) {
result = -result; //负号处理
}
}
return result;
}
}
数组
- 求最大连续子数组的和。
输入一个整型数组,数组中有正数也有负数。数组中一个或多个整数形成一个子数组,求所有连续子数组的和的最大值,要求时间复杂度为 O(n)。 比如输入 {1, -2, 3, 10, -4, 7, 2, -5},能产生子数组最大和的子数组为 {3,10,-4,7,2},最大和为 18。
O(n)的时间复杂度就决定了我们只能遍历一次,大致思想就是如果sum小于0,则直接将sum赋值为arr[i],否则就加上arr[i]。最后再与返回值进行比较即可。具体的逻辑叙述比较麻烦,可以跟着下面的代码走一遍就可以理解了。
public class 最大连续子数组的和 {
public static void main(String[] args) {
int[] arr = {1, -2, 3, 10, -4, 7, 2, -5};
System.out.println(getResult(arr));
}
private static int getResult(int[] arr) {
if (arr == null || arr.length <= 0) {
return -1;
}
int res = Integer.MIN_VALUE;
//sum保存当前累积和
int sum = 0;
for (int i = 0; i < arr.length; i++) {
// 如果小于 0,则直接说明不可能是从前面开始的。不加之前的值,直接算当前值
if (sum < 0) {
sum = arr[i];
} else {
// 如果大于 0,则相加
sum += arr[i];
}
// 如果添加后的值大于之前存放的最大值,则更新最大值
if (res < sum) {
res = sum;
}
}
return res;
}
}
- 旋转数组中的最小数
把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为 1。
这个题有两种解法,第一种就是遍历一遍找到最小的数,时间复杂度是O(n),另一种解法就是利用二分的思想,时间复杂度为O(logn)。实例代码为后一种方法。第二种方法的原理可以借助快速排序来进行理解,主要需要注意的是该旋转数组是一个递增排序的数组。
public class 旋转数组中最小的数 {
public static void main(String[] args) {
int[] arr = {4, 5, 6, 2, 3};
System.out.println(getResult(arr));
}
private static int getResult(int[] arr) {
if (arr == null || arr.length <= 0) {
return -1;
}
if (arr.length == 1) {
return arr[0];
}
int pre = 0, end = arr.length - 1, mid;
while (arr[pre] >= arr[end]) {
if (end - pre == 1) {
return arr[end];
}
mid = pre + (end - pre) / 2;
if (arr[mid] >= arr[pre]) {
pre = mid;
} else {
end = mid;
}
}
return -1;
}
}
- 数字在排序数组中出现的次数
统计一个数字在排序数组中出现的次数。
因为数组是有序,因而只需要找到第一个和最后一个等于该数的位置做差即可。当然若是无序数组直接使用HashMap即可,时间复杂度为O(n);
public class 数字在排序数组中出现的次数 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 3, 3, 3, 4, 5};
int target = 3;
System.out.println(getResult(arr, target));
}
private static int getResult(int[] arr, int target) {
if (arr == null || arr.length <= 0) {
return -1;
}
int left = searchLeft(arr, target);
int right = searchRight(arr, target);
if (left == -1 || right == -1) { //表示不存在
return -1;
}
return right - left + 1;
}
/**
* 从右向左找第一个
*/
private static int searchRight(int[] arr, int target) {
for (int i = arr.length - 1; i >= 0; i--) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
private static int searchLeft(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
}
- 二维数组的查找
在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
利用好该数组的有序性进行遍历即可。
public class 二维数组的查找 {
public static void main(String[] args) {
int[][] arr = {{1, 2, 8, 9}, {2, 4, 9, 12}, {4, 7, 10, 13}, {6, 8, 11, 15}};
int target = 18;
System.out.println(getResult(arr, target) ? "存在" : "不存在");
}
private static boolean getResult(int[][] arr, int target) {
if (arr == null || arr.length <= 0) {
return false;
}
int p = 0, q = arr[0].length - 1;
while (p < arr.length && q >= 0) {
if (arr[p][q] == target) {
return true;
}
if (arr[p][q] < target) {
p++;
} else {
q--;
}
}
return false;
}
}
- 数组顺序调整
输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分。
这题进一步抽象就是满足一定条件的元素都移动到数组的前面,不满足的移动到后面。所以,需要有一个参数用来传递判断函数。最优解法就是数组两头分别有一个指针,然后向中间靠拢。符合条件,就一直向中间移动;不符合条件,就停下来指针,交换两个元素;然后继续移动,直到两个指针相遇。
public class 数组顺序调整 {
public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 6, 5};
getResult(arr);
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i] + " ");
}
}
private static void getResult(int[] arr) {
if (arr == null || arr.length <= 0) {
return;
}
int p = 0, q = arr.length - 1;
while (p < q) {
if (arr[p] % 2 != 0) {//找到一个偶数
p++;
}
if (arr[q] % 2 != 1) {//找到一个奇数
q--;
}
if (arr[p] % 2 == 0 && arr[q] % 2 == 1) {
swap(arr, p, q);
p++;
q--;
}
}
}
private static void swap(int[] arr, int p, int q) {
int temp = arr[p];
arr[p] = arr[q];
arr[q] = temp;
}
}
- 把数组排成最小的数
输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为 321323。
因为涉及拼接,所以可以将其看做字符串,同时规避了大数溢出的问题,而且字符串的比较规则和数字相同。借助自定义排序,可以快速比较两个数的大小。比如只看{3, 32}这两个数字。它们可以拼接成 332 和 323,按照题目要求,这里应该取 323。也就是说,此处自定义函数应该返回-1。
public class 把数组排成最小的数 {
public static void main(String[] args) {
Integer[] arr = {3, 32, 321};
System.out.println(getResult(arr));
}
private static String getResult(Integer[] arr) {
List<Integer> arrList = Arrays.asList(arr);
arrList.sort((o1, o2) -> {
String a = o1 + "" + o2;
String b = o2 + "" + o1;
if (Integer.valueOf(a) > Integer.valueOf(b)) {
return 1;
} else if (Integer.valueOf(a) < Integer.valueOf(b)) {
return -1;
}
return 0;
});
StringBuilder res = new StringBuilder();
for (Integer item : arrList) {
res.append(item);
}
return res.toString();
}
}