目前进度:80/100
1.滑动窗口最大值
1.暴力解法(超时)
定义数组维护滑动窗口,每次更新窗口并找最大值
public int[] maxSlidingWindow(int[] nums, int k) {
int[] window = new int[k]; //定义一个长度为k的滑动窗口;
int[] ans = new int[nums.length - k + 1]; //定义答案数组
int max;
for (int i = 0; i <= nums.length - k; i++){
window = initWin(nums, i, k, window); //维护滑动窗口
max = findMax(window);
ans[i] = max;
}
return ans;
}
public int[] initWin(int[] nums, int init, int k, int[] win){
for (int i = 0; i < k; i++){
win[i] = nums[init + i];
}
return win;
}
public int findMax(int[] win){
int tem = win[0];
for (int i : win){
if (tem < i){
tem = i;
}
}
return tem;
}
2.使用优先队列(大根堆)储存并维护窗口
不需要考虑窗口内的大小、顺序,只要比窗口最大值还大的干扰值,能够正确被移除即可。即:{1,2,4,3}和{1,2,2,3,3,4}最后找到的最大值都一样。
在Java的优先队列(PriorityQueue)中,当使用自定义的比较器来确定元素的顺序时,返回负值表示第一个参数排在第二个参数之前,返回正值表示第二个参数排在第一个参数之前,返回0表示两个参数相等,顺序不变。
通过自定义compare()实现降序排列即堆顶永远是当前窗口最大值。
public int compare(int[] pair1, int[] pair2) {
return pair2[0] - pair1[0];
}
compare()在两值相等的情况下,入队早的排前面。由于本身按下标升序入队,所以符合滑动窗口变化规律 ,不需要对相等的情况单独处理。(与力扣题解不同)
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] pair1, int[] pair2) {
return pair2[0] - pair1[0];
}
});
for (int i = 0; i < k; ++i) {
pq.offer(new int[]{nums[i], i});
}
int[] ans = new int[n - k + 1];
ans[0] = pq.peek()[0];
for (int i = k; i < n; ++i) {
pq.offer(new int[]{nums[i], i});
while (pq.peek()[1] <= i - k) {
pq.poll();
}
ans[i - k + 1] = pq.peek()[0];
}
return ans;
}
2.最小覆盖子串
t串可能有重复的,所以,选择哈希表储存t的字符和出现次数,即tMap。新建sMap维护当前滑动窗口的字符和出现次数。
使用双指针的思路。定义左右指针l,r,r先从左到右遍历,直到requireClass == 0(requireClass起到一个flag的作用)。这时候左指针从左到右遍历,目的是缩小滑动窗口边界。
但是,如果只到这一步就停止了,最后结果只是一个包含t的子串,不是最小子串。
所以,为了得到最小子串,需要定义minLength和minStart,目的是储存最小子串的起始位置长度,而滑动窗口需要一直遍历到最后一位,只有最小的length才能更新到minLength。
class Solution {
public String minWindow(String s, String t) {
if (s == null || t == null || s.length() == 0 || t.length() == 0) {
return "";
}
HashMap<Character, Integer> sMap = new HashMap<>();
HashMap<Character, Integer> tMap = new HashMap<>();
// 初始化 tMap,记录 t 中每个字符的出现次数
for (char c : t.toCharArray()) {
tMap.put(c, tMap.getOrDefault(c, 0) + 1);
}
int left = 0; // 窗口左边界
int right = 0; // 窗口右边界
int minLength = Integer.MAX_VALUE; // 最小子串长度
int minStart = 0; // 最小子串起始位置
int requiredChars = tMap.size(); // 需要匹配的字符数
while (right < s.length()) {
char currentChar = s.charAt(right);
sMap.put(currentChar, sMap.getOrDefault(currentChar, 0) + 1);
if (tMap.containsKey(currentChar) && sMap.get(currentChar).intValue() == tMap.get(currentChar).intValue()) {
requiredChars--;
}
// 当所有 t 中的字符都在窗口中找到时
while (requiredChars == 0) {
if (right - left + 1 < minLength) {
minLength = right - left + 1;
minStart = left;
}
char leftChar = s.charAt(left);
sMap.put(leftChar, sMap.get(leftChar) - 1);
if (tMap.containsKey(leftChar) && sMap.get(leftChar).intValue() < tMap.get(leftChar).intValue()) {
requiredChars++;
}
left++;
} //主要目的是缩小左边界
right++;
}
return minLength == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLength);
}
}
3.删除链表倒数第N个节点
注意定义dummy 解决头节点删除问题。
4.环形链表II
1.用哈希表记录走过的节点
再次出现的就是环形链表的首部
public ListNode detectCycle(ListNode head){
HashMap<ListNode, Boolean> check = new HashMap<ListNode, Boolean>();
if (head == null){
return null;
}
while (head.next != null){
if (check.containsKey(head)){
return head;
}
else {
check.put(head,true);
}
head = head.next;
}
return null;
}
2.快慢指针
第一次相遇时,快指针和慢指针走过的路程,正好是圈数的整数倍。
第二次人为移动快指针到head,这时候设置两指针速度相等,它们一定会在入环的第一个节点相遇。所以返回第二次相遇的节点。
5.两两交换链表中的节点
定义dummy处理头节点,两个及两个以上节点的链表才需要处理。交换节点的顺序“
dummy.next = dummy.next.next; dummy.next.next = temp; dummy.next.next.next = next;
”是固定的。
“dummy.next != null && dummy.next.next != null” 且的顺序也是固定的,因为“ListNode next = dummy.next.next.next;”节点可能是null,对空节点的.next操作会报错。而Java如果且的前一个是错,就不会执行后一个,因此可以避免程序抛出异常。
记得每次前进两步。
最后返回的时候,返回的是新的头节点,即“newHead = newHead.next;”
public ListNode swapPairs(ListNode head) {
ListNode dummy = new ListNode(0, head);
ListNode newHead = dummy;
if (head == null || head.next == null){
return head;
}
while (dummy.next != null && dummy.next.next != null){ //两个及以上
ListNode temp = dummy.next;
ListNode next = dummy.next.next.next;
dummy.next = dummy.next.next;
dummy.next.next = temp;
dummy.next.next.next = next;
dummy = dummy.next.next;
}
newHead = newHead.next;
return newHead;
}
6.K个一组翻转链表
把翻转链表单独抽象出来,按遍历顺序(左->右),每个节点指向新的尾结点,并更新尾结点(右->左)当tail == prev(绝对没有任何一个节点会指向翻转后的首节点)循环结束。
public ListNode[] myReverse(ListNode head, ListNode tail) {
ListNode prev = tail.next;
ListNode p = head;
while (prev != tail) {
ListNode nex = p.next;
p.next = prev;
prev = p;
p = nex;
}
return new ListNode[]{tail, head};
}
7.排序链表
1.定义活动数组储存链表,活动数组冒泡排序,排序后结果赋值给链表=>超时。换成快排还超时,再换归并排序,通过。
public class Solution {
public ListNode sortList(ListNode head) {
// 将链表中的值提取到 ArrayList 中
ArrayList<Integer> ans = new ArrayList<>();
ListNode p = head;
while (p != null) {
ans.add(p.val);
p = p.next;
}
// 对 ArrayList 使用归并排序算法进行排序
mergeSort(ans, 0, ans.size() - 1);
// 将排序后的值重新赋给链表节点
p = head;
for (int i = 0; i < ans.size(); i++) {
p.val = ans.get(i);
p = p.next;
}
return head;
}
public void mergeSort(ArrayList<Integer> ans, int low, int high) {
if (low < high) {
int mid = low + (high - low) / 2;
mergeSort(ans, low, mid);
mergeSort(ans, mid + 1, high);
merge(ans, low, mid, high);
}
}
public void merge(ArrayList<Integer> ans, int low, int mid, int high) {
int n1 = mid - low + 1;
int n2 = high - mid;
// 创建临时数组
int[] L = new int[n1];
int[] R = new int[n2];
// 将数据复制到临时数组 L 和 R 中
for (int i = 0; i < n1; ++i) {
L[i] = ans.get(low + i);
}
for (int j = 0; j < n2; ++j) {
R[j] = ans.get(mid + 1 + j);
}
// 归并临时数组 L 和 R 到 ans
int i = 0, j = 0, k = low;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
ans.set(k, L[i]);
i++;
} else {
ans.set(k, R[j]);
j++;
}
k++;
}
// 将剩余元素复制到 ans
while (i < n1) {
ans.set(k, L[i]);
i++;
k++;
}
while (j < n2) {
ans.set(k, R[j]);
j++;
k++;
}
}
}
不使用额外空间:归并排序本身就是不停地拆分再合并,因此很适合操作链表。
class Solution {
public ListNode sortList(ListNode head) {
//不使用额外空间的归并排
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail) {
if (head == null){
return head;
}
if (head.next == tail){ //相当于链表只有一个节点,传进来的tail实质是结束标志。不处理tail,在向上递归的中间过程中,tail会作为mid(第二个的head)在第二个sortlist()中处理,最后一次递归传进来的tail是null,
head.next = null; //这一步是为了断开链表,方便后边重排序。
return head;
}
//快慢指针找mid
ListNode fast = head;
ListNode slow = head;
while (fast != tail && fast.next != tail){
fast = fast.next.next;
slow = slow.next;
}
ListNode mid = slow;
ListNode h1 = sortList(head, mid);
ListNode h2 = sortList(mid, tail);
ListNode sorted = merge(h1, h2);
return sorted;
}
public ListNode merge(ListNode h1, ListNode h2){
if (h1 == null || h2 == null){
return h1 == null? h2:h1;
}
ListNode dummy = new ListNode(0);
ListNode temp = dummy;
while (h1 != null && h2 != null){
if (h1.val < h2.val){
temp.next = h1;
h1 = h1.next;
}
else {
temp.next = h2;
h2 = h2.next;
}
temp = temp.next;
}
if (h1 == null){
temp.next = h2;
}
else {
temp.next = h1;
}
return dummy.next;
}
}
8.合并 K 个升序链表
public ListNode merge(ListNode h1, ListNode h2){
if (h1 == null || h2 == null){
return h1 == null? h2:h1;
}
ListNode dummy = new ListNode(0);
ListNode temp = dummy;
while (h1 != null && h2 != null){
if (h1.val < h2.val){
temp.next = h1;
h1 = h1.next;
}
else {
temp.next = h2;
h2 = h2.next;
}
temp = temp.next;
}
if (h1 == null){
temp.next = h2;
}
else {
temp.next = h1;
}
return dummy.next;
}
public ListNode mergeKLists(ListNode[] lists) {
int num = lists.length; //取k的值
ListNode temp = lists[0];
for (int i = 1; i < num; i++){
temp = merge(temp, lists[i+1]);
}
return temp;
}
9.随机链表的复制
先构建一个完整的链表,再处理random,拷贝前后节点的相对关系不变(head<->temp, head.random <-> temp.random),因此新建哈希表,储存新旧链表对应节点的对应关系。
class Solution {
public Node copyRandomList(Node head) {
if (head == null) {
return null;
}
HashMap<Node, Node> map = new HashMap<>();
Node dummy = new Node(0);
Node newCurr = dummy;
Node curr = head;
// 第一遍循环,复制每个节点的值到新节点,同时构建原始节点到新节点的映射关系
while (curr != null) {
Node newNode = new Node(curr.val);
map.put(curr, newNode);
newCurr.next = newNode;
newCurr = newCurr.next;
curr = curr.next;
}
// 第二遍循环,设置新节点的随机指针
curr = head;
newCurr = dummy.next;
while (curr != null) {
newCurr.random = map.get(curr.random);
curr = curr.next;
newCurr = newCurr.next;
}
return dummy.next;
}
}
10. 旋转图像
因为题目要求在原二维数组的基础上修改,在遍历过程中原二维数组会被更改,所以需要新的空间存储原有的数据关系(相当于快照)。新建record数组存储依次遍历的元素,寻找两个数组下标之间的关系。分析示例1
record = [1, 2, 3, 4, 5, 6, 7, 8, 9], 新二维数组下标与record下标对应关系如下:
[0, 0] = 2 * 3 [1,0] = 2 * 3 + 1 [2,0] = 2 * 3 + 2
[0, 1] = 1 * 3 [1,1] = 1 * 3 + 1 [2,1] = 1 * 3 + 2
[0, 2] = 0 * 3 [1,2] = 0 * 3 + 1 [2,2] = 0 * 3 + 2
观察可知:matrix[i][j] = record[n * (n - 1 - j) + i]
public void rotate(int[][] matrix) {
int n = matrix.length;
int[] record = new int[n * n];
int count = 0;
for (int i = 0; i < n; i++){
for (int j = 0; j < n; j++){
record[count++] = matrix[i][j]; //record储存数据
}
}
for (int i = 0; i < n; i++){
for (int j = 0; j < n; j++){
matrix[i][j] = record[n * (n - 1 - j) + i];
}
}
}
11.LRU 缓存
1.记录最近最久未使用的节点 + put()的时间复杂度是O(1) => 双向链表。双向链表既可以维护所有节点的使用状态,又可以通过dummy tail 迅速删除尾结点。
2.get() put()的实现功能与hashmap相似。
故本题使用hashmap+双链表实现
注意:在put
方法中,应该检查缓存中是否已经存在相同的键,如果存在则更新值并将节点移动到头部。
import java.util.HashMap;
class LRUCache {
class myNode {
public int key;
public int val;
public myNode prev;
public myNode next;
public myNode(int key, int val) {
this.key = key;
this.val = val;
}
}
private HashMap<Integer, myNode> record = new HashMap<>();
private myNode head, tail;
private int size;
private int capacity;
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
head = new myNode(-1, -1);
tail = new myNode(-1, -1);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (!record.containsKey(key)) {
return -1;
}
myNode temp = record.get(key);
// remove
temp.prev.next = temp.next;
temp.next.prev = temp.prev;
// add to head
addToHead(temp);
return record.get(key).val;
}
public void put(int key, int value) {
// check if the key exists
if (record.containsKey(key)) {
myNode node = record.get(key);
// update value
node.val = value;
// remove the node from the list
node.prev.next = node.next;
node.next.prev = node.prev;
// move the node to the head
addToHead(node);
} else {
// if the key does not exist, add a new node to the head
myNode newNode = new myNode(key, value);
record.put(key, newNode);
addToHead(newNode);
// check if the capacity is exceeded
if (size == capacity) {
// remove the last node
myNode lastNode = tail.prev;
lastNode.prev.next = tail;
tail.prev = lastNode.prev;
record.remove(lastNode.key);
size--;
}
size++;
}
}
private void addToHead(myNode temp) {
temp.next = head.next;
head.next.prev = temp;
head.next = temp;
temp.prev = head;
}
}
12.二叉搜索树中第K小的元素
中序遍历二叉搜索树,得到升序排列,第K个就是所求
public int kthSmallest(TreeNode root, int k) {
ArrayList<Integer> record = new ArrayList<>();
middleSort(root, record);
int ans = record.get(k - 1);
return ans;
}
public void middleSort(TreeNode root, ArrayList<Integer> record){
if (root == null){
return;
}
middleSort(root.left, record);
record.add(root.val);
middleSort(root.right, record);
}
13. 二叉树的右视图
每层最右节点就是右视图
广度搜索:维护队列 : remove() => add(左子树)=> add(右子树)
不断覆盖最右值
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public List<Integer> rightSideView(TreeNode root) {
ArrayList<Integer> ans = new ArrayList<>();
HashMap<Integer, Integer> depthRecord = new HashMap<>();
Queue<TreeNode> nodeQueue = new LinkedList<>();
Queue<Integer> depth = new LinkedList<>();
nodeQueue.add(root);
depth.add(0);
while (!nodeQueue.isEmpty()){
TreeNode temp = nodeQueue.remove();
int nowDepth = depth.remove();
if (temp != null){
depthRecord.put(nowDepth, temp.val);
nodeQueue.add(temp.left);
nodeQueue.add(temp.right);
depth.add(nowDepth + 1);
depth.add(nowDepth + 1);
}
}
int count = 0;
while (depthRecord.containsKey(count++)){
ans.add(depthRecord.get(count - 1));
}
return ans;
}
}
14.二叉树的层序遍历
队列
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<List<Integer>>();
if (root == null) {
return ans;
}
Queue<TreeNode> nodeScan = new LinkedList<>();
nodeScan.add(root);
while (!nodeScan.isEmpty()){
ArrayList<Integer> everyD = new ArrayList<>();
int everySize = nodeScan.size();
for (int i = 0; i < everySize; i++){
TreeNode temp = nodeScan.poll();
everyD.add(temp.val);
if (temp.left != null){
nodeScan.add(temp.left); //空节点会影响size
}
if (temp.right != null){
nodeScan.add(temp.right);
}
}
ans.add(everyD);
}
return ans;
}
15.二叉树展开为链表
返回类型为void => 需要在原树基础上进行修改,最容易想到新建一个数组存储节点修改后的顺序。
public void flatten(TreeNode root) {
//先序遍历
List<TreeNode> ans = new ArrayList<>();
preOrder(root, ans);
int n = ans.size();
for (int i = 1; i < n; i++){
root.left = null;
root.right = ans.get(i);
root = root.right;
}
}
public void preOrder(TreeNode root, List<TreeNode> ans){
if (root == null){
return ;
}
ans.add(root);
preOrder(root.left, ans);
preOrder(root.right, ans);
}
16.二叉树的最近公共祖先
16.1
1.根节点一定是公共祖先,向下遍历找新根节点
2.新根节点要么在左,要么在右,不可能既在左,又在右。(观察可知)
3.祖先就是:向下遍历,既能遍历到p,又能遍历到q
超时 30/32:因为每次都层序遍历节点
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//根节点一定是公共祖先,向下遍历查看是否有能做新根节点的节点
boolean L = ifRoot(root.left, p) && ifRoot(root.left, q);
boolean R = ifRoot(root.right, p) && ifRoot(root.right, q);
TreeNode ans = root;
while (L||R){ //只要有一个是真,就可以继续往下找,公共祖先要么在左节点,要么在右节点,不可能既在左节点,又在右节点
if (L){
root = root.left;
L = ifRoot(root.left, p) && ifRoot(root.left, q);
R = ifRoot(root.right, p) && ifRoot(root.right, q);
} else if (R) {
root = root.right;
L = ifRoot(root.left, p) && ifRoot(root.left, q);
R = ifRoot(root.right, p) && ifRoot(root.right, q);
}
ans = root;
}
return ans;
}
public boolean ifRoot(TreeNode root, TreeNode p){
Queue<TreeNode> scan = new LinkedList<>(); //层序遍历维护队列
scan.add(root);
while (!scan.isEmpty()){
TreeNode temp = scan.poll();
if (temp != null){
if (temp == p){
return true; //找到p
}
scan.add(temp.left);
scan.add(temp.right);
}
}
return false;
}
16.2
public class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) {
return root; // 根节点为空,或者是p、q节点本身,则直接返回根节点
}
// 递归查找左子树中是否包含p或q
TreeNode left = lowestCommonAncestor(root.left, p, q);
// 递归查找右子树中是否包含p或q
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 如果左子树和右子树分别包含了p和q,则当前节点为它们的最近公共祖先
if (left != null && right != null) {
return root;
}
// 否则,返回非空的节点,如果都为空,则返回空
return left != null ? left : right;
}
}
17.从前序与中序遍历序列构造二叉树
从先序遍历里取出来的根放到中序遍历里找就能确定出左右子树的结构。“找”的过程可以用递归实现。
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || inorder == null || preorder.length != inorder.length || preorder.length == 0) {
return null;
}
return buildTreeHelper(preorder, 0, preorder.length - 1, inorder, 0, inorder.length - 1);
}
private TreeNode buildTreeHelper(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd) {
if (preStart > preEnd || inStart > inEnd) {
return null;
}
// 根据先序遍历结果确定根节点
int rootVal = preorder[preStart];
TreeNode root = new TreeNode(rootVal);
// 在中序遍历中找到根节点的位置
int rootIndex = inStart;
while (rootIndex <= inEnd && inorder[rootIndex] != rootVal) {
rootIndex++;
}
// 计算左子树和右子树的节点个数
int leftSize = rootIndex - inStart;
// 递归构造左右子树
root.left = buildTreeHelper(preorder, preStart + 1, preStart + leftSize, inorder, inStart, rootIndex - 1);
root.right = buildTreeHelper(preorder, preStart + leftSize + 1, preEnd, inorder, rootIndex + 1, inEnd);
return root;
}
18.课程表
一开始认为课程之间的关系是单链表,快慢指针判断有无环。后来发现不太对,1是链表只有一个后继,而本题的依赖关系可以是多对多;2是有些课程是游离的,没有依赖关系。综合上述两种情况,本题储存元素和它们关系的数据结构是图。
Java的传参是值传递,一不小心就会出错,可以善用这种将经常传递的变量定义为类成员变量的方式。需要用到深搜/递归的时候,用这种方式一方面不需要传来传去,数据始终是最新的,另一方面可以节省内存,省去了很多中间变量,方便gc(扯远了)
有个需要注意的是,如果在深搜过程中出现全搜完的点(值为2),并不表示环的出现,因为我们其实不知道图的起点,如果先搜索完中间节点,再搜到根节点,也会出现这种情况。
本题最方便的地方是课程(也就是点)刚好就是下标,不用再储存点了哈哈哈。
int[] used;
boolean result;
List<List<Integer>> edges;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<>(); //定义有向图
used = new int[numCourses];
result = true;
for (int i = 0; i < numCourses; i++) {
edges.add(new ArrayList<>());
} // 初始化有向图的点,正好下标和点一样
for (int[] each : prerequisites){
edges.get(each[1]).add(each[0]); //初始化有向图的边
}
for (int i = 0; i < numCourses; i++) { //对每个未被搜索的点进行深搜,找有向图的环
if (used[i] == 0 && result)
dfs(i);
}
return result;
}
private void dfs(int nowPoint) { //搜索过程如果发现已经搜过的点,说明出现了环
used[nowPoint] = 1;
for (int i : edges.get(nowPoint)) {
if (used[i] == 0) {
dfs(i);
}
else if(used[i] == 1){
result = false;
return;
}
}
used[nowPoint] = 2; //全搜完是2
}
19.搜索旋转排序数组
二分查找的变种,可以看成分成两个数组分别进行二分查找。需要注意
1.nums只有一个元素的情况
2.第一个旋转数组长度是n,第二个是0,也就是未旋转。
3.边界值处理,mid需要放到右边的查找数组
public int search(int[] nums, int target) {
int ans1 = -1;
int ans2 = -1;
if (nums.length == 1 && nums[0] == target){
return 0;
}
for (int i = 1; i < nums.length; i++){
if (nums[i] < nums[i - 1]){
ans1 = simpleSearch(nums, 0, i - 1, target);
ans2 = simpleSearch(nums, i, nums.length - 1,target);
}
}
ans1 = Math.max(ans1, ans2);
ans2 = simpleSearch(nums, 0, nums.length - 1,target);
return ans1 == -1? ans2 : ans1;
}
public int simpleSearch(int[] nums, int start, int end, int target) {
while (start <= end) {
int temp = (start + end) / 2;
if (nums[temp] < target) {
start = temp + 1;
} else if (nums[temp] > target) {
end = temp - 1;
}
else {
return temp;
}
}
return -1;
}
20.寻找重复数
1.鸽巢原理
鸽巢原理基本思想是:如果有 \(n\) 个鸽子放进 \(n-1\) 个鸽巢里,那么至少有一个鸽巢里会有不止一只鸽子。
cnt[i]是原数组中小于等于mid的个数,如果cnt[i]大于mid,说明重复在左边;反之在右边。
public int findDuplicate(int[] nums) {
int n = nums.length;
int l = 1, r = n - 1, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1;
int cnt = 0;
for (int i = 0; i < n; ++i) {
if (nums[i] <= mid) {
cnt++;
}
}//依次记录每次小于等于mid的cnt
if (cnt <= mid) {
l = mid + 1;
} else {
r = mid - 1;
}
}
ans = l;
return ans;
}
21.打家劫舍
动态规划,对于k>2,只有偷或不偷,偷:总金额 = f(k) = nums[k] + f(k-2);不偷:总金额 = f(k - 1),两者取大。
public int rob(int[] nums) {
int l = nums.length;
if (l == 1) {
return nums[0];
}
else if (l == 2) {
return Math.max(nums[0], nums[1]);
}
nums[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < l; i++) {
nums[i] = Math.max(nums[i - 1], nums[i - 2] + nums[i]);
}
return nums[l - 1];
}
22.每日温度
只有后一个比前一个小的情况下,才需要暂存,基于此可以用单调栈解题(队列也可以),基于题意,此单调栈是单调递减的。
public int[] dailyTemperatures(int[] temperatures) {
int[] ans = new int[temperatures.length];
Deque<Integer> stack = new LinkedList<>();
for (int i = 0; i < temperatures.length; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int index = stack.pop();
ans[index] = i - index;
}
stack.push(i);
}
return ans;
}
23.组合总和
借助dfs和回溯的思想进行每次搜索,我们只控制每次回溯的步骤和终止条件。整体代码与回溯tag下前几题架构一致。
class Solution {
List<Integer> temp = new ArrayList<>();
int sum = 0;
List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
dfs(0, candidates, target);
return ans;
}
public void dfs(int cur, int[] candidates, int target) {
if (sum == target) {
ans.add(new ArrayList<>(temp));
return;
}
if (sum > target) {
return;
}
for (int i = cur; i < candidates.length; i++) {
temp.add(candidates[i]);
sum += candidates[i];
dfs(i, candidates, target);
temp.remove(temp.size() - 1);
sum -= candidates[i];
}
}
}
24.单词搜索
“其中“相邻”单元格是那些水平相邻或垂直相邻的单元格” => 向四个方向搜索
“同一个单元格内的字母不允许被重复使用” => 定义二维数组存放该位置在同一次搜索过程中是否访问过
class Solution {
public boolean flag = false;
public boolean exist(char[][] board, String word) {
int m = board.length;
int n = board[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
dfs(board, word, visited, i, j, 0);
}
}
return flag;
}
public void dfs(char[][] board, String word, boolean[][] visited, int i, int j, int index) {
if (index == word.length()) {
flag = true;
return;
}
if (i < 0 || j >= board[0].length || i >= board.length || j < 0 || visited[i][j] || board[i][j] != word.charAt(index)) {
return;
}
visited[i][j] = true; //对本元素检查完毕
//向四个方向继续搜索
dfs(board, word, visited, i + 1, j, index + 1);
dfs(board, word, visited, i - 1, j, index + 1);
dfs(board, word, visited, i, j + 1, index + 1);
dfs(board, word, visited, i, j - 1, index + 1);
visited[i][j] = false;
}
}
25.零钱兑换
amount可达则一定有amount - coin可达(coin是coins里的一个元素),据此可以使用动态规划的思想解题,在完成了可达性判断的同时还可以维护最小值。
import java.util.Arrays;
public class Solution {
public int coinChange(int[] coins, int amount) {
// 初始化数组,使用 amount + 1 作为初始值,这意味着默认情况下无法达到该金额
int[] f = new int[amount + 1];
Arrays.fill(f, amount + 1);
f[0] = 0; // 金额为0时,硬币数为0
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i - coin >= 0) {
f[i] = Math.min(f[i], f[i - coin] + 1);
}
}
}
// 如果f[amount]依然是初始化时的值,说明无法凑出该金额,返回-1
return f[amount] > amount ? -1 : f[amount];
}
}
如果amount可达,则f[amount] 一定<= amount(f[amount] 代表凑成总金额amount所需的最少的硬币个数)。所以我们给数组赋初值Arrays.fill(f, amount + 1)。同时“f[i] = Math.min(f[i], f[i - coin] + 1);”这步除了更新最小值也有可达性校验,即如果amount不可达,数组的结果不会被更新。
注意一个特例f[0] = 0