Java解leetcode,助力面试之中等10道题(四)
第153题 寻找旋转排序数组中的最小值
已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到:
若旋转 4 次,则可以得到 [4,5,6,7,0,1,2]
若旋转 7 次,则可以得到 [0,1,2,4,5,6,7]
注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。
给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。
示例 1:
输入 | 输出 |
---|---|
nums = [3,4,5,1,2] | 1 |
原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。
示例 2:
输入 | 输出 |
---|---|
nums = [4,5,6,7,0,1,2] | 0 |
解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。
示例 3:
输入 | 输出 |
---|---|
nums = [11,13,15,17] | 11 |
解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。
解题思路
用二分查找来查询,因为数组时旋转有序的,所以判定中间值与右边值谁大谁小就行,如果右边值大,说明最小值在中间数及中间数的左边,否则最小值在中间数的右边,然后再重新使用二分法查找。
代码
// 寻找旋转排序数组中的最小值:二分查找
class Solution {
public int findMin(int[] nums) {
int low = 0;
int high = nums.length - 1;
while (low < high) {
int pivot = low + (high - low) / 2;
if (nums[pivot] < nums[high]) {//如果中间值比最右边的值小,则将high设为pivot
high = pivot;
} else {
low = pivot + 1;//否则最小值一定在中间数的右边
}
}
return nums[low];
}
}
时间复杂度为O(log n),n数组长度
空间复杂度为O(1)
第162题 寻找峰值
峰值元素是指其值大于左右相邻值的元素。
给你一个输入数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。
你可以假设 nums[-1] = nums[n] = -∞ 。
示例 1:
输入 | 输出 |
---|---|
nums = [1,2,3,1] | 2 |
解释:3 是峰值元素,你的函数应该返回其索引 2。
示例 2:
输入 | 输出 |
---|---|
nums = [1,2,1,3,5,6,4] | 1 或 5 |
解释:你的函数可以返回索引 1,其峰值元素为 2;
或者返回索引 5, 其峰值元素为 6。
解题思路
用二分查找来做,l,r指向数组头尾,当l=r时,代表找到了峰值,因为如果中间数比右边数大,则将r=mid,这时候只要不断将l移到r处,也就是左边的数小于右边的数,就可以证明中间数为峰值
代码
// 寻找峰值:二分查找
public class Solution {
public int findPeakElement(int[] nums) {
int l = 0, r = nums.length - 1;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] > nums[mid + 1])//如果中间数大于它右边的数,则令r为中间数,否则令l为中间数加1
r = mid;
else
l = mid + 1;
}
return l;
}
}
时间复杂度为O(log n),n为数组长度
空间复杂度为O(1)
第198题 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入 | 输出 |
---|---|
[1,2,3,1] | 4 |
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入 | 输出 |
---|---|
[2,7,9,3,1] | 12 |
解题思路
使用动态规划,从第二间房开始,每间房的最大偷窃金额,取决去前两间房的最大偷窃金额,当前房的最大金额等于上一间房与上上间房加当前房金额的较大值,然后不断向后遍历找最终的金额。
代码
// 打家劫舍:动态规划
class Solution {
public int rob(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
if (length == 1) {
return nums[0];
}
int first = nums[0], second = Math.max(nums[0], nums[1]);//first为第一个数,second为第一个数与第二个数之间的较大值
for (int i = 2; i < length; i++) {//从第三间房开始
int temp = second;//令temp为前两间房中金额大的那个数
second = Math.max(first + nums[i], second);//选取较大的值,一个是偷了当前的房,一个是没偷当前的房
first = temp;//变更first为下一间房
}
return second;
}
}
时间复杂度为O(n),n为数组的长度
空间复杂度为O(1)
第200题 岛屿数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入 | 输出 |
---|---|
grid = [ [“1”,“1”,“1”,“1”,“0”], [“1”,“1”,“0”,“1”,“0”], [“1”,“1”,“0”,“0”,“0”], [“0”,“0”,“0”,“0”,“0”] | 1 |
示例 2:
输入 | 输出 |
---|---|
grid = [ [“1”,“1”,“1”,“1”,“0”], [“1”,“1”,“0”,“1”,“0”], [“1”,“1”,“0”,“0”,“0”], [“0”,“0”,“0”,“0”,“0”] | 3 |
解题思路
遍历数组,当发现1时,将岛屿数加1,然后用dfs搜索四周是否存在相连的1,都将它们标记为0,直到没有1,然后继续返回第一步还未遍历完的数组,执行同样的操作。
代码
// 岛屿数量:DFS
class Solution {
void dfs(char[][] grid, int r, int c) {
int nr = grid.length;
int nc = grid[0].length;
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}//dfs,如果超出给定的范围,或者便利到的为0,则跳过
grid[r][c] = '0';//将当前的1变为0,然后搜索四周,如果还发现1,则继续变为1,直到四周为0
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;//如果遍历到了1,则加上一个岛屿数
dfs(grid, r, c);//调用dfs搜索四周是否还有1
}
}
}
return num_islands;
}
}
时间复杂度为O(mn),m,n分别为矩阵的行和列
空间复杂度为O(mn)
第207题 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
示例 1:
输入 | 输出 |
---|---|
numCourses = 2, prerequisites = [[1,0]] | true |
解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
示例 2:
输入 | 输出 |
---|---|
numCourses = 2, prerequisites = [[1,0],[0,1]] | false |
解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
解题思路
判断是否存在环,运用dfs搜索前置,并将搜索过的数标记为1,未搜索的数标记为0,如果在搜索过程中遇见了1,则代表存在环,不是合理的课程表,否则如果搜索到当前数没有前置,代表不是环,再看看是否都为1,如果都为1,则代表不是环,是合理的课程表。
代码
// 课程表:DFS
class Solution {
List<List<Integer>> edges;
int[] visited;
boolean valid = true;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}//将课程加入数组中
visited = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}//前置条件
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {//当前课程未被遍历,调用dfs
dfs(i);
}
}
return valid;
}
public void dfs(int u) {
visited[u] = 1;
for (int v: edges.get(u)) {//获取当前课程的前置课程
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
} else if (visited[v] == 1) {//如果还能遍历到已出现过的课程,代表不能上完所有课程
valid = false;
return;
}
}
visited[u] = 2;
}
}
时间复杂度为O(m+n),m,n分别为先修课程的要求数以及课程数
空间复杂度为O(m+n)
第208题 实现 Trie (前缀树)
Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。
请你实现 Trie 类:
Trie() 初始化前缀树对象。
void insert(String word) 向前缀树中插入字符串 word 。
boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false 。
boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false 。
示例 1:
输入 | 输出 |
---|---|
[“Trie”, “insert”, “search”, “search”, “startsWith”, “insert”, “search”][[], [“apple”], [“apple”], [“app”], [“app”], [“app”], [“app”]] | [null, null, true, false, true, null, true] |
解释
Trie trie = new Trie();
trie.insert(“apple”);
trie.search(“apple”); // 返回 True
trie.search(“app”); // 返回 False
trie.startsWith(“app”); // 返回 True
trie.insert(“app”);
trie.search(“app”); // 返回 True
解题思路
建立前缀树,如果是插入操作,则将每个单词依次做为孩子节点插入,如果是搜索操作,则依次取出查看前缀树中是否存在,然后比对。
代码
// 实现 Trie (前缀树):字典树
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26];
isEnd = false;//判断是否为字符串的结尾
}
public void insert(String word) {//插入字符串
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();//如果为空树,则新建前缀树
}
node = node.children[index];//如果不是空树,则将当前索引依次插入前缀树,做为孩子节点
}
node.isEnd = true;//将结尾标记为true
}
public boolean search(String word) {//搜索zfc2
Trie node = searchPrefix(word);
return node != null && node.isEnd;//判断得到的孩子节点是否为空且是否为终止
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String prefix) {//搜索前缀
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {//依次看孩子节点是否为空,不为空,则将孩子索引传给node
return null;
}
node = node.children[index];
}
return node;
}
}
时间复杂度为O(∣S∣),∣S∣为插入或查询字符串的长度
空间复杂度为O(∣T∣⋅Σ)∣T∣为插入字符串的长度之和,Σ为字符集的大小
第209题 长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入 | 输出 |
---|---|
target = 7, nums = [2,3,1,2,4,3] | 2 |
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
示例 2:
输入 | 输出 |
---|---|
target = 4, nums = [1,4,4] | 1 |
示例 3:
输入 | 输出 |
---|---|
target = 11, nums = [1,1,1,1,1,1,1,1] | 0 |
解题思路
不断移动窗口判断最小数组,首先将当前数加入总和,判断是否大于等于目标数,如果大于,则变更最短子数组长度为原长度与目前长度的较小值,然后将总和减去起始位置数,并将起始位置start右移一位,继续判断,如果符合,则继续执行以上操作,不符合则将结束位置end右移一位,并将当前数加入总和,继续判断,直到遍历完数组。
代码
// 长度最小的子数组:滑动窗口
class Solution {
public int minSubArrayLen(int s, int[] nums) {
int n = nums.length;
if (n == 0) {
return 0;
}
int ans = Integer.MAX_VALUE;
int start = 0, end = 0;
int sum = 0;
while (end < n) {//遍历数组
sum += nums[end];//计算当上当前数的总和
while (sum >= s) {//只要总和超过目标数
ans = Math.min(ans, end - start + 1);//计算符合条件的最短子数组
sum -= nums[start];//减去起始值,并将起始位置右移一位
start++;
}
end++;
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
时间复杂度为O(n),n为数组的长度
空间复杂度为O(1)
第210题 课程表 II
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入 | 输出 |
---|---|
2, [[1,0]] | [0,1] |
解释:总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入 | 输出 |
---|---|
4, [[1,0],[2,0],[3,1],[3,2]] | [0,1,2,3] or [0,2,1,3] |
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
解题思路
将各节点状态标记为0,1,2,分别代表未搜索,搜索中,已搜索,如果在搜索中找到了本身,则代表有环,返回false,如果搜索到的数为0,则将该数放在当前数之前,然后将该数的状态设为1,直到找完所有节点
代码
// 课程表 II:DFS
class Solution {
// 存储有向图
List<List<Integer>> edges;
// 标记每个节点的状态:0=未搜索,1=搜索中,2=已完成
int[] visited;
// 用数组来模拟栈,下标 n-1 为栈底,0 为栈顶
int[] result;
// 判断有向图中是否有环
boolean valid = true;
// 栈下标
int index;
public int[] findOrder(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses];
result = new int[numCourses];
index = numCourses - 1;
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 每次挑选一个「未搜索」的节点,开始进行深度优先搜索
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
if (!valid) {
return new int[0];
}
// 如果没有环,那么就有拓扑排序
return result;
}
public void dfs(int u) {
// 将节点标记为「搜索中」
visited[u] = 1;
// 搜索其相邻节点
// 只要发现有环,立刻停止搜索
for (int v: edges.get(u)) {
// 如果「未搜索」那么搜索相邻节点
if (visited[v] == 0) {
dfs(v);
if (!valid) {
return;
}
}
// 如果「搜索中」说明找到了环
else if (visited[v] == 1) {
valid = false;
return;
}
}
// 将节点标记为「已完成」
visited[u] = 2;
// 将节点入栈
result[index--] = u;
}
}
时间复杂度为O(n+m),n为课程数,m为先修课程的要求数
空间复杂度为O(n+m)
第213题 打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入 | 输出 |
---|---|
nums = [2,3,2] | 3 |
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入 | 输出 |
---|---|
nums = [1,2,3,1] | 4 |
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入 | 输出 |
---|---|
nums = [0] | 0 |
解题思路
本题与198题类似,唯一不同就是首尾相连,因此还是使用动态规划来求解,不过在最后加入了一个比值,比较没有第一间房的最大值和没有最后一间房的最大值,取两者之间大的值
代码
// 打家劫舍 II:动态规划
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if (length == 1) {
return nums[0];
} else if (length == 2) {
return Math.max(nums[0], nums[1]);
}
return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));//判断没有开头那间房或者没有末尾那间房的最大金额,因为首尾相连。
}
public int robRange(int[] nums, int start, int end) {
int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i <= end; i++) {//一次跳两个,计算最大金额
int temp = second;
second = Math.max(first + nums[i], second);
first = temp;
}
return second;
}
}
时间复杂度为O(n),n为数组长度
空间复杂度为O(1)
第215题 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入 | 输出 |
---|---|
[3,2,1,5,6,4] 和 k = 2 | 5 |
示例 2:
输入 | 输出 |
---|---|
[3,2,3,1,2,4,5,5,6] 和 k = 4 | 4 |
解题思路
本题使用快速排序,快速排序主要是将原数组分区,分成两个子数组,让左边的子数组整体都小于右边的子数组,然后递归调用继续分区,找到q为倒数第k个下标即可。
代码
// 数组中的第K个最大元素:排序
class Solution {
Random random = new Random();
public int findKthLargest(int[] nums, int k) {
return quickSelect(nums, 0, nums.length - 1, nums.length - k);//快速排序
}
public int quickSelect(int[] a, int l, int r, int index) {//快排
int q = randomPartition(a, l, r);//将数组分区
if (q == index) {
return a[q];
} else {
return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
}
}
public int randomPartition(int[] a, int l, int r) {//随机分区
int i = random.nextInt(r - l + 1) + l;
swap(a, i, r);
return partition(a, l, r);
}
public int partition(int[] a, int l, int r) {//分区,使得左边的数整体小于右边的数
int x = a[r], i = l - 1;
for (int j = l; j < r; ++j) {
if (a[j] <= x) {
swap(a, ++i, j);
}
}
swap(a, i + 1, r);
return i + 1;
}
public void swap(int[] a, int i, int j) {//交换值
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
时间复杂度为O(n),n为数组长度
空间复杂度为O(log n)