目录
207. 课程表
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,判断是否可能完成所有课程的学习?
示例 1:
输入: 2, [[1,0]]
输出: true
解释: 总共有 2 门课程。学习课程 1 之前,你需要完成课程 0。所以这是可能的。
示例 2:
输入: 2, [[1,0],[0,1]]
输出: false
解释: 总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0;并且学习课程 0 之前,你还应先完成课程 1。这是不可能的。
//最佳方法:dfs(beats99%)
public class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
ArrayList<Integer>[] graph = new ArrayList[numCourses]; //用list保存每个课程的依赖课程
for(int i = 0; i < numCourses; i++) {
graph[i] = new ArrayList<Integer>();
}
for(int[] pre : prerequisites) { //根据对称性可知,这个地方其实pre[1]和pre[0]交换也无所谓
//graph[pre[1]].add(pre[0]); //这样写结果也相同
graph[pre[0]].add(pre[1]);
}
boolean[] visited = new boolean[numCourses]; //尝试过的课程
boolean[] finished = new boolean[numCourses]; //能完成的课程
for(int i = 0; i < numCourses; i++) { //逐个课程验证能否完成
if(!canFinish(graph, visited, finished, i)){
return false;
}
}
return true;
}
public boolean canFinish(ArrayList[] graph, boolean[] visited, boolean[] finished, int s) {
if(finished[s]) {
return true;
}
else if(visited[s]) { //这个地方说明递归出现环路,因此无法完成返回false
return false;
}
else {
visited[s] = true; //标记已尝试过,避免递归成环卡死
}
for(int i = 0; i < graph[s].size(); i++){
int adj = (int)graph[s].get(i); //逐个验证s的依赖课程adj能否完成
if(!canFinish(graph, visited, finished, adj)){
return false;
}
}
finished[s] = true; //标记s可以完成
return true;
}
}
208.实现Trie(前缀树)
实现一个 Trie (前缀树),包含 insert, search, 和 startsWith 这三个操作。
示例:
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
说明:
你可以假设所有的输入都是由小写字母 a-z 构成的。
保证所有输入均为非空字符串。
class Trie {
//用一个HashSet就OK
private HashSet<String> set;
/** Initialize your data structure here. */
public Trie() {
this.set = new HashSet<String>();
}
/** Inserts a word into the trie. */
public void insert(String word) {
this.set.add(word);
}
/** Returns if the word is in the trie. */
public boolean search(String word) {
return set.contains(word);
}
/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
for(String w : set) {
if(w.startsWith(prefix))
return true;
}
return false;
}
}
215.数组中的第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
说明:
你可以假设 k 总是有效的,且 1 ≤ k ≤ 数组的长度。
class Solution {
public int findKthLargest(int[] nums, int k) {
//shuffle(nums);
return partition(nums, nums.length - k, 0, nums.length-1);
}
public int partition(int[] nums, int k, int s, int e) {
int i = s;
int j = e;
//测试数据显示 mid-of-3nums 比 shuffle 更快, 1ms vs 7ms
int mid = s + (e - s) / 2;
int to = choosePivotIndex(nums, s, mid, e);
swap(nums, s, to);
int flag = nums[s];
while(i < j) {
while(i < j && nums[j] >= flag) {
j--;
}
nums[i] = nums[j];
while(i < j && nums[i] < flag) {
i++;
}
nums[j] = nums[i];
}
if(i == k) {
return flag;
}
else if(i > k) {
return partition(nums, k, s,i - 1);
}
else {
return partition(nums, k,i + 1, e);
}
}
public int choosePivotIndex(int[] nums, int a, int b, int c){
if(nums[a] > nums[b]){
if(nums[c] > nums[a]){
return a;
}
else if(nums[c] > nums[b]){
return c;
}
else{
return b;
}
}
else{
if(nums[c] > nums[b]){
return b;
}
else if(nums[c] > nums[a]){
return c;
}
else{
return a;
}
}
}
/*
public void shuffle(int[] nums) {
Random rnd = new Random();
for(int i = nums.length - 1; i > 0; i--) {
int j = rnd.nextInt(i + 1);
swap(nums, i, j);
}
}
*/
public void swap(int[] N, int i, int j) {
int tmp = N[i];
N[i] = N[j];
N[j] = tmp;
}
}
221.最大正方形
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
示例:
输入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
输出: 4
class Solution {
public int maximalSquare(char[][] matrix) {
if(matrix == null || matrix.length == 0 || matrix[0].length == 0)
return 0;
int m = matrix.length;
int n = matrix[0].length;
int[][] dp = new int[m + 1][n + 1];
int max = 0;
for(int i = 1; i <= m; i++) {
for(int j = 1; j <= n; j++) {
//等于'1'时,从matrix[i - 1][j - 1]向左上扩散构成正方形
if(matrix[i - 1][j - 1] == '1') {
//关键点就是三个中的最小值 + 1
//因为既然另外两个边长都>=它,就一定能组成新的正方形的两条边
dp[i][j] = 1 + Math.min(dp[i - 1][j], Math.min(dp[i][j - 1], dp[i - 1][j - 1]));
max = Math.max(max, dp[i][j]);
}
}
}
return max * max;
}
}
236.二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//对于子树递归时,返回非空则说明返回值为p或者q,返回空说明两个都不含
if(root == null || root == p || root == q)
return root;
//先对左右子树递归到底,求返回值
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
//都空,说明p和q都不含
if(left == null && right == null)
return null;
//都不空,说明分别在两个子树中,则返回root
if(left != null && right != null)
return root;
//只有一个为空,则另一个即所求
return left == null ? right : left;
}
}
238.除自身以外数组的乘积
给定长度为 n 的整数数组 nums,其中 n > 1,返回输出数组 output ,其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
示例:
输入: [1,2,3,4]
输出: [24,12,8,6]
说明: 请不要使用除法,且在 O(n) 时间复杂度内完成此题。
进阶:
你可以在常数空间复杂度内完成这个题目吗?( 出于对空间复杂度分析的目的,输出数组不被视为额外空间。)
class Solution {
//剑指offer做过
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] res = new int[n];
res[0] = 1;
//求左侧乘积
for(int i = 1; i < n; i++) {
res[i] = res[i - 1] * nums[i - 1];
}
//求右侧乘积
int rightProduct = 1;
for(int i = n - 1; i >= 0; i--) {
res[i] *= rightProduct;
rightProduct *= nums[i];
}
return res;
}
}
240.搜索二维矩阵II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
示例:
现有矩阵 matrix 如下:
[
[1, 4, 7, 11, 15],
[2, 5, 8, 12, 19],
[3, 6, 9, 16, 22],
[10, 13, 14, 17, 24],
[18, 21, 23, 26, 30]
]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
if(matrix.length == 0 || matrix[0].length == 0) {
return false;
}
//从右上角向左下角寻找,剑指Offer做过
int col = matrix[0].length-1;
int row = 0;
while(col >= 0 && row <= matrix.length - 1) {
if(target == matrix[row][col]) {
return true;
}
else if(target < matrix[row][col]) {
col--;
}
else {
row++;
}
}
return false;
}
}
279.完全平方数
给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...
)使得它们的和等于n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
class Solution {
public int numSquares(int n) {
//动态规划
int[] dp = new int[n + 1];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 1; i <= n; i++) {
for(int j = 1; i - j * j >= 0; j++) {
//递推公式 1+dp[i-j*j]
dp[i] = Math.min(dp[i], 1 + dp[i - j * j]);
}
}
return dp[n];
}
}
287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums,其数字都在 1 到 n 之间(包括 1 和 n),可知至少存在一个重复的整数。假设只有一个重复的整数,找出这个重复的数。
示例 1:
输入: [1,3,4,2,2]
输出: 2
示例 2:
输入: [3,1,3,4,2]
输出: 3
说明:
不能更改原数组(假设数组是只读的)。
只能使用额外的 O(1) 的空间。
时间复杂度小于 O(n2) 。
数组中只有一个重复的数字,但它可能不止重复出现一次。
class Solution {
public int findDuplicate(int[] nums) {
//和链表中找环一个思路,把i视为listNode,nums[i - 1]视为listNode.next
int n = nums.length;
int slow = n, fast = n;
//通过slow=nums[slow-1]的方式跳跃,保证不会卡死在一个位置上
//因此只有存在环(重复值)才会slow==fast
do {
slow = nums[slow - 1];
fast = nums[nums[fast - 1] - 1];
} while(slow != fast);
//链表找环最后一步
slow = n;
while(slow != fast) {
slow = nums[slow - 1];
fast = nums[fast - 1];
}
return slow;
}
}
300.最长上升子序列
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
class Solution {
public int lengthOfLIS(int[] nums) {
//tailNum[i]代表i + 1长度的子序列的结尾num, 容易理解是一个递增序列
int[] tailNum = new int[nums.length];
int max = 0;
for(int num : nums) {
int start = 0, end = max; //tailNum数组中是 0到max-1 范围有数据
//二分查找num可以替换的tailNum[i]并且替换
//如果tailNum[i-1] < num <= tailNum[i], 更新tailNum[i]
//或者num比所有tailNum[i]都大,则插入到max处(原来无数据)
//这里不太好理解,可以理解为每个前面元素的更新都会改变更新处向后的地方的更新条件
//但只要最后一个元素没有被更新变小,新增元素的条件就不会变,因此不影响长度更新
//但可能随着前面的更新,最终导致最后一个元素变小,即改变了新增元素的条件
//所以对每个元素都要尝试二分查找,替换前面的值
//这样做tailNum中可能不是LIS,但长度始终不会错
while(start != end) {
int mid = (start + end) / 2;
if(tailNum[mid] < num)
start = mid + 1;
else
end = mid;
}
tailNum[start] = num;
//等于max说明start一直在右移end没动, 也就是num比所有tailNum[i]都大
if(start == max)
++max;
}
return max;
}
}
314. 二叉树垂直遍历
public class Solution {
public List<List<Integer>> verticalOrder(TreeNode root) {
List<List<Integer>> results = new ArrayList<>();
if (root == null) return results;
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
Map<Integer, List<Integer>> map = new HashMap<>();
LinkedList<Position> queue = new LinkedList<>();
queue.add(new Position(root, 0));
while (!queue.isEmpty()) {
Position position = queue.remove();
min = Math.min(min, position.column);
max = Math.max(max, position.column);
List<Integer> list = map.get(position.column);
if (list == null) {
list = new ArrayList<>();
map.put(position.column, list);
}
list.add(position.node.val);
if (position.node.left != null) queue.add(new Position(position.node.left, position.column-1));
if (position.node.right != null) queue.add(new Position(position.node.right, position.column+1));
}
for(int i=min; i<=max; i++) {
List<Integer> list = map.get(i);
if (list != null) results.add(list);
}
return results;
}
}
class Position {
TreeNode node;
int column;
Position(TreeNode node, int column) {
this.node = node;
this.column = column;
}
}
319. 灯泡开关
初始时有 n 个灯泡关闭。 第 1 轮,你打开所有的灯泡。 第 2 轮,每两个灯泡你关闭一次。 第 3 轮,每三个灯泡切换一次开关(如果关闭则开启,如果开启则关闭)。第 i 轮,每 i 个灯泡切换一次开关。 对于第 n 轮,你只切换最后一个灯泡的开关。 找出 n 轮后有多少个亮着的灯泡。
示例:
输入: 3
输出: 1
解释:
初始时, 灯泡状态 [关闭, 关闭, 关闭].
第一轮后, 灯泡状态 [开启, 开启, 开启].
第二轮后, 灯泡状态 [开启, 关闭, 开启].
第三轮后, 灯泡状态 [开启, 关闭, 关闭].
你应该返回 1,因为只有一个灯泡还亮着。
class Solution {
public int bulbSwitch(int n) {
//只有平方数被开关奇数次,其他都被开关偶数次
int count = 0;
for(int a = 1; a * a <= n; a++) {
count++;
}
return count;
}
}
343. 整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
class Solution {
public int integerBreak(int n) {
//4要拆成2+2,5要拆成2+3,6要拆成3+3
//很容易看出最大的因数是3,尽量拆出3
if(n == 2) {
return 1;
}
if(n == 3) {
return 2;
}
int max = 1;
while(n - 3 > 1){
n -= 3;
max *= 3;
}
max *= n;
return max;
}
}
328. 奇偶链表
给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。
请尝试使用原地算法完成。你的算法的空间复杂度应为 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 Solution {
public ListNode oddEvenList(ListNode head) {
if( head == null || head.next == null){
return head;
}
ListNode oddTail = head;
ListNode evenTail = head.next;
//保存偶数节点的头
ListNode evenHead = head.next;
while(oddTail != null && evenTail != null && evenTail.next != null){
oddTail.next = evenTail.next;
oddTail = oddTail.next;
evenTail.next = oddTail.next;
evenTail = evenTail.next;
}
oddTail.next = evenHead;
return head;
}
}
390. 消除游戏
给定一个从1 到 n 排序的整数列表。
首先,从左到右,从第一个数字开始,每隔一个数字进行删除,直到列表的末尾。
第二步,在剩下的数字中,从右到左,从倒数第一个数字开始,每隔一个数字进行删除,直到列表开头。
我们不断重复这两步,从左到右和从右到左交替进行,直到只剩下一个数字。
返回长度为 n 的列表中,最后剩下的数字。
示例:
输入:
n = 9,
1 2 3 4 5 6 7 8 9
2 4 6 8
2 6
6
输出:
6
class Solution {
public int lastRemaining(int n) {
//答案是正向开始和逆向开始的对称关系,两个结果中心对称,和为1+n
//进而可以推导出f(2*n)和f(n)的递推公式
return n == 1 ? 1 : 2 * (n / 2 + 1 - lastRemaining(n / 2));
}
}