这篇文章主要是用于记录笔者遇到过的各种笔试题,笔试题是面试前一个比较重要的考核指标,如果笔试题做的很糟糕,很可能连面试的机会都没有,笔者自己就因为春招的时候笔试太糟糕只获得了很少的面试机会,所以对于笔试我们还是要比较重视。还有最重要的一点是笔试一定要诚实,被发现了作弊行为后果是很严重的~
最后说一下笔试过程的相关注意事项:
题目描述
Given a string containing just the characters
'('
,')'
,'{'
,'}'
,'['
and']'
, determine if the input string is valid.
An input string is valid if:
- Open brackets must be closed by the same type of brackets.
- Open brackets must be closed int the correct order.
Note that an empty string is also considered valid.
Example 1:
Input: “()”
Output: true
Example 2:
Input: “()[]{}”
Output: true
Example 3:
Input: “(]”
Output: false
问题分析
这道题的思路是使用一个栈来进行判断,先将第一个字符压入栈中,然后逐个遍历后面的字符,如果遇到相匹配的字符就出栈。遍历完成之后判断栈是否为空,为空说明是合法的,否则不合法。
实现代码
public class Main {
public static boolean isValid(String str){
if (str == null){
return false;
}
// 空字符串也是合法的
if ("".equals(str)){
return true;
}
char[] ch = str.toCharArray();
Stack<Character> stack = new Stack<>();
stack.push(ch[0]);
for (int i = 1; i < ch.length; i++){
if (!stack.isEmpty() && isMatch(stack.peek(), ch[i])){
stack.pop();
} else {
stack.push(ch[i]);
}
}
return stack.isEmpty();
}
private static boolean isMatch(char a, char b){
return (a == '(' && b == ')') ||
(a == '[' && b == ']') ||
(a == '{' && b == '}');
}
public static void main(String[] args) {
System.out.println(isValid("(([]))[]"));
}
}
题目描述
给定一个M行N列的矩阵(M*N个格子),每个格子中放着一定数量的平安果。你从左上角的各自开始,只能向下或者向右走,目的地是右下角的格子。每走过一个格子,就把格子上的平安果都收集起来。求你最多能收集到多少平安果。
注意:当经过一个格子时,需要一次性把格子里的平安果都拿走。
限制条件:1
输入描述
输入包含两部分:
第一行M, N
接下来M行,包含N个平安果数量
输出描述
一个整数,表示最多拿走的平安果的数量。
输入
2 4
1 2 3 40
6 7 8 90
输出
136
问题分析
这道题是一道很典型的动态规划问题,用 f ( i , j ) \ f(i, j) f(i,j) 表示到达坐标为 ( i , j ) \ (i, j) (i,j) 的格子时所能拿到的平安果的最大数量。而 f ( i , j ) \ f(i, j) f(i,j) 的来源有两个,一个是从左边 f ( i − 1 , j ) \ f(i-1, j) f(i−1,j) 而来,另一个是从上边 f ( i , j − 1 ) \ f(i, j-1) f(i,j−1) 而来,我们只需要在二者之中取最大值即可,代码如下所示:
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int m = in.nextInt();
int[][] apples = new int[n][m];
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
apples[i][j] = in.nextInt();
}
}
solution(apples);
}
in.close();
}
private static void solution(int[][] apples) {
if (apples == null || (apples.length == 0)){
System.out.println(0);
return;
}
int n = apples.length;
int m = apples[0].length;
int[][] dp = new int[n][m];
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
int left = 0;
int up = 0;
if (i > 0){
left = dp[i-1][j];
}
if (j > 0){
up = dp[i][j-1];
}
dp[i][j] = Math.max(left, up) + apples[i][j];
}
}
System.out.println(dp[n-1][m-1]);
}
}
题目描述
小Q是一名体育老师,需要对学生们的队伍整队。现在有n名学生站成一排,第i个学生的身高是hi。现在小Q一声令下:“向右看齐!”,所有的学生都向右看齐了。如果对于下标为i的学生来说,如果学生j满足i
输入描述
第一行输入一个数字n。1 <= n <= 100,000
之后每行一个数字,表示每一个孩子的身高hi(1 <= hi <= 1,000,000)
输出描述
共n行,按顺序输出每位同学的最近看齐的对象,如果没有,则输出0。
输入
6
3
2
6
1
1
2
输出
3
3
0
6
6
0
问题分析
这道题的难度不大,通过暴力解法的方式其实也可以很轻易地求出来,但是复杂度为 O ( n 2 ) \ O(n^{2}) O(n2),在测试的时候很容易造成超时,因此暴力解法并不可行。
而优化的方法笔者这里想到的是从后往前遍历,利用后面的信息减少冗余的判断,例如队列 3 2 6 1 1 2,数字 6 对齐右边的时候,其右边的 1 小于 6,因此我们直接找到 1 的对齐目标,如果对齐目标仍然小于 6,那么我们就继续往对齐目标的对齐目标找…依次类推,直到找不到对齐目标或者找到一个目标大于 6 为止。代码如下所示:
public class Main {
public static void solution(int[] people){
if (people == null || people.length == 0){
throw new IllegalArgumentException();
}
if (people.length == 1){
System.out.println(0);
}
int n = people.length;
int[] record = new int[n];
// 最右边的人肯定没有可对齐的人
record[n-1] = -1;
for (int i = n-2; i >= 0; i--){
int target = i+1;
while (target != -1 && people[i] >= people[target]){
target = record[target];
}
record[i] = target;
}
for (int i = 0; i < n; i++){
System.out.print((record[i] + 1) + " ");
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int[] people = new int[n];
for (int i = 0; i < n; i++){
people[i] = in.nextInt();
}
solution(people);
}
}
}
问题描述
有 n 个人围坐在一个圆桌周围进行一场圆桌会议,会议开始前从第s (注意不是5) 开始报数,数到第 m 的人就出列退出会议,然后从出列的下一个人重新开始报数,数到第 m 的人又出列,…,,如此重复直到所有的人全部出列为止,现在希望你能求出按退出会议次序得到的人员的序号序列。
输入描述
三个正整数n,s,m(n,s,m < 10000)
输出描述
退出会议次序序号,一行一个。
输入
3 1 2
输出
2
1
3
问题分析
这是一道很经典的约瑟夫环问题,只不过做了一点变式,代码如下所示:
public class JosephRing {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int s = in.nextInt();
int m = in.nextInt();
solution(n, s, m);
}
in.close();
}
private static void solution(int n, int s, int m) {
if (n <= 0 || s <= 0 || m <= 0 || s > m){
throw new IllegalArgumentException();
}
// 由于不是从1开始数的,因此要计算出它的偏移值
int offset = -(n+s-1) % n;
for (int i = 1; i <= n; i ++){
int result = (f(n, m, i) + offset + n - 1) % n + 1;
System.out.println(result);
}
}
private static int f(int n, int m, int turn){
if (turn == 1){
return (n + m - 1) % n + 1;
} else {
return (f(n-1, m, turn-1) + m - 1) % n + 1;
}
}
}
如果对约瑟夫环不熟悉的同学,可以参考这篇文章:【约瑟夫环问题递归解法的一点理解】,说的非常详细。
问题描述
小Q打算穿越怪兽谷,他不会打怪,但是他有钱,他知道,只要给怪兽一定的金币,怪兽就会一直护送着他出谷。
在谷中,他会依次遇见N只怪兽,每只怪兽都有自己的武力值和要“贿赂”它所需的金币数,如果小Q没有“贿赂”某只怪兽,而这只怪兽“武力值”又大于护送他的怪兽武力之和,这只怪兽就会攻击他。
小Q想知道,要想成功穿越怪兽而不被攻击,他最少要准备多少金币。
输入描述
第一行输入一个整数N,代表怪兽的只数。
第二行输入N的整数d1,d2,。。。,dn,代表武力值
第三行输入N个整数p1,p2,。。。,pn,代表收买第N只怪兽所需的金币数
(1 <= N <= 50,1 <= d1,d2,…,dn <= 1012,1 <= p1,p2,…,pn <= 2)
输出描述
输出一个整数,代表所需最小金币数
输入
3
8 5 10
1 1 2
输出
2
输入
4
1 2 4 8
1 2 1 2
输出
6
问题分析
这道题是一道比较典型的动态规划问题,在遇到怪兽的时候,如果我们的武力值小于怪兽,那么我们为了保命只能买下它,而如果我们的武力值大于怪兽,我们可以选择买下它,也可以选择直接将它打到,代码如下所示:
public class Main {
static int[] power;
static int[] price;
public static int solution() {
if (power == null || power.length == 0){
return 0;
}
return solution(0, 0, 0);
}
private static int solution(int selfPower, int cost, int i) {
if (i == power.length){
return cost;
}
if (selfPower < power[i]){
return solution(selfPower+power[i], cost+price[i], i+1);
} else {
int a = solution(selfPower, cost, i+1);
int b = solution(selfPower+power[i], cost+price[i], i+1);
return Math.min(a, b);
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
power = new int[n];
price = new int[n];
for (int i = 0; i < n; i++){
power[i] = in.nextInt();
}
for (int i = 0; i < n; i++){
price[i] = in.nextInt();
}
System.out.println(solution());
}
in.close();
}
}
如果对动态规划问题比较陌生的可以查看这篇文章:【动态规划问题】,可能会对你理解该问题的代码有所帮助~
问题描述
给定一个字符串s,你可以从中删除一些字符,使得剩下的串是一个回文串。如何删除才能使得回文串最长呢?输出需要删除的字符个数。
输入描述
输入数据有多组,每组包含一个字符串s,且保证:1<=s.length<=1000.
输出描述
对于每组数据,输出一个整数,代表最少需要删除的字符个数。
输入
abcda
输出
2
2
问题分析
这道题我们可以做如下转换:求原字符串和反转字符串的公共最长子序列,将原字符串长度减去该子序列的长度即为要删除的字符个数。所以这道题转化为了一个动态规划问题,代码如下所示:
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
String str = in.nextLine();
System.out.println(solution(str));
}
}
public static int solution(String str) {
StringBuilder src = new StringBuilder(str);
String reverse = src.reverse().toString();
int n = str.length();
int[][] dp = new int[n+1][n+1];
for (int i = 1; i <= n; i++){
for (int j = 1; j <= n; j++){
if (str.charAt(i-1) == reverse.charAt(j-1)){
dp[i][j] = dp[i-1][j-1] + 1;
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return n-dp[n][n];
}
}
问题描述
小Q最近遇到了一个难题:把一个字符串的大写字母放到字符串的后面,各个字符的相对位置不变,且不能申请额外的空间。你能帮帮小Q吗?
输入描述
输入数据有多组,每组包含一个字符串s,且保证:1<=s.length<=1000.
输出描述
对于每组数据,输出移位后的字符串。
输入
AkleBiCeilD
输出
kleieilABCD
问题分析
这道题的简单做法就是从前往后遍历,检测到大写字母时直接将大写字母移动到字符串的最后一个位置上去,时间复杂度为 O ( n 2 ) \ O(n^{2}) O(n2),实现代码如下所示:
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
String str = in.nextLine();
System.out.println(solution(str));
}
}
private static String solution(String str) {
if (str == null || str.length() == 0){
return "";
}
int len = str.length();
int n = len-1;
char[] ch = str.toCharArray();
for (int i = 0; i <= n; ){
if (ch[i] <= 'Z' && ch[i] >= 'A'){
char temp = ch[i];
int index = i;
// 将大写字符后面的字符全部往前挪一个位置,空出最后面的位置给大写字符进行放置
for (int j = i+1; j < len; j++){
ch[index++] = ch[j];
}
ch[len-1] = temp;
n--;
} else {
i++;
}
}
return new String(ch);
}
}
问题描述
小Q今天在上厕所时想到了这个问题:有n个数,两两组成二元组,相差最小的有多少对呢?相差最大呢?
输入描述
输入包含多组测试数据。
对于每组测试数据:
N - 本组测试数据有n个数
a1,a2…an - 需要计算的数据
保证:
1<=N<=100000,0<=ai<=INT_MAX.
输出描述
对于每组数据,输出两个数,第一个数表示差最小的对数,第二个数表示差最大的对数。
输入
6
45 12 45 32 5 6
输出
1 2
问题分析
先将数据从小到大进行排序,相差最大的肯定是头尾两个数组成的了,而相差最小的则是出现在相邻两个数之差,代码如下所示:
问题描述
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例一
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例二
输入: coins = [2], amount = 3
输出: -1
说明
你可以认为每种硬币的数量是无限的。
问题分析
典型的动态规划问题,用 d p [ i ] \ dp[i] dp[i] 表示凑成 i \ i i 元时,所需硬币的个数,代码如下所示:
public class Main {
public static int solution(int[] coins, int amount) {
if (coins == null || coins.length == 0 || amount <= 0){
return -1;
}
int n = coins.length;
int[] dp = new int[amount+1];
for (int i = 0; i <= dp.length; i++) {
dp[i] = Integer.MAX_VALUE;
}
dp[0] = 0;
for (int i = 1; i <= amount; i++){
for (int j = 0; j < n; j++){
if (i-coins[j] >= 0 && dp[i-coins[j]] != Integer.MAX_VALUE && dp[i] > dp[i-coins[j]] + 1){
dp[i] = dp[i-coins[j]] + 1;
}
}
}
return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int amount = in.nextInt();
int[] coins = new int[n];
for (int i = 0; i < n; i++){
coins[i] = in.nextInt();
}
System.out.println(solution(coins, amount));
}
}
}
问题描述
输入一个 N × M \ N×M N×M 矩阵栅格,每个栅格都有一个高程值代表海拔高度,小明从出发点到终点有几条不同路径?每次只能往更高海拔的地方去,走过的地方不能再走,只能前后左右走。
输入描述
第一行代表 M × N \ M×N M×N 网格大小。后面是 M × N \ M×N M×N 的矩阵。最后一行前两个数字代表起始坐标,后面两个数字代表目的坐标(坐标从左上角(0,0)开始)。
输出描述
路径数
输入
5 4
0 1 0 0
0 2 3 3
0 3 0 0
0 4 5 6
0 7 6 0
0 1 4 1
输出
2
问题分析
走迷宫类的问题一般采用深度优先搜索的方式进行解题,虽然题目有规定走过的地方不能再走,但是由于我们只能往高的地方走,因此走过的地方明显是不会再走的了,可以省去这个判断,代码如下所示:
public class Main {
static int[][] dp;
static int startX;
static int startY;
static int endX;
static int endY;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int m = in.nextInt();
dp = new int[n][m];
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
dp[i][j] = in.nextInt();
}
}
startX = in.nextInt();
startY = in.nextInt();
endX = in.nextInt();
endY = in.nextInt();
System.out.println(solution());
}
}
private static int solution() {
if (dp == null){
return 0;
}
return dfs(startX, startY);
}
private static int dfs(int x, int y) {
int result = 0;
int n = dp.length;
int m = dp[0].length;
if (x == endX && y == endY){
return 1;
} else {
if (x - 1 >= 0 && dp[x-1][y] > dp[x][y]){
result += dfs(x-1, y);
}
if (x + 1 < n && dp[x+1][y] > dp[x][y]){
result += dfs(x+1, y);
}
if (y - 1 >= 0 && dp[x][y-1] > dp[x][y]){
result += dfs(x, y-1);
}
if (y + 1 < m && dp[x][y+1] > dp[x][y]){
result += dfs(x, y+1);
}
}
return result;
}
}
问题描述
输入一个字符串(不含空格),请寻找输入中包含的所有蛇形字符串。
蛇形字符串定义:
- 蛇形字符串由连续字符对组成,其特点如下:
1.1 字符对定义:字符对由同一字母的大写和小写组成(前大后小)。如:Aa,Dd
1.2 蛇形字符串中包含的字符对,必须是连续字母,并按照字母顺序排序。如:AaBbCc 或 OoPpQqRrSs- 从输入中寻找字符组成蛇形字符串(字符顺序不限),符合规则:
2.1 每次寻找必须是最长的蛇形字符串
2.2 使用过的字符串不能重复使用
例:输入 SxxsrR^AaSs 正确过程(粗体为寻找到的字符):Step1: S xx srR^AaSs -> RrSs(找到两对连续字符对:Ss, Rr。可以组成蛇形字符串。另,Ss后应该是Tt,但当前字符串SxxsrR^AaSs中不包含,所以当前蛇形字符串到Ss结束。本轮查找解结果为RrSs。)
Step2: xx^Aa Ss -> Aa
Step3: xx^Ss -> Ss
错误过程1:
SxxsrR^AaSs(违反规则2.1:Ss, Rr可以组成更长的连续蛇形字符串RrSs)
问题分析
这道题被说的挺复杂的,实际上难度一般,笔者的做法是用两个长度均为 26 的数组分别保存字符串中大小写字母的个数,然后再对这两个数组进行整理,例如 A 有 6 个,a 有 1 个,那么 A 我们也记录为1个,因为多出的5个我们是无法组成蛇形字符串的。最后从 A/a 开始逐个输出即可,代码如下所示:
public class Main {
static int[] up = new int[26];
static int[] down = new int[26];
public static void solution(String str){
if (str == null || str.length() <= 1){
return;
}
// 1. 遍历存储
for (int i = 0; i < str.length(); i++){
char c = str.charAt(i);
if (c <= 'z' && c >= 'a'){
down[c-'a']++;
} else if (c <= 'Z' && c >= 'A'){
up[c-'A']++;
}
}
// 2. 调整数组
for (int i = 0; i < 26; i++){
int min = Math.min(down[i], up[i]);
down[i] = min;
up[i] = min;
}
// 3. 输出
for (int i = 0; i < 26;){
if (up[i] > 0){
StringBuilder sb = new StringBuilder();
for (int j = i; j < 26; j++){
if (up[j] == 0){
break;
}
char upChar = (char) (j + 'A');
char downChar = (char) (j + 'a');
up[j]--;
sb.append(upChar).append(downChar);
}
System.out.println(sb);
i = 0;
} else {
i++;
}
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
String str = in.next();
solution(str);
}
}
}
问题描述
蜂巢在坐标(0,0)的位置,有五处花丛,蜜蜂从蜂巢出发,要把五处花丛的花蜜采完再回到蜂巢,蜜蜂飞行的最短距离是多少?
输入描述
一行输入,10个数分别是五处花丛的坐标( x1, y1, x2, y2, x3, y3, x4, y4, x5, y5),用空格隔开。
输出描述
输出最短距离,距离向下取整。
问题分析
这道题是一道旅行商问题,一开始想到是用贪心算法来做,但是最后贪心算法没做出来,后面看了别人的实现,用的全排列的方式,虽然属于暴力求解,但是对于5个点的情况还是可以接受的,代码如下所示:
public class Main {
private static int dist = Integer.MAX_VALUE;
private static Point[] points = new Point[6];
static class Point{
int x;
int y;
Point(int x, int y){
this.x = x;
this.y = y;
}
}
/**
* 交换
*/
private static void swap(Point[] points, int i, int j){
Point temp = points[i];
points[i] = points[j];
points[j] = temp;
}
/**
* 全排类,穷举出所有可能的路径依次计算然后取最小值即为我们要求的路径长度
*/
public static void solution(int k){
if (k == 5){
dist = Math.min(dist, calculateDist());
} else {
for (int i = k; i <= 5; i++){
swap(points, i, k);
solution(k+1);
swap(points, i, k);
}
}
}
private static int calculateDist() {
double sum = 0;
for (int i = 1; i < 6; i++){
int x1 = points[i].x;
int y1 = points[i].y;
int x2 = points[i-1].x;
int y2 = points[i-1].y;
sum += Math.sqrt(Math.pow(x2-x1, 2) + Math.pow(y2-y1, 2));
}
// 返程路段
sum += Math.sqrt(Math.pow(points[0].x - points[5].x, 2)
+ Math.pow(points[0].y - points[5].y, 2));
return (int) Math.ceil(sum);
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
Point point = new Point(0,0);
points[0] = point;
while (in.hasNext()){
for (int i = 1; i <= 5; i++){
point = new Point(in.nextInt(), in.nextInt());
points[i] = point;
}
// 这里我们从1开始全排列而不是0,因为蜂巢永远是蜜蜂的起点,所以它不需要
// 参与全排列,永远位于首位
solution(1);
System.out.println(dist);
}
}
}
如果对这道题的实现代码有疑问的话可以先学习下全排列算法的相关知识再来看这段代码就会很简单了!
问题描述
一个农夫饲养了一批怪物牛,他发现这批牛的繁殖能力惊人,一对牛每月繁殖一对小牛。每一对小牛在出生后,需要花三个月时间来生长,第四个月开始繁殖。
输入描述
第一行输入N,表示N组数据
第二行开始,输入N数据,每组数据两行:分别为 M ( 1 ≤ M ≤ 100 ) \ M(1≤ M ≤ 100) M(1≤M≤100) 对牛,第 N ( 1 ≤ M ≤ 50 ) \ N(1≤ M ≤ 50) N(1≤M≤50) 个月。
输出描述
输出N组数据的结果,每组结果占一行
假设怪物牛不存在死亡的情况,计算开始初始数量为M对牛的情况下,第N个月牛的总数(对)。
问题分析
这道题有点类似于找规律的问题,笔者的做法是直接模拟牛的生长规律,通过递归的方式获得最终结果,代码如下所示:
public class Main {
public static int solution(int parent, int n){
if (parent == 0){
return 0;
}
return f(parent, 0, 0, 0, 0, 0, n);
}
/**
* 状态方程
*
* @param parent 成年牛的对数
* @param child0 初生的小牛的对数
* @param child1 一个月大的牛的对数
* @param child2 两个月大的牛的对数
* @param child3 三个月大的牛的对数
* @param i 第i个月
* @param n 总共有n个月
* @return 所有牛的对数
*/
private static int f(int parent, int child0, int child1, int child2, int child3, int i, int n) {
if (i == n){
return parent + child0 + child1 + child2 + child3;
} else {
return f(parent+child3, parent+child3, child0, child1, child2, i+1, n);
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int cnt = in.nextInt();
for (int i = 0; i < cnt; i++){
int n = in.nextInt();
int m = in.nextInt();
System.out.println(solution(n, m));
}
}
in.close();
}
}
问题描述
小Q所在的王国有n个城市,城市之间有m条单向道路连接起来。对于一个城市v,从城市v出发可以到达的城市数量为x,从某个城市出发可以到达城市v的城市数量为y,如果y>x,则城市v是一个重要城市(间接可达也算可以到达)。
小Q希望你能帮他计算一下王国中一共有多少个重要城市。
输入描述
输入包括m+1行,
第一行包括两个数n和m(1 <= n,m <= 1000),分别表示城市数和道路数。
接下来m行,每行两个树u,v(1 <= u,v <= n),表示一条从u到v的有向道路,输入中可能包含重边和自环。
输出描述
输出一个数,重要节点的个数。
输入
4 3
2 1
3 2
4 3
输出
2
问题分析
这道题凭直觉就是一道有向图的问题,采用一个二维数组 reachable[ ][ ] 来表示两个点之前是否可达,例如 reachable[1][2] = true表示城市1到城市2是可达的,注意这里的可达是包括间接可达的。因此问题的关键就变成了除去题目中给出的直接可达的点之外,我们还要关于某个城市间接可达的点,这里采用了深度优先搜索进行处理,代码如下所示:
public class Main {
static boolean[][] dp;
static boolean[][] reachable;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int m = in.nextInt();
dp = new boolean[n][n];
reachable = new boolean[n][n];
for (int i = 0; i < m; i++){
int from = in.nextInt();
int to = in.nextInt();
dp[from-1][to-1] = true;
}
System.out.println(solution());
}
}
private static int solution() {
if (dp == null || dp.length == 0){
return 0;
}
int n = dp.length;
// 深度优先搜索
for (int i = 0; i < n; i++){
dfs(i, i);
}
int result = 0;
for (int i = 0; i < n; i++){
int from = 0;
int to = 0;
for (int j = 0; j < n; j++){
if (reachable[i][j]){
to++;
}
if (reachable[j][i]){
from++;
}
}
if (from > to){
result++;
}
}
return result;
}
/**
* 深度优先搜索
*
* @param src 原始点
* @param from 原始点可达的点
*/
private static void dfs(int src, int from) {
for (int i = 0; i < dp[from].length; i++){
if (dp[from][i] && src != i && !reachable[src][i]){
reachable[src][i] = true;
dfs(src, i);
}
}
}
}
问题描述
在一个排好序的链表中存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,并返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5。
问题分析
这道题是剑指offer上的一道链表题目,链表的特点是只能前进,不能后退,因此这道题的一个基本的思路就是需要一个前结点保存前一个结点的值,然后当前结点进行判断和处理,代码如下所示:
public class DeleteDuplication {
public static class Node {
public int val;
public Node next;
public Node(int val){
this.val = val;
}
}
public static Node solution(Node node){
// 0或1个结点的情况直接返回
if (node == null || node.next == null){
return node;
}
// 定义一个头结点方便进行处理
Node p = new Node(-1);
p.next = node;
Node preNode = p;
Node curNode = node;
// curNode为空说明我们已经遍历完了所有的结点
// curNode.next为空说明我们已经到达最后一个结点,而这个结点是不需要删除的(不存在重复)
while (curNode != null && curNode.next != null){
// 存在重复结点
if (curNode.val == curNode.next.val){
Node temp = curNode.next.next;
// 找到不等于重复结点的结点
while (temp != null && temp.val == curNode.val){
temp = temp.next;
}
curNode = temp;
preNode.next = curNode;
} else {
// 正常结点直接向前移动即可
preNode = curNode;
curNode = curNode.next;
}
}
return p.next;
}
}
除此之外我们还可以采用递归的方式来做这道题,代码如下所示:
public static Node solution(Node node) {
// 递归终止条件
if (node == null || node.next == null){
return node;
}
// 存在重复结点时找到与重复结点不相等的结点返回
if (node.val == node.next.val){
Node temp = node.next.next;
while (temp != null && temp.val == node.val){
temp = temp.next;
}
return deleteDuplication(temp);
} else {
// 否则保存该结点,继续向前遍历
node.next = deleteDuplication(node.next);
}
return node;
}
问题描述
统计一个数字在排序数组中出现的次数。例如,输入排序数组 { 1, 2, 3, 3, 3, 3, 4, 5 } 和数字3,由于3在这个数组中出现了4次,因此输出4。
问题分析
这道题如果采用顺序查找的方式复杂度为 O ( n ) O(n) O(n),显然不会是理想的求解方式,如果采用其他方法例如二分查找我们是有希望将复杂度将为 O ( l o g n ) O(logn) O(logn)的,所以可以使用二分查找分别找到目标数字k第一次出现的位置以及最后一次出现的位置,然后两个位置的下标值相减就可以得到相应的出现次数了,代码如下所示:
public class Q53 {
public static int getNumberOfK(int[] array, int k) {
if (array == null || array.length == 0){
return 0;
}
int first = getFirstK(array, k);
int last = getLastK(array, k);
// 返回-1说明数组中不存在k
if (first == -1 || last == -1){
return 0;
}
return last - first + 1;
}
/**
* 二分法查找第一个k的出现位置
*
* @param array
* @param k
* @return
*/
private static int getFirstK(int[] array, int k){
int lo = 0;
int hi = array.length-1;
while (lo <= hi){
int mid = lo + (hi - lo) / 2;
if (array[mid] == k){
if (mid == 0 || array[mid-1] != k){
return mid;
} else {
hi = mid - 1;
}
} else if (array[mid] < k){
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
/**
* 二分法查找最后一个k的出现位置
*
* @param array
* @param k
* @return
*/
private static int getLastK(int[] array, int k){
int lo = 0;
int hi = array.length-1;
while (lo <= hi){
int mid = lo + (hi - lo) / 2;
if (array[mid] == k){
if (mid == array.length-1 || array[mid+1] != k){
return mid;
} else {
lo = mid + 1;
}
} else if (array[mid] < k){
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
int[] a = {1, 2, 2, 3, 3, 3, 3, 4, 5 };
System.out.println(getNumberOfK(a, 3));
}
}
问题描述
一个长度为n-1的递增排序数组中的所有数字都是唯一的,并且每个数字都在范围0 ~ n-1之内。在范围0 ~ n-1内的n个数字中有且只有一个数字不在该数组中,请找出这个数字。
问题分析
采用二分法的方式可以将时间复杂度变为 O ( l o g n ) O(logn) O(logn),具体实现方式是找到第一个数组下标和所在元素不相等,因为这个数组是递增的关系,所以这个方法是行得通的,代码如下所示:
public class Main {
public static int solution(int[] a){
// 非法输入情况
if (a == null || a.length == 0){
return -1;
}
int lo = 0;
int hi = a.length-1;
while (lo <= hi){
int mid = lo + (hi-lo) / 2;
if (a[mid] != mid){
if (mid == 0 || a[mid-1] == mid-1){
return mid;
} else {
hi = mid - 1;
}
} else {
lo = mid + 1;
}
}
// 当遍历完仍然没有找到时说明是n
return a.length;
}
public static void main(String[] args) {
int[] a = {0, 1, 2, 4, 5};
System.out.println(solution(a));
}
}
问题描述
假设一个单调递增的数组里的每个元素都是整数并且是唯一的。请编程实现一个函数,找出数组中任意一个数值等于其下标的的元素。例如,在数组 { -3, -1, 1, 3, 5} 中,数字3和它的下标相等。
问题分析
利用数组的递增特性,用二分法同样可以快速做出来,代码如下所示:
public class Main {
public static int solution(int[] array){
// 非法输入
if (array == null || array.length == 0){
return -1;
}
int lo = 0;
int hi = array.length-1;
while (lo <= hi){
int mid = lo + (hi - lo) / 2;
if (array[mid] == mid){
return mid;
} else if (array[mid] < mid){
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
int[] a = { -3, -1, 1, 2, 4};
System.out.println(solution(a));
}
}
问题描述
一种双核CPU的两个核能够同时的处理任务,现在有n个已知数据量的任务需要交给CPU处理,假设已知CPU的每个核1秒可以处理1kb,每个核同时只能处理一项任务。n个任务可以按照任意顺序放入CPU进行处理,现在需要设计一个方案让CPU处理完这批任务所需的时间最少,求这个最小的时间。
输入描述
输入包括两行: 第一行为整数n(1 ≤ n ≤ 50) 第二行为n个整数length[i](1024 ≤ length[i] ≤ 4194304),表示每个任务的长度为 length[i]kb,每个数均为1024的倍数。
输出描述
输出一个整数,表示最少需要处理的时间
输入
5 3072 3072 7168 3072 1024
输出
9216
问题分析
假定所有任务的处理时间和为 s u m sum sum,并且假定处理器处理时间为 n 1 n_1 n1并且 n 1 ≤ 1 / 2 s u m n_1 ≤1/2sum n1≤1/2sum,那么对于这个问题来说,需要求的就是在不大于 1 / 2 s u m 1/2sum 1/2sum的条件下, n 1 n_1 n1的最大值,最后 s u m − n 1 sum-n_1 sum−n1即为我们所求的处理时间,所以这道题就转化为了一个 0 1 背包问题,代码如下所示:
public class Main {
private static int solution(int[] task){
if (task == null || task.length == 0){
return 0;
}
int n = task.length;
int sum = sum(task);
int half = sum / 2;
int[][] dp = new int[n+1][half+1];
for (int i = 1; i <= n; i++){
for (int j = 1; j <= half; j++){
if (j < task[i-1]){
dp[i][j] = dp[i-1][j];
} else {
int a = dp[i-1][j];
int b = dp[i-1][j-task[i-1]] + task[i-1];
dp[i][j] = Math.max(a, b);
}
}
}
return (sum-dp[n][half]) * 1024;
}
private static int sum(int[] task) {
int result = 0;
for (int i = 0; i < task.length; i++){
result += task[i];
}
return result;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
int n = in.nextInt();
int[] task = new int[n];
for (int i = 0; i < n; i++){
task[i] = (in.nextInt() / 1024);
}
System.out.println(solution(task));
}
}
}
问题描述
在幼儿园有n个小朋友排列为一个队伍,从左到右一个挨着一个编号为(0~n-1)。其中有一些是男生,有一些是女生,男生用’B’表示,女生用’G’表示。小朋友们都很顽皮,当一个男生挨着的是女生的时候就会发生矛盾。作为幼儿园的老师,你需要让男生挨着女生或者女生挨着男生的情况最少。你只能在原队形上进行调整,每次调整只能让相邻的两个小朋友交换位置,现在需要尽快完成队伍调整,你需要计算出最少需要调整多少次可以让上述情况最少。例如:
GGBBG -> GGBGB -> GGGBB
这样就使之前的两处男女相邻变为一处相邻,需要调整队形2次
输入描述
输入数据包括一个长度为n且只包含G和B的字符串.n不超过50.
输出描述
输出一个整数,表示最少需要的调整队伍的次数
输入
GGBBG
输出
2
问题分析
要使得小朋友之间的矛盾最少,明显只能是将男女归为两拨,只产生一对矛盾,而小朋友两两之间只能彼此交换顺序,这其实很像插入排序的思想,所以这道题笔者的做法是模拟插入排序,分别用两个整型数组将男女赋值为0和1,通过对这两个数组进行排序,统计交换次数,最终交换次数较小的即为结果,代码如下所示:
public class Main {
private static int solution(String queue){
// 小朋友少于3个的情况下不需要交换
if (queue == null || queue.length() < 3){
return 0;
}
int n = queue.length();
int[] a = new int[n];
int[] b = new int[n];
int count1 = 0;
int count2 = 0;
// 为两个数组进行初始化赋值
for (int i = 0; i < n; i++){
if (queue.charAt(i) == 'B'){
a[i] = 0;
b[i] = 1;
} else {
a[i] = 1;
b[i] = 0;
}
}
// 数组a插入排序
for (int i = 1; i < n; i++){
for (int j = i; j > 0 && less(a[j], a[j-1]); j--){
exch(a, j, j-1);
count1++;
}
}
// 数组b插入排序
for (int i = 1; i < n; i++){
for (int j = i; j > 0 && less(b[j], b[j-1]); j--){
exch(b, j, j-1);
count2++;
}
}
// 二者取小
return Math.min(count1, count2);
}
private static boolean less(int v, int w){
return v < w;
}
private static void exch(int[] a, int i, int j){
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()){
String str = in.next();
System.out.println(solution(str));
}
in.close();
}
}
问题描述
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?例如,一只股票在某些时间节点的价格为{ 9, 11, 8, 5, 7, 12, 16, 14 }。如果我们能在价格为5的时候买入并在价格为16时卖出,则能收获最大的利润 11。
实现代码
class Main {
public static int maxProfit(int[] prices) {
if (prices == null || prices.length < 2){
return 0;
}
int min = prices[0];
int result = 0;
for (int i = 1; i < prices.length; i++){
if (min > prices[i]){
min = prices[i];
}
result = Math.max(result, prices[i]-min);
}
return result;
}
}
问题描述
砸金蛋,金蛋顺序排列,每个金蛋有价值 m i m_i mi,砸第 i i i 个金蛋可获得 m i − 1 × m i × m i + 1 m_{i-1} \times m_i \times m_{i+1} mi−1×mi×mi+1的金钱,然后第 i i i个金蛋相当于消失,继续砸别的,问最大化收益是多少。
输入描述
第一行输入n,代表金蛋个数;
第二行输入n个数,代表每个金蛋的金钱。
输出描述
最大化收益的值
输入
3
9 6 3
输出
198
问题分析
这道题是一道典型的动态规划题目,采用区间 d p dp dp 的思路即可解出该题,核心思路为:
d p [ i ] [ j ] = m a x ( d p [ i ] [ k ] + d p [ k ] [ j ] + m k − 1 × m k × m k + 1 ) dp[i][j]=max(dp[i][k]+dp[k][j]+m_{k-1} \times m_k \times m_{k+1}) dp[i][j]=max(dp[i][k]+dp[k][j]+mk−1×mk×mk+1)
代码如下所示:
public class Main {
private static int solution(int[] eggs){
if (eggs == null || eggs.length == 0){
return 0;
}
int n = eggs.length;
// 初始化设置
int[] nums = new int[n+3];
nums[1] = nums[n+2] = 1;
for (int i = 0; i < n; i++){
nums[i+2] = eggs[i];
}
n += 2;
// dp[i][j]表示区间[i, j)的金蛋砸碎之后获得的最多钱
int[][] dp = new int[n+1][n+1];
for (int len = 1; len <= n; len++){ // 区间长度
for (int i = 2; i+len <= n; i++){ // 区间起点i
int j = i+len; // 区间终点j
for (int k = i; k < j; k++){ // 分割点k
dp[i][j] = Math.max(dp[i][j], dp[i][k] + dp[k+1][j] + nums[i-1]*nums[k]*nums[j]);
}
}
}
return dp[2][n];
}
public static void main(String[] args) {
// 先砸6、再砸3、最后砸9
// 162+27+9=198
int[] eggs = {9, 6, 3};
System.out.println(solution(eggs));
}
}
问题描述
反转一个单链表。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
问题分析:
这道题的解法有两种,一种是采用常规的迭代法进行翻转,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1),代码如下所示:
public class Main {
public static ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
return prev;
}
}
除此之外还可以采用递归法进行链表的翻转,假设当前链表除了头结点之外其它都已经反转完成,那么如果对头结点进行反转呢?很明显是 head.next.next = head,如下图所示:
此外我们还需要注意将 head.next 置 null,否则的话链表会成环。递归法时间复杂度为 O ( n ) O(n) O(n),空间因为使用了栈的关系,所以复杂度为 O ( n ) O(n) O(n)。实现代码如下:
public class Main {
public static ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode result = reverseList(head.next);
head.next.next = head;
head.next = null;
return result;
}
}
问题描述
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例 :
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明 :
- 你的算法只能使用常数的额外空间。
- 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
问题分析:
这道题可以看做是翻转链表问题的加强版,做法是使用两个指针分别指向要翻转的结点的头部和尾部,然后进行部分翻转,代码如下所示:
public class Main {
public static ListNode reverseKGroup(ListNode head, int k) {
if (head == null || head.next == null || k == 1){
return head;
}
ListNode result = new ListNode(-1);
result.next = head;
ListNode prev = result;
ListNode end = result;
while (end.next != null) {
for (int i = 0; i < k && end != null; i++) {
end = end.next;
}
if (end == null) {
break;
}
ListNode start = prev.next;
ListNode nextNode = end.next;
end.next = null; // 先将链表断开进行翻转
prev.next = reverseList(start); // 实现代码见21题反转链表
start.next = nextNode; // 翻转完成后再将链表重新接上
prev = start;
end = prev;
}
return result.next;
}
}
上述代码时间复杂度为 O ( n 2 / k ) O(n^{2}/k) O(n2/k),空间复杂度为 O ( 1 ) O(1) O(1),因为我们除了新建了一个临时结点之外无其他创建对象的操作。
问题描述
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么头部剩余节点数量不够一组的不需要逆序。
示例 :
给定这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 1->2->5->4->3
问题分析
这道题是20届秋招字节跳动的一道现场编程题,和 22 题比较的话变化的地方只有一个,那就是如果链表不是 k 的倍数的话不再是不翻转尾部的剩余节点,而是不翻转头部的剩余节点。
其实这道题想的明白的话一下子就可以做出来,它是 22 题的一种变式,以下面的链表为例,假设 k = 3 k=3 k=3:
我们可以先将整个链表进行一次翻转:
然后再以 k = 3 k=3 k=3 的形式进行部分翻转:
最后再进行一次对整个链表的翻转:
可以看到就是我们所要的结果了,代码如下所示:
public class Main {
private static ListNode reverseKGroup(ListNode head, int k){
if (k < 1){
throw new IllegalArgumentException("k < 1");
}
if (head == null || head.next == null || k == 1){
return head;
}
ListNode result = new ListNode(-1);
result.next = reverseList(head);
ListNode prev = result;
ListNode end = result;
while (end.next != null){
for (int i = 0; i < k && end != null; i++){
end = end.next;
}
if (end == null){
break;
}
ListNode start = prev.next;
ListNode nextNode = end.next;
end.next = null;
prev.next = reverseList(start);
start.next = nextNode;
prev = start;
end = prev;
}
return reverseList(result.next);
}
}
问题描述
判断一个整数是否是回文数。回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
示例 1:
输入: 121
输出: true
示例 2:输入: -121
输出: false 解释:
从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例3:输入: 10
输出: false
解释: 从右向左读, 为 01 。因此它不是一个回文数。
进阶:
你能不将整数转为字符串来解决这个问题吗?
问题分析
这道题表面上看不难,但是埋着一个非常大的坑,那就是数字溢出的问题,如果我们只是简单地翻转数字之后判断和原数字是否相等,那么翻转后的数字是很有可能造成数字溢出的,虽然说不处理也不会造成结果判断错误,但是如果是一道手撕代码题会给面试官造成逻辑不严谨的坏印象。
所以这道题我们的做法需要做些改变,既然翻转整个数会造成数字溢出的可能,那么我们只处理一半的数,然后和前半部分做比较不就可以了。代码如下所示:
public class Main {
public boolean IsPalindrome(int x){
if (x < 0 || (x % 10 == 0 && x != 0)) {
return false;
}
int temp = 0;
while (temp < x) {
temp *= 10;
temp += x % 10;
x /= 10;
}
return temp == x || x == temp/10;
}
}
问题描述
数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如,输入一个长度为 9 的数组 { 1, 2, 3, 2, 2, 2, 5, 4, 2 }。由于数字 2 在数组中出现了 5 次,超过数组长度的一半,因此输出 2。
问题分析
如果数组中该数的出现次数超过一半,设数组中位数为 k k k,数组中第 k k k大的数即为所求的数,因此可以采用二分查找法,时间复杂度为 O ( n ) O(n) O(n),代码如下所示:
public class Main {
public static int solution(int[] nums){
if (nums == null || nums.length == 0){
throw new IllegalArgumentException();
}
int k = nums.length / 2;
int val = select(nums, k);
int count = 0;
for (int i = 0; i < nums.length; i++){
if (nums[i] == val){
count++;
}
}
if (count <= nums.length/2){
throw new IllegalArgumentException();
}
return val;
}
private static int select(int[] nums, int k) {
int lo = 0, hi = nums.length-1;
while (lo < hi){
int pivot = partition(nums, lo, hi);
if (pivot == k){
return nums[k];
} else if (pivot < k){
lo = pivot + 1;
} else {
hi = pivot - 1;
}
}
return nums[k];
}
private static int partition(int[] nums, int lo, int hi) {
int val = nums[lo];
int i = lo, j = hi+1;
while (true){
while (less(nums[++i], val)) if (i == hi) break;
while (less(val, nums[--j])) if (j == lo) break;
if (i >= j) break;
swap(nums, i, j);
}
swap(nums, lo, j);
return j;
}
private static void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
private static boolean less(int v, int w) {
return v < w;
}
}
除此之外还可以使用计数法来解该题。当数组中有一个数的出现次数比其他所有数字出现次数的和还要多时,可以保存两个数,一个用于记录数组中的数字,一个用于记录次数。当遍历数字时,如果下一个数字和之前保存的数相同,次数加一,否则减一。如果次数为 0,那么我们需要保存下一个数字,并把次数置 1。由于要找的数字出现次数比其他数字出现的次数都多,那么要找的数字肯定是最后一次把数字设为 1 时对应的数字,代码如下:
public class Main {
public static int solution(int[] nums){
if (nums == null || nums.length == 0){
throw new IllegalArgumentException();
}
int num = nums[0];
int count = 1;
for (int i = 1; i < nums.length; i++){
if (count == 0){
num = nums[i];
count = 1;
} else if (nums[i] == num){
count++;
} else {
count--;
}
}
count = 0;
for (int i = 0; i < nums.length; i++){
if (nums[i] == num){
count++;
}
}
if (count <= nums.length/2){
throw new IllegalArgumentException();
}
return num;
}
}
问题描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。
示例:输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
问题分析
这道题的官方解法可以参照这篇文章:接雨水官方题解,写的非常详细,笔者这里只记录下实现代码。
1)暴力法
public class Main {
public static int solution(int[] height){
if (height == null){
throw new IllegalArgumentException("input array is null");
}
if (height.length < 3){
return 0;
}
int n = height.length;
int result = 0;
for (int i = 1; i < n; i++){
int maxLeft = 0;
int maxRight = 0;
for (int j = i; j >= 0; j--){
maxLeft = Math.max(maxLeft, height[j]);
}
for (int j = i; j < n; j++){
maxRight = Math.max(maxRight, height[j]);
}
result += Math.min(maxLeft, maxRight) - height[i];
}
return result;
}
}
暴力法是通过计算每一格能蓄的最大雨水量来得出结果的,它的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。
2)动态规划法
动态规划属于对暴力法的改进,将左右的最大值记录下来就可以不用重复地求取最大值了,实现代码如下:
public class Main {
public static int solution(int[] height){
if (height == null){
throw new IllegalArgumentException("input array is null");
}
if (height.length < 3){
return 0;
}
int n = height.length;
int[] maxLeft = new int[n];
int[] maxRight = new int[n];
maxLeft[0] = height[0];
maxRight[n-1] = height[n-1];
for (int i = 1; i < n; i++){
maxLeft[i] = Math.max(maxLeft[i-1], height[i]);
}
for (int i = n-2; i > 0; i--){
maxRight[i] = Math.max(maxRight[i+1], height[i]);
}
int result = 0;
for (int i = 1; i < n; i++){
result += Math.min(maxLeft[i], maxRight[i]) - height[i];
}
return result;
}
}
时间复杂度和空间复杂度均为 O ( n ) O(n) O(n),相当于用空间换时间的做法。
3)双指针法
详细算法解析请见题解,下面直接给出实现代码:
public class Main {
public static int solution(int[] height){
if (height == null){
throw new IllegalArgumentException("input array is null");
}
int left = 0, right = height.length-1;
int maxLeft = 0, maxRight = 0;
int result = 0;
while (left < right){
if (height[left] < height[right]){
if (height[left] >= maxLeft){
maxLeft = height[left];
} else {
result += maxLeft - height[left];
}
left++;
} else {
if (height[right] >= maxRight){
maxRight = height[right];
} else {
result += maxRight - height[right];
}
right--;
}
}
return result;
}
}
这种做法时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
给出一个 32 位的有符号整数,你需要将这个整数中每位上的数字进行反转。
示例 1:
输入: 123
输出: 321
示例 2:输入: -123
输出: -321
示例 3:输入: 120
输出: 21
问题分析
题目不难,但是需要特别注意范围溢出的问题。实现代码如下:
public class Main {
public static int solution(int x) {
int result = 0;
while (x != 0) {
int pop = x % 10;
x /= 10;
if (result > Integer.MAX_VALUE/10 || (result == Integer.MAX_VALUE/10 && pop > 7)) {
return 0;
}
if (result < Integer.MIN_VALUE/10 || (result == Integer.MIN_VALUE/10 && pop < -8)) {
return 0;
}
result *= 10;
result += pop;
}
return result;
}
}
问题描述
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。 节点的右子树只包含大于当前节点的数。 所有左子树和右子树自身必须也是二叉搜索树。
问题分析
这道题的解法最常规的是通过中序遍历去遍历整棵树,如果中序遍历完成后符合递增规则的话,返回true即可,代码实现如下:
public class Main {
public static boolean isBST(TreeNode root){
if (root == null){
return true;
}
double min = -Double.MAX_VALUE;
Stack<TreeNode> stack = new Stack<>();
while (!stack.isEmpty() || root != null){
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
if (root.val <= min){
return false;
}
min = root.val;
root = root.right;
}
return true;
}
}
除了中序遍历的方式外,我们还可以采用递归法判断,实现代码如下所示:
public class Main {
public static boolean isBST(TreeNode root){
return isBST(root, null, null);
}
private static boolean isBST(TreeNode node, Integer min, Integer max){
if (node == null){
return true;
}
int val = node.val;
if (min != null && val <= min) return false;
if (max != null && val >= min) return false;
if (!isBST(node.left, min, val)) return false;
if (!isBST(node.right, val, max)) return false;
return true;
}
}
问题描述
给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。
你可以假设除了数字 0 之外,这两个数字都不会以零开头。
进阶:
如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。
示例:
输入: (7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出: 7 -> 8 -> 0 -> 7
问题分析
这道题最简单的做法就是翻转链表然后进行计算,实现代码如下所示:
public class Main {
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if (l1 == null){
return l2;
}
if (l2 == null) {
return l1;
}
ListNode rev1 = reverseList(l1);
ListNode rev2 = reverseList(l2);
ListNode result = null;
int carry = 1;
while (rev1 != null || rev2 != null || carry != 0){
int sum = carry;
if (rev1 != null){
sum += rev1.val;
}
if (rev2 != null){
sum += rev2.val;
}
ListNode newNode = new ListNode(sum % 10);
newNode.next = result;
result = newNode;
carry = sum / 10;
}
return result;
}
private static ListNode reverseList(ListNode list){
ListNode prev = null;
ListNode curr = list;
while (curr != null) {
ListNode nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
return prev;
}
}
如果不希望出现修改链表的情况,我们可以采用两个栈存储链表的数值,一样可以达到效果,实现代码如下:
public class Main {
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
if (l1 == null){
return l2;
}
if (l2 == null) {
return l1;
}
Stack<Integer> s1 = new Stack<>();
Stack<Integer> s2 = new Stack<>();
ListNode temp = l1;
while (temp != null){
s1.push(temp.val);
temp = temp.next;
}
temp = l2;
while (temp != null){
s2.push(temp.val);
temp = temp.next;
}
int carry = 0;
ListNode result = null;
while (!s1.isEmpty() || !s2.isEmpty() || carry != 0) {
int sum = carry;
if (!s1.isEmpty()) {
sum += s1.pop();
}
if (!s2.isEmpty()) {
sum += s2.pop();
}
ListNode newNode = new ListNode(sum % 10);
newNode.next = result;
result = newNode;
}
return result;
}
}
问题描述
给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。
你可以假设每种输入只会对应一个答案。但是,你不能重复利用这个数组中同样的元素。
示例:
给定 nums = [2, 7, 11, 15], target = 9
因为 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]
问题分析
这道题的解答方式有很多种,最简单也是最容易想到的就是暴力法,实现代码如下所示:
public class Main {
/**
* 暴力法
*/
public static int[] towsum(int[] nums, int target){
if (nums == null || nums.length < 2){
return new int[2];
}
int n = nums.length;
for (int i = 0; i < n-1; i++) {
for (int j = i+1; j < n; j++){
if (nums[i]+nums[j] == target){
return new int[]{i, j};
}
}
}
return new int[2];
}
}
这种方式的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。而优化的思路可以通过哈希表的方式来解决,实现代码如下:
public class Main {
/**
* 哈希表优化
*/
public static int[] towsum(int[] nums, int target){
if (nums == null || nums.length < 2){
return new int[2];
}
int n = nums.length;
Map<Integer, Integer> map = new HashMap(n); // 构造器建议加上参数n,否则频繁的扩容会降低效率
for (int i = 0; i < n; i++) {
if (map.containsKey(target-nums[i])){
return new int[]{map.get(target-nums[i]), i)};
}
map.put(nums[i], i);
}
return new int[2];
}
}
上述方法通过一次遍历的方式即可完成,时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)。需要注意的是哈希表初始化时尽量带上参数n,避免因为扩容导致性能下降,采用哈希表保存数据的原因是理论上在哈希表中查询一个元素复杂度为 O ( 1 ) O(1) O(1)。
问题描述
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
问题分析
这道题的思路参照的是Leetcode上的一个题解,思路直接点超链接观看即可,这里只记录下代码:
public class Main {
public static List<List<Integer>> threeSum(int[] nums) {
if (nums == null || nums.length < 3){
return new ArrayList<>();
}
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums); 先对数组进行排序
for (int i = 0; i < nums.length; i++) {
if (nums[i] > 0) break; // 如果第一个数大于0那么不存在三数之和等于0的数
if (i > 0 && nums[i] == nums[i-1]) continue; // 去重
int lo = i+1, hi = nums.length-1;
while (lo < hi) {
int sum = nums[i] + nums[lo] + nums[hi];
if (sum == 0) {
result.add(Arrays.asList(nums[i], nums[lo], nums[hi]));
// 去重
while (lo < hi && nums[lo] == nums[lo+1]) lo++;
while (lo < hi && nums[hi] == nums[hi-1]) hi--;
lo++;
hi--;
} else if (sum < 0){
lo++;
} else {
hi--;
}
}
}
return result;
}
}
这种解法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
问题描述
twosum:找出一个数组中所有和为 0 的整数对的数量。
threesum: 找出一个数组中所有和为 0 的三元组的数量。
问题分析
为了简化起见我们假设数组中所有的数都是不相同的,那么对于 twosum 问题来说,我们可以先将数据排好序,然后遍历数组,查找数组中是否含有该数的相反数,如果我们采用二分法查找,查找一个数的时间复杂度为 O ( l o g n ) O(logn) O(logn),如果遍历一个数组那么总的时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn),而排序所需的时间也为 O ( n l o g n ) O(nlogn) O(nlogn),所以这种解法的总时间复杂度就是 O ( n l o g n ) O(nlogn) O(nlogn),实现代码如下:
public class Main {
public static int twosumCount(int[] nums){
if (nums == null || nums.length < 2) {
return 0;
}
Arrays.sort(nums);
int result = 0;
for (int i = 0; i < nums.length; i++) {
// 为了避免重复计数,只统计索引值大于i的情况
if (binarySearch(nums, 0-nums[i]) > i) {
result++;
}
}
return result;
}
/**
* 返回有序数组nums中值为target的索引值,不存在返回-1
*/
private static int binarySearch(int[] nums, int target){
int lo = 0, hi = nums.length-1;
while (lo <= hi) {
int mid = lo + (hi-lo)/2;
if (nums[i] == target){
return mid;
} else if (nums[i] < target) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
}
而对于三元组来说,它其实就是二元组的扩展,我们可以再加一层循环,所以复杂度就上升为 O ( n 2 l o g n ) O(n^{2}logn) O(n2logn),实现代码如下所示:
public class Main {
public static int twosumCount(int[] nums){
if (nums == null || nums.length < 3) {
return 0;
}
Arrays.sort(nums);
int result = 0;
for (int i = 0; i < nums.length-2; i++) {
for (int j = i+1; j < nums.length-1; j++) {
if (binarySearch(nums, 0-(nums[i]+nums[j]) > j) {
result++;
}
}
}
return result;
}
/**
* 返回有序数组nums中值为target的索引值,不存在返回-1
*/
private static int binarySearch(int[] nums, int target){
int lo = 0, hi = nums.length-1;
while (lo <= hi) {
int mid = lo + (hi-lo)/2;
if (nums[i] == target){
return mid;
} else if (nums[i] < target) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
}
问题描述
输入一个整数 n,求 1~n 这 n 个整数的十进制表示中 1 出现的次数。例如输入 12,1~12 这些整数中包含 1 的数字有1、10、11 和 12,1 一共出现了 5 次。
问题分析
这道题最容易想到的一种方式是直接遍历这 n 个数,然后计算每个数中 1 出现的次数即可,实现代码如下:
public class Main {
public static int solution(int n){
if (n <= 0){
throw new IllegalArgumentException("n <= 0");
}
int result = 0;
for (int i = 1; i <= n; i++){
result += numberOf1(i);
}
return result;
}
private static int numberOf1(int num) {
int result = 0;
while (num != 0){
if (num % 10 == 1){
result++;
}
num /= 10;
}
return result;
}
}
对于每个数字 n,我们都需要对其进行处理,n 有 O ( l o g n ) O(logn) O(logn)位,所以这种解法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
如果想要优化,则需要从数字本身去寻找规律,由于《剑指Offer》中已经说的非常详细,就不在这里班门弄斧了,直接上代码:
public class Main {
public static int solution(int n){
if (n <= 0){
throw new IllegalArgumentException("n <= 0");
}
char[] num = String.valueOf(n).toCharArray();
return numberOf1(num, 0)
}
private static int numberOf1(chr[] num, int i) {
if (i == num.length){
return 0;
}
int len = nums.length - i;
int first = char[i] - '0';
if (len == 1 && first == 0){
return 0;
}
if (len == 1 && first > 0){
return 1;
}
// 最高位为1的时候的个数
int countOfHighestDigit = 0;
if (first > 1) {
countOfHighestDigit = (int) Math.pow(10, len-1);
} else if (first == 1) {
String str = new String(num, i+1, num.length-1);
countOfHighestDigit = Integer.valueOf(str) + 1;
}
// 其他位为1的时候的个数
int countOfOtherDigit = first * (len-1) * (int) Math.pow(10, len-2);
// 递归部分的个数
int countOfRecursive = numberOf1(num, i+1);
return countOfHighestDigit + countOfOtherDigit + countOfRecursive;
}
}
这种优化过后的解法时间复杂度取决于 n 的位数,所以为 O ( l o g n ) O(logn) O(logn)。
问题分析
有了前面 33 题做的铺垫,我们能够很自然的写出下列代码:
public class Main {
public static int solution(int n){
if (n <= 0) {
throw new IllegalArgumentException("n <= 0");
}
String num = String.valueOf(n);
return numberOf7(num, 0);
}
private static int numberOf7(String num, int i){
if (i == num.length()) {
return 0;
}
int len = num.length() - i;
// first为最大位数的数字
int first = num.charAt(i) - '0';
if (len == 1 && first < 7){
return 0;
}
if (len == 1 && first >= 7) {
return 1;
}
int countOfHighestDigit = 0;
if (first > 7) {
countOfHighestDigit = (int) Math.pow(10, len-1);
} else if (first == 7) {
String sub = num.substring(i+1);
countOfHighestDigit = Integer.valueOf(sub) + 1;
}
// 最高位固定时其他位数出现7的次数
int countOfOtherDigit = (int)(first * (len-1) * Math.pow(10, len-2));
int countOfRecursive = numberOf7(num, i+1);
return countOfHighestDigit + countOfOtherDigit + countOfRecursive;
}
public static void main(String[] args) {
System.out.println(solution(100000));
}
}
大数问题的解决方法一般是通过模拟法来解决,下面是各个运算的实现代码。我们这里不考虑输入值为负数的情况,并且每个输入值前面也无冗余 0(如 “023”)。
1). 大数加法
大数相加比较简单,只需要简单处理进位即可,代码实现如下所示:
public class Main {
public static String bigIntegerAdd(String num1, String num2) {
if (!isValid(num1) || !isValid(num2)) {
throw new IllegalArgumentException();
}
int n = Math.max(num1.length(), num2.length());
int[] x = new int[n];
int[] y = new int[n];
int[] z = new int[n];
// 初始化
for (int i = 0; i < num1.length(); i++) {
x[i] = num1.charAt(num1.length()-1-i) - '0';
}
for (int i = 0; i < num2.length(); i++) {
y[i] = num2.charAt(num2.length()-1-i) - '0';
}
// 相加计算
int carry = 0;
for (int i = 0; i < n; i++) {
int sum = carry + x[i] + y[i];
z[i] = sum % 10;
carry = sum / 10;
}
StringBuilder sb = new StringBuilder();
if (carry == 1){
sb.append(1);
}
for (int i = n-1; i >= 0; i--) {
sb.append(z[i]);
}
return sb.toString();
}
private static boolean isValid(String num) {
if (num == null || num.length() == 0) {
return false;
}
for (int i = 0; i < num.length(); i++) {
char ch = num.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
return true;
}
}
2). 大数减法
大数相减相比起大数相加的处理稍微麻烦一些,需要需要三点:
实现代码如下所示:
public class Main {
public static String bigIntegerSub(String num1, String num2) {
if (!isValid(num1) || !isValid(num2)) {
throw new IllegalArgumentException();
}
// isNav表示结果是否为负值
boolean isNav = false;
// 为了方便处理我们让num1大于num2
int cmp = compare(num1, num2);
if (cmp == 0){
return "0";
} else if (cmp < 0) {
String temp = num1;
num1 = num2;
num2 = temp;
isNav = true;
}
int n = num1.length();
int[] x = new int[n];
int[] y = new int[n];
int[] z = new int[n];
// 初始化
for (int i = 0; i < num1.length(); i++) {
x[i] = num1.charAt(num1.length()-1-i) - '0';
}
for (int i = 0; i < num2.length(); i++) {
y[i] = num2.charAt(num2.length()-1-i) - '0';
}
// 相减计算
for (int i = 0; i < n; i++) {
if (x[i] >= y[i]) {
z[i] = x[i] - y[i];
} else {
z[i] = 10 + x[i] - y[i];
x[i+1]--;
}
}
// 去除多余的0
int index = n-1;
for (int i = n-1; i >= 0; i--) {
if (z[i] != 0) {
index = i;
break;
}
}
StringBuilder sb = new StringBuilder();
if (isNav) {
sb.append("-");
}
for (int i = index; i >= 0; i--) {
sb.append(z[i]);
}
return sb.toString();
}
private static int compare(String num1, String num2) {
int len1 = num1.length();
int len2 = num2.length();
if (len1 > len2) {
return 1;
} else if (len2 > len1) {
return -1;
} else {
return num1.compareTo(num2);
}
}
private static boolean isValid(String num) {
if (num == null || num.length() == 0) {
return false;
}
for (int i = 0; i < num.length(); i++) {
char ch = num.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
return true;
}
}
3). 大数乘法
大数相乘同样是采用模拟法的方式进行处理,两个数相乘的最大长度是两个数长度相加,例如 99 × 999 = 98901 99\times999=98901 99×999=98901,也就是两位数乘以三位数的最大长度为 5 位数,最小长度为 4 位数,实现代码如下所示:
public class Main {
public static String bigIntegerMutiply(String num1, String num2) {
if (!isValid(num1) || !isValid(num2)) {
throw new IllegalArgumentException();
}
int len1 = num1.length();
int len2 = num2.length();
int n = len1 + len2;
int[] x = new int[len1];
int[] y = new int[len2];
int[] z = new int[n];
// 初始化
for (int i = 0; i < len1; i++) {
x[i] = num1.charAt(len1-1-i) - '0';
}
for (int i = 0; i < len2; i++) {
y[i] = num2.charAt(len2-1-i) - '0';
}
// 模拟乘法计算
for (int i = 0; i < len1; i++) {
for (int j = 0; j < len2; j++) {
z[i+j] += x[i] * y[j];
}
}
// 对z进行调整
for (int i = 0; i < n; i++) {
if (i == n-1 || z[i] < 10) {
continue;
}
z[i+1] += z[i] / 10;
z[i] %= 10;
}
StringBuilder sb = new StringBuilder();
for (int i = n-1; i >= 0; i--) {
if (i == n-1 && z[i] == 0) {
continue;
}
sb.append(z[i]);
}
return sb.toString();
}
private static boolean isValid(String num) {
if (num == null || num.length() == 0) {
return false;
}
for (int i = 0; i < num.length(); i++) {
char ch = num.charAt(i);
if (ch < '0' || ch > '9') {
return false;
}
}
return true;
}
}
4). 大数除法
大数除法可分为两种:取模和求商,两者的本质是一样的,这里只写求商的代码。大数除法的本质就是对除数不断地进行减法操作,每减一次结果就可以+1,直到被除数小于除数为止。但是一次一次减非常的慢,我们可以想办法以尽可能大的倍数去减,从而提高效率,实现代码如下:
5). 大数阶乘
大数阶乘采用的是每一位分别乘以 n,然后再逐位进行调整的方式实现的,代码如下:
public class Main {
public static String bigIntegerFactorial(int n) {
if (n < 0) {
throw new IllegalArgumentException("n < 0");
}
int digit = 1;
int[] result = new int[2001];
result[0] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 0; j < digit; j++) {
result[j] *= i;
}
// 调整
for (int j = 0; j < digit; j++) {
if (result[i] < 10) {
continue;
}
result[j+1] += result[j] / 10;
result[j] /= 10;
}
if (result[digit] != 0) {
digit++;
}
}
StringBuilder sb = new StringBuilder();
for (int i = digit-1; i >= 0; i--) {
sb.append(result[i]);
}
return sb.toString();
}
}
问题描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。
示例 2:输入: “cbbd”
输出: “bb”
问题分析
最长回文子串的最优解算法是马拉车算法,但是在面试过程中手撕马拉车算法是比较有难度的一件事,这里采用另一种效率较低但是比较容易的思路,实现代码如下:
public class Main {
public static String longestPalindrome(String str) {
if (str == null || str.length() < 2) {
return str;
}
int lo = 0, hi = 0;
for (int i = 0; i < n; i++){
int len1 = expand(str, i, i);
int len2 = expand(str, i, i+1);
int len = Math.max(len1, len2);
if (len > hi - lo) {
lo = i - (len-1)/2;
hi = i + len/2;
}
}
return str.substring(lo, hi+1);
}
/**
* 从left和right开始往两侧扩展,计算最长回文子串长度
*/
private static int expand(String str, int left, int right){
while (left >= 0 && right < str.length() && str.charAt(left) == str.charAt(right)) {
left--;
right++;
}
return right-left-1;
}
}
这种方法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
问题分析
这道题根据滑窗机制结合HashMap即可在遍历一次数组的情况下完成对最大不重复子串的计算,实现代码如下:
class Main {
/**
* 思路:假设[i, j-1]为不重复子串的滑窗,当s[j]与[i, j-1]中存在字符重复时,假设为s[j'],
* 那么滑窗的下一个起始点位置为j'+1
*/
public static int lengthOfLongestSubstring(String s) {
if (s == null){
throw new IllegalArgumentException();
}
// map键为字符,值为该字符的下一个索引值
Map<Character, Integer> map = new HashMap<>();
int i = 0;
int result = 0;
for (int j = 0; j < s.length(); i++){
char ch = s.charAt(j);
if (map.containsKey(ch)){
i = Math.max(map.get(ch), i);
}
result = Math.max(j-i+1, result);
map.put(ch, j+1);
}
}
}
该种解法的时间复杂度为 O ( n ) O(n) O(n)。
问题描述
给定一个整数矩阵,找出最长递增路径的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。
示例 1:
输入: nums =
[
[9,9,4],
[6,6,8],
[2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]。
示例 2:输入: nums =
[
[3,4,5],
[3,2,6],
[2,2,1]
]
输出: 4
解释: 最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。
问题分析
采用 DFS 的方式,我们能够很快地写出这道题的答案,实现代码如下:
public class Main {
private static final int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public static int longestIncreasingPath(int[][] matrix){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return 0;
}
int n = matrix.length;
int m = matrix[0].length;
int result = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result = Math.max(result, dfs(matrix, n, m, i, j));
}
}
}
private static int dfs(int[][] matrix, int n, int m, int i, int j){
int result = 0;
for (int[] d : dirs){
int x = i + d[0];
int y = j + d[1]
if (x >= 0 && x < n && y >= 0 && y < m && matrix[x][y] > matrix[i][j]){
result = Math.max(result, dfs(matrix, n, m, i, j));
}
}
return result++;
}
}
这种做法可以解决问题,但是复杂度非常高,所以往往会导致超时。这种解法的时间复杂度为 O ( 2 m + n ) O(2^{m+n}) O(2m+n)。
优化的方式是采用缓存,因为很容易发现在DFS的时候是包含了很多的重复计算的,因此我们可以使用一个缓存数组来保存值,加快程序的速度,代码如下所示:
public class Main {
private static final int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
public static int longestIncreasingPath(int[][] matrix){
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
return 0;
}
int n = matrix.length;
int m = matrix[0].length;
int[][] cache = new int[n][m];
int result = 0;
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
result = Math.max(result, dfs(matrix, cache, n, m, i, j));
}
}
return result;
}
private static int dfs(int[][] matrix, int[][] cache, int n, int m, int i, int j){
if (cache[i][j] != 0) return cache[i][j];
for (int[] d : dirs){
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && x < n && y >= 0 && y < n && matrix[x][y] > matrix[i][j]){
cache[i][j] = Math.max(cahche[i][j], dfs(matrix, cache, n, m, x, y));
}
}
return ++cache[i][j];
}
}
在添加了缓存之后,复杂度降为了 O ( n m ) O(nm) O(nm)。
问题描述
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。
示例 1:
输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL
示例 2:输入: 2->1->3->5->6->4->7->NULL
输出: 2->3->6->7->1->5->4->NULL
说明:
- 应当保持奇数节点和偶数节点的相对顺序。
- 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
问题分析
这道题的思路是维护两个指针,一个连接的是奇数索引的链表,一个维护的是偶数索引的链表,最后把偶数索引的链表接在奇数索引链表的后面即可,实现代码如下:
class Main {
public static ListNode oddEvenList(ListNode head) {
if (head == null || head.next == null){
return head;
}
ListNode odd = head; // 奇数索引
ListNode even = head.next, evenHead = even; // 偶数索引
while (even != null && even.next != null) {
odd.next = even.next;
odd = odd.next;
even.next = odd.next;
even = even.next;
}
odd.next = evenHead;
return head;
}
}
此种解法时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
给定正整数 N ,我们按任何顺序(包括原始顺序)将数字重新排序,注意其前导数字不能为零。
如果我们可以通过上述方式得到 2 的幂,返回 true;否则,返回 false。
示例 1:
输入:1
输出:true
示例 2:
输入:10
输出:false
示例 3:
输入:16
输出:true
示例 4:
输入:24
输出:false
示例 5:
输入:46
输出:true
问题分析
对于输入值 n,我们将其分解,然后判断数组中的元素是否和某个2的次幂的数组组成完全相等即可。例如数字 46 分解为数组 {4, 6},它和 64 的组成数组 {6, 4} 是相等的,所以可以组成2的次幂,实现代码如下:
public class Main{
public static boolean reorderedPowerOf2(int n) {
if (n < 1 || n > 1000000000){
throw new IllegalArgumentException();
}
int[] temp = helper(n);
for (int i = 0; i < 31; i++){
if (Arrays.equals(temp, helper(1 << i))){
return true;
}
}
return false;
}
private static int[] helper(int n){
int[] a = new int[10];
while (n != 0){
a[n % 10]++;
n /= 10;
}
return a;
}
}
问题描述
编写一个函数来验证输入的字符串是否是有效的 IPv4 或 IPv6 地址。
IPv4 地址由十进制数和点来表示,每个地址包含4个十进制数,其范围为 0 - 255, 用(".")分割。比如,172.16.254.1;
同时,IPv4 地址内的数不会以 0 开头。比如,地址 172.16.254.01 是不合法的。
IPv6 地址由8组16进制的数字来表示,每组表示 16 比特。这些组数字通过 (":")分割。比如, 2001:0db8:85a3:0000:0000:8a2e:0370:7334 是一个有效的地址。而且,我们可以加入一些以 0 开头的数字,字母可以使用大写,也可以是小写。所以, 2001:db8:85a3:0:0:8A2E:0370:7334 也是一个有效的 IPv6 address地址 (即,忽略 0 开头,忽略大小写)。
然而,我们不能因为某个组的值为 0,而使用一个空的组,以至于出现 (::) 的情况。 比如, 2001:0db8:85a3::8A2E:0370:7334 是无效的 IPv6 地址。
同时,在 IPv6 地址中,多余的 0 也是不被允许的。比如, 02001:0db8:85a3:0000:0000:8a2e:0370:7334 是无效的。
说明: 你可以认为给定的字符串里没有空格或者其他特殊字符。
示例 1:
输入: “172.16.254.1”
输出: “IPv4”
解释: 这是一个有效的 IPv4 地址, 所以返回 “IPv4”。
示例 2:
输入: “2001:0db8:85a3:0:0:8A2E:0370:7334”
输出: “IPv6”
解释: 这是一个有效的 IPv6 地址, 所以返回 “IPv6”。
示例 3:
输入: “256.256.256.256”
输出: “Neither”
解释: 这个地址既不是 IPv4 也不是 IPv6 地址。
问题分析
题目本身是不难的,但是需要考虑的非法IP情况非常的多,要一次性考虑清楚还是挺有难度的,下面先将这些非法情况列举出来:
实现代码如下所示:
public class Main {
public static String validIPAddress(String IP) {
if (isValidIPv4(IP)){
return "IPv4";
} else if (isValidIPv6(IP)){
return "IPv6";
} else {
return "Neither";
}
}
private static boolean isValidIPv4(String ip){
if (ip == null || ip.startsWith(".") || ip.endsWith(".")){
return false;
}
// 点的个数
int dotCount = 0;
int num = 0;
for (int i = 0; i < ip.length(); i++){
char ch = ip.charAt(i);
if (ch == '.'){
// 检测是否存在二连"."的情况
if (i > 0 && ip.charAt(i-1) == '.'){
return false;
}
dotCount++;
num = 0;
} else if (ch <= '9' && ch >= '0'){
// 检测是否村咋冗余0
if (i > 0 && ip.charAt(i-1) == '0'){
return false;
}
num *= 10;
num += ch - '0';
if (num > 255){
return false;
}
} else {
return false;
}
}
return dotCount == 3;
}
private static boolean isValidIPv6(String ip){
if (ip == null || ip.startsWith(":") || ip.endsWith(":")){
return false;
}
// ':'的个数
int dotCount = 0;
StringBuilder temp = new StringBuilder();
for (int i = 0; i < ip.length(); i++){
char ch = ip.charAt(i);
if (ch == ':'){
// 检测是否存在二连":"的情况
if (i > 0 && ip.charAt(i-1) == ':'){
return false;
}
dotCount++;
temp.delete(0, temp.length());
} else if (isValidHex(ch)){
temp.append(ch);
// 检测是否存在冗余0的情况
if (temp.length() > 4){
return false;
}
} else {
return false;
}
}
return dotCount == 7;
}
private static boolean isValidHex(char ch){
return (ch >= '0' && ch <= '9') ||
(ch >= 'a' && ch <= 'f') ||
(ch >= 'A' && ch <= 'F');
}
}
此种解法时间复杂度为 O ( n ) O(n) O(n)。
问题描述
给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。
示例:
输入: “25525511135”
输出: [“255.255.11.135”, “255.255.111.35”]
问题分析
此题适合用回溯法进行解答,代码如下所示:
public class Main {
public static List<String> restoreIpAddresses(String s){
if (s == null || s.length() < 4 || s.length() > 12){
return new ArrayList<>();
}
LinkedList<String> segments = new LinkedList<>();
List<String> result = new ArrayList<>();
restoreCore(s, -1, 0, segments, result);
return result;
}
/**
* 恢复IP地址的核心方法,思想:回溯
* @param s
* @param prev 上一个点标定的位置
* @param dotCount 点的个数
* @param segments 存储ip地址有效整型的list
* @param result 存储合法ip地址的list
*/
private static void restoreCore(String s, int prev, int dotCount, LinkedList<String> segments, List<String> result){
int n = Math.min(s.length()-1, prev+4);
for (int curr = prev+1; curr < n; curr++){
String segment = s.substring(prev+1, curr+1);
if (isValid(segment)){
segments.add(segment);
// 当点为3个时,尝试去更新result,但是不一定会成功
// 因为第三个点之后的整型还未判断
if (dotCount + 1 == 3){
tryUpdateResult(s, curr, segments, result);
} else {
// 点数小于3时继续标点
restoreCore(s, curr, dotCount+1, segments, result);
}
// 回溯
segments.removeLast();
}
}
}
private static void tryUpdateResult(String s, int curr, LinkedList<String> segments, List<String> result){
String segment = s.substring(curr+1);
// 检测第三个点后的整型是否合法,判断合法
// 之后才会将ip地址添加到result中
if (isValid(segment)){
segments.add(segment);
String ip = convertIp(segments);
result.add(ip);
segments.removeLast();
}
}
private static String convertIp(LinkedList<String> segments){
StringBuilder sb = new StringBuilder();
for (String segment : segments){
sb.append(segment).append(".");
}
return sb.substring(0, sb.length()-1);
}
/**
* 判断segment是否为合法的整型,segment <= 255并且不能含有冗余的0值
* @param segment
* @return
*/
private static boolean isValid(String segment){
int n = segment.length();
if (n > 3){
return false;
}
return segment.charAt(0) == '0' ? n == 1 : Integer.valueOf(segment) <= 255;
}
}
问题描述
在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数。例如,在数组{7, 5, 6, 4}中,一共存在5个逆序对,分别是(7, 6)、(7, 5)、(7, 4)、(6, 4)和(5, 4)。
问题分析
这道题的解题思路是通过递归排序的方法实现的,详细的解题思路可以见《剑指Offer》面试题53,这里只给出实现代码,如下所示:
public class Main {
public static int inversePair(int[] a){
if (a == null || a.length < 2){
return 0;
}
// 递归排序所需的辅助数组
int n = a.length;
int[] aux = new int[n];
Arrays.copy(aux, a);
return inversePair(a, aux, 0, n-1);
}
private static int inversePair(int[] a, int[] aux, int lo, int hi){
if (lo == hi){
aux[lo] = a[lo];
}
int len = (hi-lo)/2;
int left = inversePair(aux, a, lo, lo+len);
int right = inversePair(aux, a, lo+len+1, hi);
int i = lo+len;
int j = hi;
int count = 0;
int copyIndex = hi;
while (i >= lo && j >= lo+len+1){
// 存在逆序对的情况
if (a[i] > a[j]){
aux[copyIndex--] = a[i--];
count += j - lo - len;
} else {
aux[copyIndex--] = a[j--];
}
}
// 将剩余的元素复制到辅助数组中
for (; i >= lo; i--){
aux[copyIndex--] = a[i];
}
for (; j >= lo+len+1; j--){
aux[copyIndex--] = a[j];
}
return left+right+count;
}
}
递归排序的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( n ) O(n) O(n),相比于直接使用暴力求解,相当于是用空间换取时间。
问题描述
给定两个大小为 m 和 n 的有序数组 nums1 和 nums2。
请你找出这两个有序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
示例 1:nums1 = [1, 3]
nums2 = [2]则中位数是 2.0
示例 2:nums1 = [1, 2]
nums2 = [3, 4]
则中位数是 (2 + 3)/2 = 2.5
问题分析
这道题的官方题解直接点链接观看吧,这里只说下笔者简略的理解:
所以实现代码如下所示:
public class Main {
public static double findMedianSortedArrays(int[] a, int[] b) {
if (a == null || b == null || (a.length == 0 && b.length == 0)){
throw new IllegalArgumentException();
}
// 确保数组a的长度不大于b
if (a.length > b.length){
int[] temp = a;
a = b;
b = temp;
}
int n = a.length;
int m = b.length;
// 双指针,用于标定 i
int lo = 0, hi = n;
int halfLen = (n + m + 1) / 2;
while (lo <= hi) {
int i = (hi + lo) / 2;
int j = halfLen - i;
if (i > 0 && a[i-1] > b[j]){
// i 太大了,需要减小
hi = i-1;
} else if (i < n && b[j-1] > a[i]){
// i 太小了,需要增大
lo = i+1;
} else { // 这是符合我们要求的 i
// 先计算左半部分的最大值
int maxLeft = 0;
if (i == 0) maxLeft = b[j-1];
else if (j == 0) maxLeft = a[i-1];
else maxLeft = Math.max(a[i-1], b[j-1]);
// 如果两个数组长度和为奇数,那么maxLeft即为目标值
if ((m + n) % 2 == 0) return maxLeft;
// 两数组长度和为偶数的话我们还需要取右半部分的最小值然后和maxLeft取平均数
int minRight = 0;
if (i == n) minRight = b[j];
else if (j == m) minRight = a[i];
else minRight = Math.min(a[i], b[j]);
return (maxLeft + minRight) / 2.0;
}
}
return 0.0;
}
这种解法的时间复杂度为 O ( l o g ( m i n ( n , m ) ) ) O(log(min(n, m))) O(log(min(n,m))),因为我们是对长度较小的数组做二分取值。空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
请判断一个链表是否为回文链表。
示例 1:
输入: 1->2
输出: false
示例 2:
输入: 1->2->2->1
输出: true
进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
问题分析
这道题的思路不算复杂,先找到链表的中点,然后对前半部分做一次链表反转,再和后半部分的链表比较就可以判断该链表是否为回文链表了,实现代码如下所示:
publc class Main {
public static boolean isPalindrome(ListNode head){
if (head == null || head.next == null){
return true;
}
ListNode slow = head;
ListNode fast = head.next;
while (fast != null && fast.next != null){
fast = fast.next.next;
slow = slow.next;
}
ListNode right = slow.next;
slow.next = null;
ListNode left = reverseList(head);
if (fast == null){
// fast为 null说明链表长度为奇数
left = left.next;
}
// 检测是否为回文
while (left != null){
if (left.val != right.val){
return false;
}
left = left.next;
right = right.next;
}
return true;
}
/**
* 反转链表的方法
*/
private static ListNode reverseList(ListNode head){
if (head == null || head.next == null){
return head;
}
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode nextNode = curr.next;
curr.next = prev;
prev = curr;
curr = nextNode;
}
return prev;
}
}
这种解法时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被计为是不同的子串。
示例 1:输入: “abc”
输出: 3
解释: 三个回文子串: “a”, “b”, “c”.
示例 2:输入: “aaa”
输出: 6
说明: 6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”.
问题分析
这道题采用中心扩展法可以较快的做出来,代码如下所示:
public class Main {
public static int countSubstrings(String s) {
if (s == null || s.length() == 0){
return 0;
}
int result = 0;
for (int i = 0; i < s.length(); i++){
result += countCore(s, i, i) + countCore(s, i, i+1);
}
return result;
}
private static int countCore(String s, int lo, int hi){
int result = 0;
while (lo >= 0 && hi < s.length() && s.charAt(lo) == s.charAt(hi)){
result++;
lo--;
hi++;
}
return result;
}
}
这种解法的算法复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
在一个长度为 n n n 的数组里的所有数字都在 0 n − 1 0~n-1 0 n−1 的范围内。数组中某些数字是重复的,但是不知道有几个数字重复了,也不知道每个数字重复了几次。请找出数组中任意一个重复的数字。例如,输入长度为 7 的数组{2, 3, 1, 0, 2, 5, 3},那么对应的输出是重复数字 2 或者 3。
问题分析
这道题的解题思路可以采用哈希表的方式去解决,遍历数组时将数组中的数逐个插入表中,并检测表中是否已经存在该数,如果存在就可以直接返回,这种方式的时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n),实现代码如下:
public class Main {
public static int solution(int[] a){
if (a == null || a.length == 0){
throw new IllegalArgumentException();
}
Set<Integer> set = new HashSet<>();
for (int i = 0; i < a.length; i++){
if (set.contains(a[i])){
return a[i];
}
set.add(a[i]);
}
throw new IllegalArgumentException();
}
}
如果我们想要在空间复杂度为 O ( 1 ) O(1) O(1) 的条件下解出这道题,那么我们就得利用数组的特点来解决了,实现代码如下所示:
public class Main {
public static int solution(int[] a){
if (a == null || a.length == 0){
throw new IllegalArgumentException();
}
for (int i = 0; i < a.length; i++){
if (i != a[i]){
int temp = a[a[i]];
if (temp == a[i]){
return temp;
}
a[a[i]] = a[i];
a[i] = temp;
}
}
throw new IllegalArgumentException();
}
}
这种解法的复杂度同样为 O ( n ) O(n) O(n)。
问题描述
把数组的负数移动到正数之前,不能改变正负数原先的次序,例如数组 {3, 4, -1, -3, 5, 2, -7, 6, 1} 移动之后变为 {-1, -3, -7, 3, 4, 5, 2, 6, 1}。
问题分析
这道题看起来是挺容易的,但是如果要求在时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1) 的情况下解答就变得不那么容易了,笔者采用的是递归法,即采用二分法将数组左右两边分别变为负数在前,正数在后的两部分,然后再将这两部分归并为一个数组,这种做法的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1),解法如下所示:
public class Main {
public static void solution(int[] a){
if (a == null || a.length < 2){
return;
}
int n = a.length;
merge(a, 0, (n-1)/2, n-1);
}
private static void merge(int[] a, int lo, int mid, int hi) {
if (lo >= hi){
return;
}
// 处理数组的左右部分
merge(a, lo, (lo+mid)/2, mid);
merge(a, mid+1, (mid+1+hi)/2, hi);
int i = lo, j = hi;
// 找到左半部分数组第一个大于0的数
for (; i <= mid; i++) if (a[i] > 0) break;
// 找到右半部分数组最后一个小于0的数
for (; j > mid; j--) if (a[j] < 0) break;
// 翻手法将对数组进行调整
// 假设原先左右部分为 ---+++ | ---+++,下面三步翻手后就变为
// ------ | ++++++
reverse(a, i, mid);
reverse(a, mid+1, j);
reverse(a, i, j);
}
private static void reverse(int[] a, int lo, int hi) {
if (lo >= hi){
return;
}
int mid = lo + (hi-lo)/2;
for (int i = lo; i <= mid; i++){
int temp = a[i];
a[i] = a[hi];
a[hi--] = temp;
}
}
public static void main(String[] args) {
int[] a = { 3, 4, -1, -3, 5, 2, -7, 6, 1};
solution(a);
for (int i : a){
System.out.print(i + " ");
}
}
}
问题描述
假设按照升序排序的数组在预先未知的某个点上进行了旋转。
( 例如,数组 [0,1,2,4,5,6,7] 可能变为 [4,5,6,7,0,1,2] )。
搜索一个给定的目标值,如果数组中存在这个目标值,则返回它的索引,否则返回 -1 。
你可以假设数组中不存在重复的元素。
你的算法时间复杂度必须是 O(log n) 级别。
示例 1:
输入: nums = [4,5,6,7,0,1,2], target = 0
输出: 4
示例 2:
输入: nums = [4,5,6,7,0,1,2], target = 3
输出: -1
问题分析
这道题的思路是先找到旋转点,然后再根据 target 和 nums[0] 的大小比较获取结果,代码如下所示:
public class Main {
public static int search(int[] nums, int target){
if (nums == null || nums.length == 0){
return -1;
}
if (nums.length == 1){
return nums[0] == target ? 0 : -1;
}
int n = nums.length;
int i = findRotateIndex(nums, 0, n-1);
if (nums[i] == target){
return i;
}
if (i == 0){
return search(nums, 0, n-1, target);
}
if (nums[0] > target){
return search(nums, i, n-1, target);
} else {
return search(nums, 0, i-1, target);
}
}
private static int findRotateIndex(int[] nums, int lo, int hi){
if (lo == hi){
return lo;
}
while (lo < hi){
int mid = (lo+hi)/2;
if (a[mid] > a[mid+1]){
return mid+1;
} else {
if (a[mid] < a[lo]){
lo = mid+1;
} else {
hi = mid-1;
}
}
}
return lo;
}
private static int search(int[] nums, int lo, int hi, int target){
while (lo <= hi){
int mid = (lo+hi)/2;
if (a[mid] == target){
return mid;
} else if (a[mid] > target){
hi = mid-1;
} else {
lo = mid+1;
}
}
return -1;
}
}
问题描述
定义栈的数据结构,请在该类型中实现一个能够得到栈最小元素的 min 函数。在该栈中,调用 min、push 及 pop 的时间复杂度都是 O ( 1 ) O(1) O(1)。
问题分析
这道题最棘手的地方是 min 函数的处理,因为需要在 O ( 1 ) O(1) O(1) 的复杂度下得到最小值而又要保持栈的特性,所以我们需要一个辅助栈,每插入一个元素时都对最小值进行保存并压入栈,代码如下所示:
public class Stack<E> {
private Node head;
private Node minHead;
private int size;
private Comparator<E> comparator;
public Stack(){
}
public Stack(Comparator<E> comparator){
this.comparator = comparator;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
public E pop(){
if (head == null){
throw new NullPointerException("stack underflow");
}
E e = head.e;
Node newHead = head.next;
Node newMinHead = minHead.next;
// help gc
head.next = null;
minHead.next = null;
head = newHead;
minHead = newMinHead;
size--;
return e;
}
public E min(){
if (head == null){
throw new NullPointerException("stack underflow");
}
return minHead.e;
}
public void push(E e){
Objects.requireNonNull(e);
Node newNode = new Node(e);
newNode.next = head;
head = newNode;
if (minHead != null && less(minHead.e, e)){
newNode = new Node(minHead.e);
} else {
newNode = new Node(e);
}
size++;
newNode.next = minHead;
minHead = newNode;
}
private boolean less(E v, E w){
return comparator != null ? comparator.compare(v, w) < 0 :
((Comparable) v).compareTo(w) < 0;
}
private class Node {
E e;
Node next;
Node(E e){
this.e = e;
}
}
/**
* Unit test
*
* @param args
*/
public static void main(String[] args) {
Stack<Integer> stack = new Stack<>();
stack.push(3);
System.out.println("min: " + stack.min());
stack.push(4);
System.out.println("min: " + stack.min());
stack.push(2);
System.out.println("min: " + stack.min());
stack.push(1);
System.out.println("min: " + stack.min());
System.out.println("pop: " + stack.pop());
System.out.println("min: " + stack.min());
System.out.println("pop: " + stack.pop());
System.out.println("min: " + stack.min());
stack.push(0);
System.out.println("min: " + stack.min());
}
}
这里因为使用了泛型,所以添加了比较器 Comparator,如果栈元素类型固定为整型的话就无需比较器,代码可以节省一些。
问题描述
问题分析
两结点的最长距离可能经过根结点、在左子树中和在右子树中三种情况,如下所示:
因此我们需要对这三种情况进行计算并取它们的最大值,代码如下所示:
public class Main {
public static int getMaximumLength(TreeNode root){
return getMaximumCore(root)[1];
}
/**
* 返回值是一个长度为2的数组,第一个值为树的深度,第二个值为结点最长距离
*/
public static int[] getMaximumCore(TreeNode root){
if (root == null){
return new int[]{0, 0};
}
int[] left = getMaximumCore(root.left);
int[] right = getMaximumCore(root.right);
int depth = Math.max(left[0], right[0]) + 1;
int len = Math.max(left[0]+right[0], Math.max(left[1], right[1]));
return new int[]{depth, len};
}
}
问题描述
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
- 首先找到需要删除的节点;
- 如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。
问题分析
核心的思路就是:将要删除的结点的右子树的最小结点替换为当前结点,实现代码如下:
class Main {
public static TreeNode deleteNode(TreeNode root, int key) {
if (root == null){
return null;
}
if (root.val > key){
root.left = deleteNode(root.left, key);
} else if (root.val < key){
root.right = deleteNode(root.right, key);
} else {
if (root.right == null) return root.left;
if (root.left == null) return root.right;
TreeNode temp = root;
root = min(temp.right);
root.right = deleteMin(temp.right);
root.left = temp.left;
}
return root;
}
/**
* 寻找二叉树的最小结点
*/
private static TreeNode min(TreeNode root){
if (root == null){
return null;
}
while (root.left != null){
root = root.left;
}
return root;
}
/**
* 删除二叉树的最小结点
*/
private static TreeNode deleteMin(TreeNode root){
if (root == null){
return null;
}
if (root.left == null){
return root.right;
}
root.left = deleteMin(root.left);
return root;
}
}
问题描述
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
问题分析
实现代码如下所示:
class Main {
public static int maxProfit(int[] prices) {
if (prices == null || prices.length < 2){
return 0;
}
int result = 0;
for (int i = 1; i < prices.length; i++){
if (prices[i] > prices[i-1]){
result += prices[i] - prices[i-1];
}
}
return result;
}
}
时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1)。
问题描述
对链表进行插入排序
问题分析
我们可以定义三个指针 prev、curr 和 last,分别指向要插入的位置、已排好序的链表结点下标以及下一个将要进行插入操作的结点,实现代码如下所示:
public class Main {
public static ListNode insertionSortList(ListNode head){
if (head == null || head.next == null){
return head;
}
ListNode h = new ListNode(-1);
h.next = head;
ListNode prev = h, curr = head, last = head;
while (curr != null){
last = curr.next;
if (last != null && last.val < curr.val){
// 寻找插入位置
while (prev.next != null && prev.next.val < last.val){
prev = prev.next;
}
// 插入操作
ListNode temp = last.next;
last.next = prev.next;
prev.next = last;
curr.next = temp;
prev = h;
} else {
// 无需插入则进行下一个结点的扫描
curr = curr.next;
}
}
return h.next;
}
}
问题描述
对单向链表进行快速排序
问题分析
对链表进行快速排序关键在于切分函数的处理,以往我们是通过双指针分别从前后往中间遍历的形式对数组进行处理的,而单向链表明显是无法从后往前遍历的。我们可以这么做,定义两个指针 p p p 和 q q q, q q q 位于 p p p 的后面,我们让 p p p 之前的结点全是小于标定值的结点,而 p p p 到 q q q 之间的结点全为大于等于标定值的结点,然后让 q q q 往后遍历,如果 q q q 的值小于标定值就和结点 p p p 进行呼唤,直到 q q q 到达最后一个结点,代码如下所示:
public class Main {
public static void quickSortList(ListNode head){
quickSortList(head, null);
}
private static void quickSortList(ListNode head, ListNode tail){
if (head == null || head == tail){
return;
}
ListNode pivot = partition(head, tail);
quickSortList(head, pivot);
quickSortList(pivot.next, tail);
}
private static ListNode partition(ListNode head, ListNode tail){
ListNode p = head;
ListNode q = head.next;
while (q != tail){
if (q.val < head.val){
p = p.next;
swap(p, q);
}
q = q.next;
}
swap(head, p);
return p;
}
private static void swap(ListNode p, ListNode q){
int temp = p.val;
p.val = q.val;
q.val = temp;
}
}
问题描述
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例 1:
输入:0 0 0
0 1 0
0 0 0
输出:0 0 0
0 1 0
0 0 0
示例 2:
输入:0 0 0
0 1 0
1 1 1
输出:0 0 0
0 1 0
1 2 1
注意:
- 给定矩阵的元素个数不超过 10000。
- 给定矩阵中至少有一个元素是 0。
- 矩阵中的元素只在四个方向上相邻: 上、下、左、右。
问题分析
这道题可以采用深度优先搜索以及动态规划两种方式来做,如下所示:
/**
* 广度优先搜索解法
*/
public class Main {
static class Pair {
int x;
int y;
Pair(int x, int y){
this.x = x;
this.y = y;
}
}
private static final int[][] dirs = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
public static int[][] updateMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
throw new IllegalArgumentException();
}
int n = matrix.length;
int m = matrix[0].length;
int[][] result = new int[n][m];
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
result[i][j] = Integer.MAX_VALUE;
}
}
LinkedList<Pair> queue = new LinkedList<>();
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
if (matrix[i][j] == 0){
result[i][j] = 0;
queue.add(new Pair(i, j));
}
}
}
while (!queue.isEmpty()){
Pair pair = queue.removeFirst();
for (int[] d : dirs){
int x = pair.x + d[0];
int y = pair.y + d[1];
if (x >= 0 && x < n && y >= 0 && y < m){
if (result[x][y] > result[pair.x][pair.y]+1){
// 能进入这个if说明(x, y)此前未被添加过
result[x][y] = result[pair.x][pair.y]+1;
queue.add(new Pair(x, y));
}
}
}
}
return result;
}
}
这种解法的时间复杂度为 O ( n m ) O(nm) O(nm),因为每个点的坐标至多会被放进队列一次;空间复杂度为 O ( n m ) O(nm) O(nm),因为用了一个队列来存储坐标值。
动态规划的实现如下:
public class Main {
public static int[][] updateMatrix(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0){
throw new IllegalArgumentException();
}
int n = matrix.length;
int m = matrix[0].length;
int[][] result = new int[n][m];
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
result[i][j] = Integer.MAX_VALUE-1;
}
}
// 从左上到右下遍历一次
for (int i = 0; i < n; i++){
for (int j = 0; j < m; j++){
if (matrix[i][j] == 0){
result[i][j] = 0;
} else {
if (i > 0){
result[i][j] = Math.min(result[i][j], result[i-1][j]+1);
}
if (j > 0){
result[i][j] = Math.min(result[i][j], result[i][j-1]+1);
}
}
}
}
// 从右下到左上遍历一次
for (int i = n-1; i >= 0; i--){
for (int j = m-1; j >= 0; j--){
if (matrix[i][j] == 0){
result[i][j] = 0;
} else {
if (i < n-1){
result[i][j] = Math.min(result[i][j], result[i+1][j]+1);
}
if (j < m-1){
result[i][j] = Math.min(result[i][j], result[i][j+1]+1);
}
}
}
}
return result;
}
}
这种解法的时间复杂度为 O ( n m ) O(nm) O(nm),空间复杂度为 O ( n m ) O(nm) O(nm)。
问题描述
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:1 <—
/ \
2 3 <—
\ \
5 4 <—
问题分析
这道题的做法有两种:广度优先搜索和深度优先搜索,广度优先搜索的思路是按层遍历树,用哈希表保存每一层的右视图,只要我们遵循先左后右的遍历规则,那么先前同一层保存的值就会被后面的覆盖,所以我们最后得到的就是每一层最右结点的值,代码如下所示:
class Main {
public static List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> map = new HashMap<>();
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<Integer> depthQueue = new LinkedList<>();
int maxDepth = -1;
nodeQueue.add(root);
depthQueue.add(0);
while (!nodeQueue.isEmpty()){
TreeNode node = nodeQueue.remove();
int depth = nodeQueue.remove();
if (node != null){
maxDepth = Math.max(maxDepth, depth);
map.put(depth, node.val);
nodeQueue.add(node.left);
nodeQueue.add(node.right);
depthQueue.add(depth+1);
depthQueue.add(depth+1);
}
}
List<Integer> result = new ArrayList<>();
for (int i = 0; i <= maxDepth; i++){
result.add(map.get(i));
}
return result;
}
}
深度优先搜索则是按照深度优先的法则,我们先不断地寻找右结点,不存在时我们才回退,代码如下所示:
class Main {
public static List<Integer> rightSideView(TreeNode root) {
Map<Integer, Integer> map = new HashMap<>();
Stack<TreeNode> nodeStack = new Stack<>();
Stack<Integer> depthStack = new Stack<>();
nodeStack.push(root);
depthStack.push(0);
int maxDepth = -1;
while (!nodeStack.isEmpty()){
TreeNode node = nodeStack.pop();
int depth = depthStack.pop();
if (node != null){
if (!map.containsKey(depth)){
map.put(depth, node.val);
maxDepth = Math.max(maxDepth, depth);
}
nodeStack.push(node.left);
nodeStack.push(node.right);
depthStack.push(depth+1);
depthStack.push(depth+1);
}
}
List<Integer> result = new ArrayList<>();
for (int i = 0; i <= maxDepth; i++){
result.add(map.get(i));
}
return result;
}
}
问题描述
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]
问题分析
这道题的做法有两种,分别是回溯法和位运算,代码分别如下所示:
class Main {
public static List<List<Integer>> subsets(int[] nums) {
if (nums == null){
throw new NullPointerException("nums == null");
}
List<List<Integer>> result = new ArrayList<>();
backTrace(nums, 0, result, new ArrayList<>());
return result;
}
private static void backTrace(int[] nums, int i, List<List<Integer>> result, List<Integer> subset){
result.add(new ArrayList<>(subset));
for (int k = i; k < nums.length; k++){
subset.add(nums[k]);
backTrace(nums, k+1, result, subset);
subset.remove(subset.size()-1);
}
}
}
除此之外我们还可以用位运算的思想来解这道题,不过位运算有个位运算的缺点是对数组长度有所限制,但这不失为一个比较巧的方法,实现代码如下:
class Main {
public static List<List<Integer>> subsets(int[] nums) {
if (nums == null){
throw new NullPointerException("nums == null");
}
List<List<Integer>> result = new ArrayList<>();
int n = nums.length;
int d = 1 << n;
for (int i = 0; i < d; i++){
List<Integer> temp = new ArrayList<>();
for (int j = 0; j < n; j++){
if (((1 << j) & i) != 0){
temp.add(nums[j]);
}
}
result.add(temp);
}
return result;
}
}
问题描述
输入一个整型数组,数组里有正数也有负数。数组中的一个或多个整数组成一个子数组。求所有子数组的和的最大值。要求时间复杂度为 O ( n ) O(n) O(n)。
问题分析
这道题有两种解法,常规解法以及动态规划,分别如下所示:
public class Main {
public static int findMaxSum(int[] nums){
if (nums == null || nums.length == 0){
throw new IllegalArgumentException();
}
int maxSum = Integer.MIN_VALUE;
int currSum = 0;
for (int i = 0; i < nums.length; i++){
if (currSum <= 0){
currSum = nums[i];
} else {
currSum += nums[i];
}
maxSum = Math.max(currSum, maxSum);
}
return maxSum;
}
}
动态规划的解法如下所示:
public class Main {
public static int findMaxSum(int[] nums){
if (nums == null || nums.length == 0){
throw new IllegalArgumentException();
}
int n = nums.length;
int max = Integer.MIN_VALUE;
int[] dp = new int[n+1];
for (int i = 1; i <= n; i++){
if (dp[i-1] <= 0){
dp[i] = nums[i-1];
} else {
dp[i] = dp[i-1] + nums[i-1];
}
max = Math.max(max, dp[i]);
}
return max;
}
}