顶部
算法:数组
题目
121. 买卖股票的最佳时机
class Solution {
public int maxProfit(int[] prices) {
if(prices.length == 0)
return 0;
int n =prices.length;
int min_prices = prices[0];
int max_profit = 0;
for(int i=0;i<n;i++){
max_profit = Math.max(max_profit,prices[i]-min_prices);
min_prices = Math.min(min_prices,prices[i]);
}
return max_profit;
}
}
169. 多数元素
class Solution {
public int majorityElement(int[] nums) {
int num = nums[0];
int count = 1;
for(int i = 1; i < nums.length; i++){
if(nums[i] == num){
count++;
}else if(nums[i] != num){
count--;
if(count == 0){
count = 1;
num = nums[i];
}
}
}
return num;
}
}
581. 最短无序连续子数组
class Solution {
public int findUnsortedSubarray(int[] nums) {
int len = nums.length - 1;
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
int start = 0;
int end = -1;
for(int i = 0; i < nums.length; i++){
//从左到右维持最大值,寻找右边界end
if(nums[i] < max){
end = i;
}else{
max = nums[i];
}
//从右到左维持最小值,寻找左边界start
if(nums[len-i] > min){
start = len - i;
}else{
min = nums[len-i];
}
}
return end - start + 1;
}
}
算法:链表
题目
2. 两数相加
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode cur = dummy;
int temp = 0;
while(l1 != null || l2 != null || temp!=0){
int x = l1 == null ? 0 : l1.val;
int y = l2 == null ? 0 : l2.val;
temp += x;
temp += y;
ListNode node = new ListNode(temp % 10);
cur.next = node;
cur = cur.next;
temp /= 10;
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
return dummy.next;
}
}
160. 相交链表
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 解法一:遍历一遍A,再判断A中是否有B(Set有contains方法)
Set<ListNode> set = new HashSet<>();
while(headA != null){
set.add(headA);
headA = headA.next;
}
while(headB != null){
if(set.contains(headB)){
return headB;
}
headB = headB.next;
}
return null;
}
}
206. 反转链表
掌握程度:★
思路:迭代
class Solution {
public ListNode reverseList(ListNode head) {
ListNode pre = null;
ListNode next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
234. 回文链表
class Solution {
public boolean isPalindrome(ListNode head) {
//边界判断
if(head == null || head.next == null)
return true;
ListNode slow = head;
ListNode fast = head;
//快慢指针,找到中点,快指针速度是慢指针两倍,快指针走完,慢指针走完一半
while(fast.next != null && fast.next.next != null){
slow = slow.next;
fast = fast.next.next;
}
//12321,后半段反转后变为12
ListNode right = reverseNode(slow.next);
while(right != null && head !=null ){
//要用&&不然会报空指针
if(right.val != head.val )
return false;
else{
right = right.next;
head = head.next;
}
}
return true;
}
public ListNode reverseNode(ListNode head){
//别忘记边界条件
if(head == null || head.next == null)
return head;
ListNode pre=null;
ListNode next;
while(head != null){
//这里改为next会错,因为我把上面的next设置为空了......,所以这里填head好
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
21. 合并两个有序链表
思路:递归/迭代
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 思路:递归
if(l1 == null) {
return l2;
}
if(l2 == null) {
return l1;
}
if(l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
19. 删除链表的倒数第 N 个结点
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 思路:快慢指针、栈
ListNode dummy = new ListNode(0, head);
ListNode fast = dummy.next;
ListNode slow = dummy;
for(int i = 0; i < n; i++){
fast = fast.next;
}
while(fast != null){
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next;
return dummy.next;
}
}
141. 环形链表
public class Solution {
public boolean hasCycle(ListNode head) {
//边界条件
if(head == null || head.next == null)
return false;
ListNode slow = head;
ListNode fast = head;
while(fast.next != null && fast.next.next != null){
//fast可能下一个不是空,下下个为空就会报错
slow = slow.next;
fast = fast.next.next;
if(slow == fast)
return true;
}
return false;
}
}
142. 环形链表 II
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // 若存在环,快指针一定能追上慢指针
slow = head;
while (slow != fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null; // 若没有环,返回 null
}
}
算法:二叉树
原理
思路:
1、遍历(traverse函数)还是分解?
2、前序(自上而下)还是后序(自下而上)?
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个
traverse函数配合外部变量来实现。2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做(二叉树前序位置是进入一个节点的时候,后序位置是离开一个节点的时候,理解这点很重要)。
题目
104. 二叉树的最大深度
class Solution {
public int maxDepth(TreeNode root) {
if(root == null)
return 0;
int L = maxDepth(root.left);
int R = maxDepth(root.right);
return Math.max(L, R) + 1;
}
}
543. 二叉树的直径
掌握程度:★★
思路:1、分解问题(根据子问题得到最后解)2、后序遍历(需要接收子树返回的信息)
class Solution {
int max = 0;
public int diameterOfBinaryTree(TreeNode root) {
int result = depth(root);
return max;
}
public int depth(TreeNode root){
if(root == null){
return 0;
}
int L = depth(root.left);
int R = depth(root.right);
max = Math.max(L + R, max);
return Math.max(L, R) + 1;
}
}
226. 翻转二叉树
掌握程度:★★
思路:1、分解问题;2、后序遍历
class Solution {
public TreeNode invertTree(TreeNode root) {
// 当前层已经拿到子层的翻转结果了,我只需要反转当前层就可以了
if(root == null)
return null;
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
}
617. 合并二叉树
掌握程度:★
思路:分解+前序
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
// 思路:分解+前序
if(root1 == null){
return root2;
}
if(root2 == null){
return root2;
}
TreeNode root = new TreeNode(root1.val + root2.val);
root.left = mergeTrees(root1.left, root2.left);
root.right = mergeTrees(root1.right, root2.right);
return root;
}
}
构造类题目:
二叉树的构造:使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。
654. 最大二叉树
掌握程度:★
思路:分解+前序;单调栈思路未实现
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
TreeNode root = build(nums, 0, nums.length-1);
return root;
}
TreeNode build(int[] nums, int L, int R){
if(L > R)
return null;
int max = -1;
int index = 0;
for(int i = L; i <= R; i++){
if(max < nums[i]){
max = nums[i];
index = i;
}
}
TreeNode node = new TreeNode(max);
node.left = build(nums, L, index-1);
node.right = build(nums, index+1, R);
return node;
}
}
101. 对称二叉树
class Solution {
//思路一:先中序遍历再判断回文
//思路二:递归 左树左子结点 == 右树右子节点 && 右树左子节点 == 左树右子节点
public boolean isSymmetric(TreeNode root) {
if(root == null)
return true;
return isSymmetric(root.left,root.right);
}
public boolean isSymmetric(TreeNode LNode,TreeNode RNode){
if(LNode == null && RNode == null)
//终止条件:左右子树都为空
return true;
if(LNode != null && RNode != null && LNode.val == RNode.val)
return isSymmetric(LNode.left,RNode.right) && isSymmetric(LNode.right,RNode.left);
return false;
}
}
94. 二叉树的中序遍历
class Solution {
List<Integer> list = new LinkedList();
public List<Integer> inorderTraversal(TreeNode root) {
if(root != null){
inorderTraversal(root.left);
list.add(root.val);
inorderTraversal(root.right);
}
return list;
}
}
105. 从前序与中序遍历序列构造二叉树
掌握程度:★
思路:分解+前序,脑子里有两个数组,注意细节
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
TreeNode root = build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
return root;
}
TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){
// 注意:结束条件
if(preStart > preEnd)
return null;
int nodeVal = preorder[preStart];
int index = map.get(nodeVal);
// 注意:index - inStart 不是index - preStart
int leftSize = index - inStart;
TreeNode node = new TreeNode(nodeVal);
node.left = build(preorder, preStart + 1, preStart + leftSize, inorder, inStart, index - 1);
node.right = build(preorder, preStart + leftSize + 1, preEnd, inorder, index + 1, inEnd);
return node;
}
}
106. 从中序与后序遍历序列构造二叉树
掌握程度:★★
思路:和上一题类似
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
TreeNode root = build(postorder, 0, postorder.length-1, inorder, 0, inorder.length-1);
return root;
}
TreeNode build(int[] postorder, int postStart, int postEnd, int[] inorder, int inStart, int inEnd){
// 注意:结束条件
if(postStart > postEnd || inStart > inEnd)
return null;
int nodeVal = postorder[postEnd];
int index = map.get(nodeVal);
// 注意:index - inStart
int leftSize = index - inStart;
TreeNode node = new TreeNode(nodeVal);
node.left = build(postorder, postStart, postStart + leftSize - 1, inorder, inStart, index-1);
node.right = build(postorder, postStart + leftSize, postEnd - 1, inorder, index+1, inEnd);
return node;
}
}
889. 根据前序和后序遍历构造二叉树
暂时不做
掌握程度:
思路:
1、首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。
2、然后把前序遍历结果的第二个元素作为左子树的根节点的值。
3、在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可
算法:字符串
题目
14. 最长公共前缀
class Solution {
public String longestCommonPrefix(String[] strs) {
String res = strs[0];
for(int i = 1; i < strs.length; i++){
int j = 0;
for(; j < res.length() && j < strs[i].length(); j++){
if(res.charAt(j) != strs[i].charAt(j)){
break;
}
}
res = res.substring(0, j);
// 有一个不符合就直接退出
if(res.equals(""))
return "";
}
return res;
}
}
165. 比较版本号
思路:分割字符串,逐个元素比较
class Solution {
public int compareVersion(String version1, String version2) {
String[] str1 = version1.split("\\.");
String[] str2 = version2.split("\\.");
for(int i = 0; i < str1.length || i < str2.length; i++){
int x = 0;
int y = 0;
if(i < str1.length)
x = Integer.parseInt(str1[i]);
if(i < str2.length)
y = Integer.parseInt(str2[i]);
if(x < y)
return -1;
else if (x > y)
return 1;
}
return 0;
}
}
179. 最大数
思路:整型数组转化为字符串数组,对字符串数组排序,(b + a).compareTo(a + b))降序排序,和s2-s1类似。注意元素都为0时直接返回0,不拼接。
class Solution {
public String largestNumber(int[] nums) {
int n = nums.length;
String[] toStr = new String[n];
for (int i = 0; i < n; i++){
toStr[i] = String.valueOf(nums[i]);
}
Arrays.sort(toStr, (a, b) -> (b + a).compareTo(a + b));
if(toStr[0].equals("0"))
return "0";
StringBuilder res = new StringBuilder();
for(String s : toStr){
res.append(s);
}
return res.toString();
}
}
168. Excel表列名称
十进制转化为其他进制
class Solution {
public String convertToTitle(int cn) {
StringBuilder sb = new StringBuilder();
while (cn > 0) {
cn--; // 解决从'B'开始算的问题
sb.append((char)(cn % 26 + 'A')); // 一直取余,从右往左算,所以sb要reverse
cn /= 26;
}
sb.reverse();
return sb.toString();
}
}
171. Excel 表列序号
其他进制转换为十进制
class Solution {
public int titleToNumber(String columnTitle) {
int ans = 0;
int num = 0;
for(int i = 0; i < columnTitle.length(); i++){
num = columnTitle.charAt(i) - 'A' + 1;
ans = ans * 26 + num;
}
return ans;
}
}
算法:哈希
题目
1. 两数之和
class Solution {
public int[] twoSum(int[] nums, int target) {
// 思路:哈希存target-nums[i]的值
HashMap<Integer,Integer> map= new HashMap<>();
for(int i = 0; i < nums.length; i++){
if(map.containsKey(nums[i])){
return new int[]{map.get(nums[i]),i};
}
map.put(target-nums[i],i);
}
return null;
}
}
12. 整数转罗马数字
class Solution {
public String intToRoman(int num) {
int[] values = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
String[] symbols = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
StringBuffer str = new StringBuffer();
for(int i = 0; i < values.length; i++){
int value = values[i];
String symbol = symbols[i];
while(num >= value){
num -= value;
str.append(symbol);
}
}
return str.toString();
}
}
13. 罗马数字转整数
class Solution {
public int romanToInt(String s) {
//类型是Character
Map<Character,Integer> map = new HashMap<>();
//细节:要用单引号
map.put('I',1);
map.put('V',5);
map.put('X',10);
map.put('L',50);
map.put('C',100);
map.put('D',500);
map.put('M',1000);
int res = 0;
int n = s.length();
// 思路:右边比左边大就减
for(int i = 0; i < n; i++){
int value = map.get(s.charAt(i));
// 最后一个不用判断,直接加
if(i < n - 1 && value < map.get(s.charAt(i + 1))){
res -= value;
}else{
res += value;
}
}
return res;
}
}
算法:排序
原理
排序
题目
15. 三数之和
思路:排序+双指针逼近+去重
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> res = new ArrayList<>();
if(nums.length == 0)
return res;
Arrays.sort(nums);
for(int i=0;i<nums.length;i++){
// i去重
if(i>0 &&nums[i] == nums[i-1])
continue;
int target = -nums[i];
int j = i + 1;
int k = nums.length - 1;
while(j < k){
// j k 去重
if(nums[j] + nums[k] == target){
List<Integer> list = new ArrayList<>();
list.add(nums[i]);
list.add(nums[j]);
list.add(nums[k]);
res.add(list);
j++;
k--;
while(j<k && nums[j] == nums[j-1])
j++;
while(k>j && nums[k] == nums[k+1])
k--;
}
else if(nums[j] + nums[k] > target)
k--;
else
j++;
}
}
return res;
}
}
16. 最接近的三数之和
思路:排序+双指针逼近
class Solution {
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
// 最小的了
int count = nums[0] + nums[1] + nums[2];
for(int i = 0; i < nums.length; i++){
int j = i+1;
int k = nums.length-1;
while(j < k){
int sum = nums[i] + nums[j] + nums[k];
// 看离target的距离多远
if(Math.abs(sum-target) < Math.abs(count-target))
count = sum;
else if(sum < target)
j++;
else if(sum > target)
k--;
else
// count最小,比count大直接返回count
return count;
}
}
return count;
}
}
49. 字母异位词分组
思路:字符排序
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for(String str : strs){
char[] array = str.toCharArray();
// 排序
Arrays.sort(array);
String s = new String(array);
List<String> list = map.getOrDefault(s, new ArrayList<>());
list.add(str);
map.put(s, list);
}
return new ArrayList<List<String>>(map.values());
}
}
56. 合并区间
思路:区间左端点排序,判断是否相交
class Solution {
public int[][] merge(int[][] intervals) {
List<int[]> res = new ArrayList<int[]>();
if (intervals.length == 0 ) return new int[0][0];
// 排序区间
Arrays.sort(intervals,(x,y) -> x[0] - y[0]);
int l = intervals[0][0], r = intervals[0][1];
for(int i = 1; i < intervals.length; i ++)
{
// 不重叠,则把前一个添加,然后移动指针
if(intervals[i][0] > r)
{
res.add(new int[]{l, r});
l = intervals[i][0];
r = intervals[i][1];
}
else
{
// 相交,即第二个左边小于第一个右边,取右边最大的
r = Math.max(r, intervals[i][1]);
}
}
// 必须添加最后一个,否则会漏掉
res.add(new int[]{l,r});
return res.toArray(new int[res.size()][]);
}
}
算法:双指针
题目
283. 移动零
class Solution {
public void moveZeroes(int[] nums) {
// 思路:双指针,非零元素往前移,最后补0
// slow记录非零个数,fast遇到0跳过,非0时将值给slow
int slow = 0;
for(int i = 0; i < nums.length; i++){
if(nums[i] != 0){
nums[slow] = nums[i];
slow++;
}
}
for(int i = slow; i < nums.length; i++){
nums[i] = 0;
}
}
}
算法:滑动窗口
原理
模板:
def findSubArray(nums):
N = len(nums) # 数组/字符串长度
left, right = 0, 0 # 双指针,表示当前遍历的区间[left, right],闭区间
sums = 0 # 用于统计 子数组/子区间 是否有效,根据题目可能会改成求和/计数
res = 0 # 保存最大的满足题目要求的 子数组/子串 长度
while right < N: # 当右边的指针没有搜索到 数组/字符串 的结尾
sums += nums[right] # 增加当前右边指针的数字/字符的求和/计数
while 区间[left, right]不符合题意: # 此时需要一直移动左指针,直至找到一个符合题意的区间
sums -= nums[left] # 移动左指针前需要从counter中减少left位置字符的求和/计数
left += 1 # 真正的移动左指针,注意不能跟上面一行代码写反
# 到 while 结束时,我们找到了一个符合题意要求的 子数组/子串
res = max(res, right - left + 1) # 需要更新结果
right += 1 # 移动右指针,去探索新的区间
return res
题目
3. 无重复字符的最长子串
class Solution {
public int lengthOfLongestSubstring(String s) {
if(s.length() == 0) return 0;
Map<Character, Integer> map = new HashMap();
int max = 0;
int left = 0;
for(int i = 0; i < s.length(); i++){
// 如果包含,map中的字符索引+1,然后取left最大值,更新左窗口
if(map.containsKey(s.charAt(i)))
left = Math.max(left, map.get(s.charAt(i)) + 1);
// 无论如何,更新一下字符当前索引,即更新右窗口
map.put(s.charAt(i), i);
// i - left + 1为窗口大小
max = Math.max(max, i-left+1);
}
return max;
}
}
1004. 最大连续1的个数 III
class Solution {
public int longestOnes(int[] nums, int k) {
int left = 0;
int right = 0;
int n = nums.length;
int sum = 0;
int res = 0;
// 右指针右移
while(right < n){
if(nums[right] == 0){
sum++;
}
// 条件不满足左指针右移
while(sum > k){
if(nums[left] == 0){
sum--;
}
left++;
}
res = Math.max(res, right - left + 1);
right++;
}
return res;
}
}
算法:二分搜索
场景
模板:
int binary_search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if(nums[mid] == target) {
// 直接返回
return mid;
}
}
// 直接返回
return -1;
}
int left_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定左侧边界
right = mid - 1;
}
}
// 最后要检查 left 越界的情况
if (left >= nums.length || nums[left] != target) {
return -1;
}
return left;
}
int right_bound(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else if (nums[mid] == target) {
// 别返回,锁定右侧边界
left = mid + 1;
}
}
// 最后要检查 right 越界的情况
if (right < 0 || nums[right] != target) {
return -1;
}
return right;
}
题目
33. 搜索旋转排序数组
class Solution {
public int search(int[] nums, int target) {
int l = 0;
int r = nums.length-1;
int num = nums[0];
while(l <= r){
int mid = l + (r - l)/2;
if(nums[mid] == target)
return mid;
// 中间比第一个大,则在左半边二分
if(nums[mid] >= num){
if(num <= target && target < nums[mid])
r = mid - 1;
else
l = mid + 1;
}
// 中间比第一个小,则在右半边二分
else if(nums[mid] < num){
if(nums[mid] < target && target <= nums[r])
l = mid + 1;
else
r = mid - 1;
}
}
return -1;
}
}
34. 在排序数组中查找元素的第一个和最后一个位置
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] res = new int[2];
if(nums.length == 0) return new int[]{-1,-1};
res[0] = leftSearch(nums,target);
res[1] = rightSearch(nums,target);
return res;
}
// 往左找
public static int leftSearch(int[] nums, int target){
int l = 0;
int r = nums.length-1;
while(l <= r){
int mid = l + (r - l)/2;
if(nums[mid] >= target)
r = mid - 1;
else if(nums[mid] < target)
l = mid + 1;
}
// 注意:target小,right左移,虽然r < 0,只要nums[l] != target 就能返回-1
// target大,left右移,l > l >= nums.length,返回-1
if(l >= nums.length || nums[l] != target) return -1;
return l;
}
// 往右找
public static int rightSearch(int[] nums, int target){
int l = 0;
int r = nums.length - 1;
while(l <= r){
int mid = l + (r - l)/2;
if(nums[mid] <= target)
l = mid + 1;
else
r = mid - 1;
}
// 注意:target小,right左移,r < 0,返回-1
// target大,left右移,只要nums[r] != target,返回-1
if(r < 0 || nums[r] != target) return -1;
return r;
}
}
240. 搜索二维矩阵 II
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for(int[] nums : matrix){
int res = binaraySearch(nums, target);
if(res >= 0){
return true;
}
}
return false;
}
public int binaraySearch(int[] nums, int target){
// 注意: int right = nums.length - 1
int left = 0;
int right = nums.length - 1;
// 注意:left <= right
while(left <= right){
int mid = (right - left) / 2 + left;
if(nums[mid] == target){
return mid;
}else if(nums[mid] > target){
right = mid - 1;
}else{
left = mid + 1;
}
}
return -1;
}
}
算法:分治
场景
待补充...
题目
215. 数组中的第K个最大元素
思路:求第K最大,即将数组逆序,求第K大
class Solution {
public int findKthLargest(int[] nums, int k) {
int low = 0;
int high = nums.length - 1;
while (true) {
int index = partition(nums, low, high);
if (index == k - 1) {
return nums[index];
}
else if (index < k - 1) {
low = index + 1;
}
else {
high = index - 1;
}
}
}
public int partition(int[] nums, int low, int high){
// 注意:降序排序
int pivot = nums[low];
while (low < high) {
while (low < high && nums[high] <= pivot) {
high--;
}
nums[low] = nums[high];
while (low < high && nums[low] >= pivot) {
low++;
}
nums[high] = nums[low];
}
nums[low] = pivot;
return low;
}
}
算法:深度搜索
场景
待补充...
题目
200.岛屿数量
class Solution {
void dfs(char[][] grid, int r, int c){
int rn = grid.length;
int cm = grid[0].length;
// 注意:grid[r][c] == '0'也会return
if(r < 0 || c < 0 || r >= rn || c >= cm || grid[r][c] == '0')
return;
grid[r][c] = '0';
dfs(grid, r - 1, c);
dfs(grid, r, c - 1);
dfs(grid, r + 1, c);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0)
return 0;
int n = grid.length;
int m = grid[0].length;
int count = 0;
// 注意:循环每个元素,然后每个元素深度搜索,找到一个1就++count,并把搜过的区域置为0
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(grid[i][j] == '1')
count++;
dfs(grid, i, j);
}
}
return count;
}
}
207.课程表
此题设置图的构建(邻接表)、拓扑排序、深度搜索,经典题目
class Solution {
// 构造一个邻接表
List<List<Integer>> graph = new ArrayList<>();
boolean hasCircle = false;
// 3种搜索状态, 0-未搜索;1-正在搜索;2-已搜索过
int[] visited;
public boolean canFinish(int numCourses, int[][] prerequisites) {
visited = new int[numCourses];
// 注意:初始化邻接表
for(int i = 0; i < numCourses; i++){
graph.add(new ArrayList<Integer>());
}
for(int[] info : prerequisites){
graph.get(info[1]).add(info[0]);
}
// 注意:不是prerequisites的长度,而是numCourses长度
for(int i = 0; i < numCourses; i++){
if(visited[i] == 0)
// 因为邻接表有i行,课程也对应i,所以从i开始
dfs(i);
}
return !hasCircle;
}
void dfs(int i){
// 正在搜索
visited[i] = 1;
// 循环单行邻接表
for(int k : graph.get(i)){
if(visited[k] == 0){
dfs(k);
}else if(visited[k] == 1){
hasCircle = true;
}
}
// 「未搜索」:我们还没有搜索到这个节点;
// 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);
// 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。
visited[i] = 2;
}
}
算法:广度搜索
场景
待补充...
题目
102. 二叉树的层序遍历
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new LinkedList<List<Integer>>();
//要用LinkedList
Queue<TreeNode> q = new LinkedList<TreeNode>();
//要放在res后面
if(root == null)
return res;
q.offer(root);
while(!q.isEmpty()){
int count = q.size();
//每次一层遍历完都要清空list,放在外面不能在里面
List<Integer> list = new ArrayList<>();
// 注意:里面还有一层循环,将当前队列中的所有节点向四周扩散
while(count > 0){
//取出节点
TreeNode node = q.poll();
//将节点的值存如list
list.add(node.val);
//左端点加入队列
if(node.left != null)
q.offer(node.left);
//右端点加入队列
if(node.right != null)
q.offer(node.right);
count--;
}
//将当层节点的值的List添加到res
res.add(list);
}
return res;
}
}
100.相同的树
class Solution {
public boolean isSameTree(TreeNode p, TreeNode q) {
if(p == null && q == null)
return true;
Queue<TreeNode> que = new LinkedList<>();
que.offer(p);
que.offer(q);
while(!que.isEmpty()){
TreeNode node1 = que.poll();
TreeNode node2 = que.poll();
// 注意:都为null的跳过这轮
if(node1 == null && node2 == null)
continue;
if(node1 == null || node2 == null || node1.val != node2.val)
return false;
que.offer(node1.left);
que.offer(node2.left);
que.offer(node1.right);
que.offer(node2.right);
}
return true;
}
}
116.填充每个节点的下一个右侧节点指针
class Solution {
public Node connect(Node root) {
if(root == null)
return null;
Queue<Node> que = new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int size = que.size();
for(int i = 0; i < size; i++){
Node node = que.poll();
if(i < size-1)
node.next = que.peek();
if(node.left != null)
que.offer(node.left);
if(node.right != null)
que.offer(node.right);
}
}
// 注意:返回root根节点即可
return root;
}
}
103.二叉树的锯齿形层序遍历
思路:借助双端队列实现,注意多了循环
class Solution {
public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if(root == null)
return res;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
boolean isLeft = true;
while(!que.isEmpty()){
Deque<Integer> deque = new LinkedList<>();
int size = que.size();
for(int i = 0; i < size; i++){
TreeNode cur = que.poll();
if(isLeft)
deque.offerLast(cur.val);
else
deque.offerFirst(cur.val);
if(cur.left != null)
que.offer(cur.left);
if(cur.right != null)
que.offer(cur.right);
}
res.add(new LinkedList<Integer>(deque));
isLeft = !isLeft;
}
return res;
}
}
130.被围绕的区域
class Solution {
int[] dx = {1, -1 , 0, 0};
int[] dy = {0, 0, 1, -1};
public void solve(char[][] board) {
// 控制上下左右查找
int n = board.length;
int m = board[0].length;
Queue<int []> que = new LinkedList<int []>();
// 处理边界,把边界O全都放在队列
for(int i = 0; i < n; i++){
if(board[i][0] == 'O'){
que.offer(new int[]{i, 0});
board[i][0] = 'A';
}
if(board[i][m-1] == 'O'){
que.offer(new int[]{i, m-1});
board[i][m-1] = 'A';
}
}
for(int i = 1; i < m-1; i++){
if(board[0][i] == 'O'){
que.offer(new int[]{0, i});
board[0][i] = 'A';
}
if(board[n-1][i] == 'O'){
que.offer(new int[]{n-1, i});
board[n-1][i] = 'A';
}
}
// 上下左右寻找和边界O相连的区域
while(!que.isEmpty()){
int[] temp = que.poll();
int x = temp[0];
int y = temp[1];
for(int i = 0; i < 4; i++){
int mx = x + dx[i];
int my = y + dy[i];
if(mx < 0 || my < 0 || mx >= n || my >= m || board[mx][my] != 'O')
continue;
que.offer(new int[]{mx, my});
board[mx][my] = 'A';
}
}
// 遍历替换
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(board[i][j] == 'A')
board[i][j] = 'O';
else if(board[i][j] == 'O')
board[i][j] = 'X';
}
}
}
}
算法:前缀和
场景
遇到「区间和」问题使用前缀和
适用的场景是原始数组不会被修改的情况下,频繁查询某个区间的累加和
这个前缀和数组 preSum 的含义也很好理解,preSum[i] 就是 nums[0..i-1] 的和。那么如果我们想求 nums[i..j] 的和,只需要一步操作 preSum[j+1]-preSum[i] 即可,而不需要重新去遍历数组了。
模板:
class PrefixSum {
// 前缀和数组
private int[] prefix;
/* 输入一个数组,构造前缀和 */
public PrefixSum(int[] nums) {
// 注意前缀和要申请n+1个空间
prefix = new int[nums.length + 1];
// 计算 nums 的累加和
for (int i = 0; i < prefix.length; i++) {
//
prefix[i+1] = prefix[i] + nums[i];
}
}
/* 查询闭区间 [i, j] 的累加和 */
public int query(int i, int j) {
return prefix[j + 1] - prefix[i];
}
}
题目
560. 和为 K 的子数组
思路:前缀和+哈希,哈希的形式和两数之和类似
public class Solution {
public int subarraySum(int[] nums, int k) {
// 思路:前缀和+哈希
int count = 0, pre = 0;
HashMap < Integer, Integer > mp = new HashMap < > ();
// 注意:这里不好理解
mp.put(0, 1);
for (int i = 0; i < nums.length; i++) {
pre += nums[i];
if (mp.containsKey(pre - k)) {
count += mp.get(pre - k);
}
mp.put(pre, mp.getOrDefault(pre, 0) + 1);
}
return count;
}
}
303. 区域和检索 - 数组不可变
public class _303区域和数组 {
int[] sum;
public void NumArray(int[] nums) {
int n = nums.length;
sum = new int[n+1];
for(int i = 0; i < n; i++){
// 求前缀和
sum[i+1] = sum[i] + nums[i];
}
}
public int sumRange(int left, int right) {
return sum[right+1] - sum[left];
}
}
304. 二维区域和检索 - 矩阵不可变
思路:二维的前缀和
class NumMatrix {
int[][] sums;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
if (m > 0) {
int n = matrix[0].length;
sums = new int[m + 1][n + 1];
// 注意:从0开始
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 求前缀和
sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
}
}
209. 长度最小的子数组
前缀和用来确定左边界,二分查找寻找右边界
class Solution {
public int minSubArrayLen(int target, int[] nums) {
int n = nums.length;
int[] sums = new int[n+1];
int ans = Integer.MAX_VALUE;
// 注意:从1开始,求前缀和
for(int i = 1; i <= n; i++){
sums[i] = sums[i-1] + nums[i-1];
}
for(int i = 1; i <= n; i++){
int value = target + sums[i-1];
int bound = Arrays.binarySearch(sums, value);
if(bound < 0){
bound = -bound - 1;
}
// 注意:判断条件不能直接else
if(bound <= n){
ans = Math.min(ans, bound-(i-1));
}
}
return ans == Integer.MAX_VALUE ? 0 : ans;
}
}
算法:差分
场景
差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。
步骤:
- 先求差分数组
- 对差分数组求前缀和
差分工具类包含:
- 传入一个原始数组,构造函数构造一个差分数组
- 差分数组d[i]加val,区间d[j+1]减val
- 叠加返回数组结果
题目
1109. 航班预订统计
class Solution {
public int[] corpFlightBookings(int[][] bookings, int n) {
int[] nums = new int[n];
Difference d = new Difference(nums);
for(int[] booking : bookings){
int i = booking[0] - 1;
int j = booking[1] - 1;
int val = booking[2];
d.increment(i, j, val);
}
return d.result();
}
}
// 差分工具类
class Difference {
private int[] diff;
Difference(int[] nums) {
diff = new int[nums.length];
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
public void increment(int i, int j, int val) {
// 差分数组d[i]加val,区间d[j+1]减val
diff[i] += val;
if (j + 1 < diff.length) {
diff[j + 1] -= val;
}
}
public int[] result() {
int[] res = new int[diff.length];
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
return res;
}
}
1094. 拼车
class Solution {
public boolean carPooling(int[][] trips, int capacity) {
int[] nums = new int[1001];
Differences d = new Differences(nums);
for (int[] trip : trips) {
// 下标不需要减一!细节是魔鬼
int i = trip[1];
int j = trip[2] - 1;
int val = trip[0];
d.increment(i, j, val);
}
return d.result(capacity);
}
}
// 查分工具类
class Differences {
public int[] diff;
Differences(int[] nums) {
diff = new int[nums.length];
diff[0] = nums[0];
for (int i = 1; i < nums.length; i++) {
diff[i] = nums[i] - nums[i - 1];
}
}
public void increment(int i, int j, int val) {
// 差分数组d[i]加val,区间d[j+1]减val
diff[i] += val;
if (j+1 < diff.length) {
diff[j+1] -= val;
}
}
public boolean result(int capacity) {
int[] res = new int[diff.length];
res[0] = diff[0];
for (int i = 1; i < diff.length; i++) {
res[i] = res[i - 1] + diff[i];
}
for (int i = 0; i < diff.length; i++) {
if (res[i] > capacity)
return false;
}
return true;
}
}
算法:单调栈
参考博客:labuladong.gitee.io/algo/2/23/6…
场景
单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。
题目
739. 每日温度
思路:单调栈,从后往前遍历,遇到比栈顶大的,则弹出,再将较大值压入栈。
单调栈模板也如下:
int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
// 这里放元素索引,而不是元素
Stack<Integer> s = new Stack<>();
/* 单调栈模板 */
for (int i = n - 1; i >= 0; i--) {
while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
s.pop();
}
// 得到索引间距
res[i] = s.isEmpty() ? 0 : (s.peek() - i);
// 将索引入栈,而不是元素
s.push(i);
}
return res;
}
496. 下一个更大元素 I
思路:单调栈
int[] nextGreaterElement(int[] nums) {
int n = nums.length;
// 存放答案的数组
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 倒着往栈里放
for (int i = n - 1; i >= 0; i--) {
// 判定个子高矮
while (!s.isEmpty() && s.peek() <= nums[i]) {
// 矮个起开
s.pop();
}
// nums[i] 身后的更大元素
res[i] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i]);
}
return res;
}
int[] nextGreaterElement(int[] nums1, int[] nums2) {
// 记录 nums2 中每个元素的下一个更大元素
int[] greater = nextGreaterElement(nums2);
// 转化成映射:元素 x -> x 的下一个最大元素
HashMap<Integer, Integer> greaterMap = new HashMap<>();
for (int i = 0; i < nums2.length; i++) {
greaterMap.put(nums2[i], greater[i]);
}
// nums1 是 nums2 的子集,所以根据 greaterMap 可以得到结果
int[] res = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
res[i] = greaterMap.get(nums1[i]);
}
return res;
}
503. 下一个更大元素 II
思路:环形数组技巧:翻倍数组并取模
class Solution {
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 数组长度加倍模拟环形数组
for (int i = 2 * n - 1; i >= 0; i--) {
// 索引 i 要求模,其他的和模板一样
while (!s.isEmpty() && s.peek() <= nums[i % n]) {
s.pop();
}
res[i % n] = s.isEmpty() ? -1 : s.peek();
s.push(nums[i % n]);
}
return res;
}
}
遗留问题
上一个更小元素/上一个更大元素/下一个更小元素/下一个更大元素 分别怎么改?
1、上一个更小元素
int[] func(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
//注意:从前往后遍历
for (int i = 0; i < n; i++) {
// 注意:单调栈,栈里大的弹出,保证小栈
while (!s.isEmpty() && temperatures[s.peek()] >= temperatures[i]) {
s.pop();
}
res[i] = s.isEmpty() ? -1 : (s.peek());
s.push(i);
}
for (int i : res) {
System.out.println(i);
}
return res;
}
2、上一个更大元素
int[] func(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
//注意:从前往后遍历
for (int i = 0; i < n; i++) {
// 注意:单调栈,栈里小的弹出,保证大栈
while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
s.pop();
}
res[i] = s.isEmpty() ? -1 : (s.peek());
s.push(i);
}
for (int i : res) {
System.out.println(i);
}
return res;
}
3、下一个更小元素
int[] func(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
//注意:从后往前遍历
for (int i = n - 1; i >= 0; i--) {
// 注意:单调栈,栈里大的弹出,保证小栈
while (!s.isEmpty() && temperatures[s.peek()] >= temperatures[i]) {
s.pop();
}
res[i] = s.isEmpty() ? -1 : (s.peek());
s.push(i);
}
for (int i : res) {
System.out.println(i);
}
return res;
}
4、下一个更大元素
int[] func(int[] temperatures) {
int n = temperatures.length;
int[] res = new int[n];
Stack<Integer> s = new Stack<>();
// 思路:从后往前遍历
for (int i = n - 1; i >= 0; i--) {
// 注意:单调栈,栈里小的弹出,保证大栈
while (!s.isEmpty() && temperatures[s.peek()] <= temperatures[i]) {
s.pop();
}
res[i] = s.isEmpty() ? -1: (s.peek());
s.push(i);
}
for(int i : res){
System.out.println(i);
}
return res;
}
算法:堆
场景
最值要想到用堆
一般来说需要构造一个优先队列,比较器需要根据实际需求写;然后再添加到堆中
比较器如果有多条件比较,即第一个条件相等,比较第二个条件,可以写成以下三元组写法:
(int[] a, int[] b) -> a[0] != b[0] ? b[0] - a[0] : a[1] - b[1])
题目
215. 数组中的第K个最大元素
思路:第K个最值,想到用堆
class Solution {
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2)->o2 - o1);
for(int n : nums){
queue.offer(n);
}
int res = 0;
for (int i = 0; i < k; i++){
res = queue.poll();
}
return res;
}
}
347. 前 K 个高频元素
经典题目
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> map = new HashMap<>();
int[] res = new int[k];
// 思路:key为值,value为次数,以次数排序
for(int n: nums){
// 注意: getOrDefault的使用
map.put(n, map.getOrDefault(n, 0) + 1);
}
// 注意:PriorityQueue的写法要带一个比较器
PriorityQueue<int[]> queue = new PriorityQueue<>((int[] a, int[] b) -> b[1] - a[1]);
// 注意:map的遍历
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
int num = entry.getKey();
int count = entry.getValue();
queue.offer(new int[]{num, count});
}
for(int i = 0; i < k; i++){
res[i] = queue.poll()[0];
}
return res;
}
}
239. 滑动窗口最大值
思路:我们将数组 nums 的前 k 个元素放入优先队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入优先队列中,此时堆顶的元素就是堆中所有元素的最大值。然而这个最大值可能并不在滑动窗口中,在这种情况下,这个值在数组 nums 中的位置出现在滑动窗口左边界的左侧。因此,当我们后续继续向右移动窗口时,这个值就永远不可能出现在滑动窗口中了,我们可以将其永久地从优先队列中移除。
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 1. 优先队列存放的是二元组(num,index) : 大顶堆(元素大小不同按元素大小排列,元素大小相同按下标进行排列)
// num : 是为了比较元素大小
// index : 是为了判断窗口的大小是否超出范围
PriorityQueue<int[]> queue = new PriorityQueue<>((int[] a, int[] b) -> a[0] != b[0] ? b[0] - a[0] : a[1] - b[1]);
// 2. 优选队列初始化 : k个元素的堆
for(int i = 0;i < k;i++){
pq.offer(new int[]{nums[i],i});
}
// 3. 处理堆逻辑
int[] res = new int[n - k + 1]; // 初始化结果数组长度 :一共有 n - k + 1个窗口
res[0] = pq.peek()[0]; // 初始化res[0] : 拿出目前堆顶的元素
for(int i = k;i < n;i++){ // 向右移动滑动窗口
pq.offer(new int[]{nums[i],i}); // 加入大顶堆中
while(pq.peek()[1] <= i - k){ // 将下标不在滑动窗口中的元素都干掉
pq.poll(); // 维护:堆的大小就是滑动窗口的大小
}
res[i - k + 1] = pq.peek()[0]; // 此时堆顶元素就是滑动窗口的最大值
}
return res;
}
}