不一定是看题目是否常规,而是在各种奇怪的问题下找到解题思路用代码把题目解出来
1.Hash相关
当使用HashMap时,为什么可以以时间复杂度O(1)根据map.get(key)查找对应的value呢?
因为物理结构用到了哈希表
根据特别的算法算出Entry应该存放的数组位置,如果位置相同,有各种方法解决冲突以及冲突之后找到value
-
q387,找到字符串的第一个不重复的字符,并返回它的索引
利用hash,value可以放各种东西,比如第一次出现的位置,如果多次出现就置为-1。hash存放是没有顺序的,但是可以做到最快速度找到,O(1)
-
q49字母异位词分组
题目的意思就是把字符串数组的abc,bac,cab…分一组
这种类型想到hash,先想到什么是键什么是值
键是唯一的,同一组唯一的相同点就是他们的字母和次数是唯一的,只是位置不同,这是解题的最关键一点
- 字母排序后作为键
- 相同字母出现的次数作为键
知识点:
- String key = String(array)好于String.valueOf(array)好于array.toString
- valueOf可以预防array为null,但是会转化为字符串"null"
- 很多类型的转换不能用强制,一般都是重新new一个所需类型再在new的同时放进去
- String字符串不能直接排成字典序,需要先转为char数组,再转回来
- 不要把数组作为Map中的key,containsKey()值一样也对应不上
-
q560和为K的子数组
哈希+子前缀方法,哈希数组里key存的是遍历过程中从头到此位置的所有数组和,value是出现次数
pre是前面所有元素的和,从j到i => pre[j−1]==pre[i]−k
比如:前6个元素和为14,目标是找7,那么就map.get(14-7),看从头到某个位置元素的和有多少个满足和为7的,累加
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Vndn7nM-1657459118118)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211111202134415.png)]
public int subarraySum(int[] nums, int k) { int count = 0; Map<Integer, Integer> map = new HashMap<>(); map.put(0 ,1); int pre = 0; for (int i = 0; i < nums.length; i++) { pre += nums[i]; if (map.containsKey(pre - k)) { // if放这里没想到,先不放进去找前面的 count+=map.get(pre - k); } map.put(pre, map.getOrDefault(pre, 0) + 1); } return count; }
-
q454 四数相加Ⅱ
ab数组的和作为key,出现次数作为value,然后把cd整体与map比较,注意map集合的各种操作
2.链表操作
-
q19,遍历一遍删除链表的倒数第n个节点
要注意删除头节点的特殊情况
-
q206反转链表
最常规的方法是用三个指针的迭代法(包括head指针),用指针保存前后节点。注意一开始定义多个指针的时候防止出现空指针异常
if(head == null || head.next == null){ return head; }
这样还是不太笨了,这样就不用非空指针判断了
ListNode pre = null, p = head; while (p != null) { ListNode next = p.next; // 还是要多定义一个临时变量 p.next = pre; pre = p; p = next; } return pre;
从前往后递归,与迭代的实质是一样的:
public ListNode reverseList(ListNode head) { return reverse(null, head); } public ListNode reverse(ListNode pre, ListNode p) { if (p == null) return pre; ListNode cur = p.next; p.next = pre; return reverse(p, cur); }
从后往前递归:
public ListNode reverseList(ListNode head) { //递归出口,这个head==null是防止提供的ListNode链表是空链表,而且要写在前面,防止空指针异常 if (head == null || head.next == null){ return head; } //这个newHead至始至终都没有改变,都是结果的第一个节点 ListNode newHead = reverseList(head.next); head.next.next = head; head.next = null; return newHead; }
几个月后掌握递归思想,自己写出来的方法,当作递归函数全部处理好了,只要最后一步就行
public ListNode reverseList(ListNode head) { if (head == null || head.next == null) return head; ListNode newHead = reverseList(head.next); head.next = null; ListNode p = newHead; while (p.next != null) { p = p.next; } p.next = head; return newHead; }
-
q61旋转链表(向右平移链表)
我一开始受反转链表启发,将反转链表写成一个方法,使用平移数组的方法做,做出来了但是代码很复杂,而且容易出现空指针以及链表成环异常,看了力扣解答原来让链表成环再断开就行了。我的再多定义三个指针,只遍历一遍的方法行不行
-
q23 合并K个升序的链表
-
不断的以相邻的两个合并,for循环里合并两个
-
二路归并排序
public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } return merge(lists, 0 , lists.length - 1); } private ListNode merge(ListNode[] lists, int left, int right) { // 递归出口 if (left == right) { return lists[left]; } int mid = left + (right - left) / 2; ListNode l1 = merge(lists, left, mid); ListNode l2 = merge(lists, mid + 1, right); return mergeTwo(l1, l2); } // 正常的合并两个链表的操作,可以用正常合并和递归合并 private ListNode mergeTwo(ListNode l1, ListNode l2) { // 优化 if (l1 == null || l2 == null) { return l1 == null? l2 : l1; } ListNode head = new ListNode(); ListNode p = head; while (l1 != null && l2 != null) { if (l1.val <= l2.val) { p.next = l1; l1 = l1.next; } else { p.next = l2; l2 = l2.next; } p = p.next; } p.next = l1 == null? l2 : l1; return head.next; // 递归合并两个 if (l1 == null) { return l2; } else if (l2 == null) { return l1; } else if (l1.val < l2.val) { l1.next = mergeTwo(l1.next, l2); return l1; } else { l2.next = mergeTwo(l1, l2.next); return l2; } }
-
使用堆的方法合并(外部排序中的一种合并方法)
public ListNode mergeKLists(ListNode[] lists) { if (lists == null || lists.length == 0) { return null; } // 建立针对链表排序的堆(优先队列) PriorityQueue<ListNode> queue = new PriorityQueue<>(new Comparator<ListNode>() { @Override public int compare(ListNode o1, ListNode o2) { return o1.val - o2.val; } }); // 每个list的第一个节点入堆比较 for (ListNode list : lists) { // 之前不记得加if,[]也有长度 if (list != null) { queue.offer(list); } } // 建立新的首部节点 ListNode head = new ListNode(); ListNode p = head; while (!queue.isEmpty()) { p.next = queue.poll(); p = p.next; // 妙啊 if (p.next != null) queue.offer(p.next); } return head.next; }
以字符串数组的最后一个字符的ascii码值从大到小排序
String[] str = sc.next().split(","); Arrays.sort(str, (o1, o2) -> o2.charAt(o2.length()-1) - o1.charAt(o1.length()-1));
-
-
q142返回环形链表开始入环的第一个节点
最简单的方法就是每遍历一个节点存入hashset,重复的就是答案
如果要求O(1)空间,数学列公式推
a+n(b+c)+b=a+(n+1)b+nca+n(b+c)+b=a+(n+1)b+nc
a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
当发现slow 与fast 相遇时,我们再额外使用一个指针 ptr。起始,它指向链表头部;随后,它和slow 每次向后移动一个位置。最终,它们会在入环点相遇
// 这里不能以fast != null 为条件,不一定有环 while (fast != null) { slow = slow.next; if (fast.next != null) { fast = fast.next.next; } else { return null; } if (fast == slow) { ListNode p = head; while (p != slow) { p = p.next; slow = slow.next; } return p; } }
-
LRU缓存机制
要求自己封装一个类,需要使用到一个哈希表和一个双向链表,其实java有一个类似的数据结构就是LinkedHashMap
哈希表用来快速查找双向链表中的节点,最新插入或者查找的放在双向链表的首部,如果空间满了,则排在尾部的淘汰,用伪头部和伪尾部节点很方便
就是搞不懂这个类怎么可以定义成这样,基础不好
put我有两个方面没考虑到
- 通过key从map取值,如果新put的node存在呢
- 从list中删除节点之后,对应的map中的也要删除
public class LRUCache { class DLinkedNode { int key; int value; DLinkedNode prev; DLinkedNode next; public DLinkedNode() {} public DLinkedNode(int _key, int _value) { key = _key; value = _value; } } private int size; private int capacity; private Map<Integer, DLinkedNode> cache = new HashMap<>(); private DLinkedNode head; private DLinkedNode tail; public LRUCache(int capacity) { this.size = 0; this.capacity = capacity; head = new DLinkedNode(); tail = new DLinkedNode(); head.next = tail; tail.prev = head; } public int get(int key) { // 通过hashmap判断有没有 if (cache.containsKey(key)) { // 通过hashmap找到DLinkedNode对应节点 DLinkedNode node = cache.get(key); // 把该节点移动到首部 moveToHead(node); return node.value; } else { return -1; } } public void put(int key, int value) { DLinkedNode node = cache.get(key); if (node == null) { // 先放入hashmap中 DLinkedNode newNode = new DLinkedNode(key, value); cache.put(key, newNode); // 判断是否满了 if (size == capacity) { DLinkedNode tail = removeTail(); cache.remove(tail.key); size--; } addToHead(newNode); size++; } else { node.value = value; moveToHead(node); } } ... }
-
q148排序链表
分析:排序时间复杂度为O(nlogn)的有 快、希、归、堆,符合链表的只有归并排序,自顶而下的递归归并更简单但是空间复杂度取决于递归调用的栈空间,自底而上的递归可以做到O(1)
-
递归
先递归,后合并
奇数个节点找到中点,偶数个节点找到中心左边的节点,再从后面切断
合并使用的是先创建一个伪头节点,两个链表哪个小就接它后面
一开始看到这题的时候没有一点思路,以为归并行不通,很复杂,其实看到思想后挺简单的,自己很快写出来了标准答案,不要畏难,没有特别难的技巧
数组的归并排序反而更复杂,需要一个临时数组存储,再覆盖原数组
小顶堆也可以做,但是比较耗时和耗空间
public ListNode sortList(ListNode head) { if (head == null || head.next == null) { return head; } // 通过快慢指针找到中间点 ListNode slow = head, fast = head.next; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; } ListNode tmp = slow.next; slow.next = null; ListNode lHead = sortList(head); ListNode rHead = sortList(tmp); // 合并 ListNode nHead = merge(lHead, rHead); return nHead; } private ListNode merge(ListNode p, ListNode q) { ListNode fakeHead = new ListNode(0); ListNode temp = fakeHead; while (p != null && q != null) { if (p.val <= q.val) { temp.next = p; p = p.next; } else { temp.next = q; q = q.next; } temp = temp.next; } if (p != null) { temp.next = p; } else { temp.next = q; } return fakeHead.next; }
-
迭代是真的复杂,细节部分很难把握
使用 for (int subLength = 1; subLength < length; subLength <<= 1)循环,第一轮循环,每组一个节点,就是把链表的所有节点全部拆分,左右排序合并成两两一组,第二轮循环每组两个节点,再排序合并成四四一组…
后面再做
-
-
q160相交链表
有多种方法
-
先得到长的长度-短的长度a,长的指针再走a长度,然后一起走
-
把走过的节点存入hashset
-
A单独的长度m,B单独的长度n,共同的长度c,A走完交互到B,B走完交互到A,都走不完那么同时为空。m+c+n = n+c+m
public ListNode getIntersectionNode(ListNode headA, ListNode headB) { if(headA == null || headB == null) { return null; } ListNode p = headA, q = headB; while (p != q) { p = p == null ? headB : p.next; q = q == null ? headA : q.next; } return p; }
-
-
q234回文链表
-
如果不限定时间复杂度,可以直接把链表复制在数组中再判断
-
限定时间复杂度为O(1),先通过快慢指针找到中点,然后把后面的链表反转再比较,如果不能改变原来的结构,那么再次反转还原
public boolean isPalindrome(ListNode head) { if (head == null) { return true; } // 快慢指针找到中点 ListNode slow = head; ListNode fast = head; while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; } ListNode newHead = reverse(slow.next); ListNode p1 = head; ListNode p2 = newHead; // p2.length == p1 或 p1-1 while (p2 != null) { if (p1.val != p2.val) { return false; } p1 = p1.next; p2 = p2.next; } return true; } // 调转链表的操作方法 private ListNode reverse(ListNode head) { ListNode curr = head; ListNode prev = null; while (curr != null) { ListNode temp = curr.next; curr.next = prev; prev = curr; curr = temp; } return prev; }
-
-
剑指t35 复杂链表的复制
-
通过map把新旧结点联系起来
if (head == null) { return null; } Node node = head; Map<Node, Node> map = new HashMap<>(); while (node != null) { Node newNode = new Node(node.val); map.put(node, newNode); node = node.next; } node = head; Node newHead = map.get(head); while (node != null) { Node newNode = map.get(node); newNode.next = map.get(node.next); newNode.random = map.get(node.random); node = node.next; } return newHead;
-
方法一的递归写法
if (!map.containsKey(head)) { Node newHead = new Node(head.val); map.put(head, newHead); newHead.next = copyRandomList(head.next); newHead.random = copyRandomList(head.random); } return map.get(head);
-
方法二:连在一起再拆开
// 新建并连接 for (Node p = head; p != null; p = p.next.next) { Node node = new Node(p.val); node.next = p.next; p.next = node; } // 找random指针位置 for (Node p = head; p != null; p = p.next.next) { if (p.random != null) { p.next.random = p.random.next; } } // 分开 Node newHead = head.next; for (Node p = head, q = newHead; p != null; p = p.next, q = q.next) { p.next = q.next; if (p.next != null) { q.next = p.next.next; } } return newHead;
-
方法三
-
把原节点和节点的对应关系存入map
-
通过关系复制指针
while(cur != null) { map.get(cur).next = map.get(cur.next); map.get(cur).random = map.get(cur.random); cur = cur.next; }
-
-
-
q82 删除排序列表中的重复元素
我的错误
- 格外处理头节点,没有想到可以用dummy哑节点,把头节点统一到普通节点的处理
- 多次出现空指针异常,取值之前没有判空
public ListNode deleteDuplicates(ListNode head) { if (head ==null) return null; ListNode dummy = new ListNode(0, head); ListNode pre = dummy, p = dummy.next; while (p != null && p.next != null) { if (pre.next.val == p.next.val) { int x = p.val; // 判空检测 while (p!= null && p.val == x) { p = p.next; } pre.next = p; } else { pre = p; p = p.next; } } return dummy.next; }
-
q707 设计链表
给ListNode套一层皮,加参数,加方法
class MyLinkedList { int size; ListNode head; public MyLinkedList() { size = 0; // 虚拟头节点 head = new ListNode(0); } public int get(int index) { } }
-
q25 K个一组反转链表
-
递归
分解为更小的子问题
public ListNode reverseKGroup(ListNode head, int k) { if (head == null) return head; ListNode a = head, b = head; for (int i = 0; i < k; i++) { if (b == null) return head; b = b.next; } // 1、先反转以 head 开头的 k 个元素。 ListNode newHead = reverse(a, b); // 2、将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数。 // 3、将上述两个过程的结果连接起来。 a.next = reverseKGroup(b, k); return newHead; } public ListNode reverse(ListNode a, ListNode b) { ListNode pre, cur, nxt; pre = null; cur = a; nxt = a; // while 终止的条件改一下就行了 while (cur != b) { nxt = cur.next; cur.next = pre; pre = cur; cur = nxt; } // 返回反转后的头结点 return pre; }
-
迭代
-
-
q146 LRU缓存
如果用LinkedHashMap就很简单,但是这题本意是让你自己实现
LinkedHashMap原理是一个Map<Integer, Node>,多个Node组成一个循环链表,把Node和DoubleLink写成内部类使用
然后因为有一个哈希表一个双向链表要保持一致,所以不要在get和put方法直接操作,在两种数据结构上提供一层抽象API,供get和put主方法调用,一改具改
注意size也最好时刻保持更新,直接调用size方法时间复杂度是n,要求时间复杂度是1
做题之前先定义一个双向循环列表,再去操作会简单很多,也更清晰
抽取出来的公共方法,参数都是Node,返回值都是void
-
K个一组翻转链表
可以定义四个指针来翻转,两个在翻转部分的头和尾,一个在头的头,一个在尾的尾,再定义一个函数翻转头到尾的部分
// 开始循环 while (next != null) { // p往后移动k - 1 for (int i = 1; i < k; i++) { p = p.next; if (p == null) return dummy.next; } next = p.next; // 开始翻转 reverseK(pre, p); last.next = p; pre.next = next; // 下一轮 last = pre; pre = next; p = next; }
3.双指针
当我们需要枚举数组中的两个元素时,如果我们发现随着第一个元素的递增,第二个元素是递减的,那么就可以使用双指针的方法,将枚举的时间复杂度从 O(N^2)减少至 O(N)
-
q11 盛最多水的容器
我原以为简单方法只是固定一边往里面走,碰到更矮的跳过,原来可以两边同时往里走
第二次,我以为一边走是跟自己比,原来是跟对方比。两边一起内卷,谁矮谁先往里卷
-
q15 三数之和
用了几个小时还是有各种各样的问题…暴力法要三重循环,如果使用双指针时间复杂度可以降低到O(n²),右边的指针在第二重for循环下只要往左边移就行了(第一重for每次都要更新右边指针到最右边),只要考虑好前两个指针的遍历过程,注意一些问题:三个指针的先后顺序不能变,如果碰到两个同样的数在一起什么情况下要跳过一重for循环。
还碰到了类似于空指针异常的空数组异常,在写nums[i-1]之前必须得保证i>0,且写在它的左边
public List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums); int n = nums.length; List<List<Integer>> res = new LinkedList<>(); for (int i = 0; i < n - 2; i++) { if (nums[i] > 0) break; if (i > 0 && nums[i] == nums[i - 1]) continue; int j = i + 1; int k = n - 1; // 开启双指针 while (j < k) { if (j - 1 != i && nums[j] == nums[j - 1]) { j++; continue; } if (k < n - 1 && nums[k] == nums[k + 1]) { k--; continue; } int sum = nums[i] + nums[j] + nums[k]; if (sum < 0) j++; else if (sum > 0) k--; else { List<Integer> list = new LinkedList<>(); list.add(nums[i]); list.add(nums[j]); list.add(nums[k]); res.add(list); j++; k--; } } } return res; }
固定住第一个,然后第二三个一个left,一个right指针,while(left<right)调整,注意,得到一个符合条件的集合后,还要继续往里走
labuladong总结的模板方法,n数之和可可以通过n-1数之和求出,在只有三数之和中不建议使用
public List<List<Integer>> threeSum(int[] nums) { // 固定一个数,调用两数之和 List<List<Integer>> res = new LinkedList<>(); Arrays.sort(nums); // 固定第一个,另两个转化为两数之和 for (int i = 0; i < nums.length; i++) { List<List<Integer>> lists = twoSum(nums, i+1, -nums[i]); // 两数之和也可能有多个结果 for (List<Integer> list : lists) { list.add(nums[i]); res.add(list); } // 因为还有for,所以也可以不移动 while (i < nums.length-1 && nums[i] == nums[i+1]) i++; } return res; } public List<List<Integer>> twoSum(int[] nums, int start, int target) { List<List<Integer>> ans = new LinkedList<>(); int low = start, high = nums.length-1; while (low < high) { int left = nums[low], right = nums[high]; if (nums[low] + nums[high] < target) { // 这里很妙,至少会移动一位 while (low < high && nums[low] == left) low++; } else if (nums[low] + nums[high] > target) { while (low < high && nums[high] == right) high--; } else { List<Integer> list = new LinkedList<>(); list.add(nums[low]); list.add(nums[high]); ans.add(list); while (low < high && nums[low] == left) low++; while (low < high && nums[high] == right) high--; } } return ans; }
-
q16最接近的三树之和
与上一题类似,但是我自己写又不小心写成了三重循环,与q11也类似,暂时固定第一个,另外两个往中间走,第一个往后遍历
-
剑指t22 链表中的倒数第k个节点(注意鲁棒性)
- 链表不会空
- k不会0
- k不大于链表长度
-
找到链表中环的第一个节点
- 快慢指针先找到指针相遇的第一个节点
- 通过这个节点计算环的节点数
- 双指针中前面的指针先走环的节点数步数再一起走直到相遇
-
q141 环形链表
-
Hash表法,原来它可以直接存链表结点,不一定只能存数字,这里都用HashSet更方便,因为重复的存入就会直接false,比ArrayList少一次格外判断
-
龟兔赛跑
但是快慢指针要多注意空指针异常,不仅开始要判断,fast指针也要判断是否到了尾结点或者next指针到了尾结点
-
-
q438 找到字符串中所有字母异位词
-
滑动窗口
- 因为字符串中的字符全是小写字母,可以用长度为26的数组记录字母出现的次数
- 设n = len(s), m = len§。记录p字符串的字母频次p_cnt,和s字符串前m个字母频次s_cnt
- 若p_cnt和s_cnt相等,则找到第一个异位词索引 0
- 继续遍历s字符串索引为[m, n)的字母,在s_cnt中每次增加一个新字母,去除一个旧字母
- 判断p_cnt和s_cnt是否相等,相等则在返回值res中新增异位词索引 i - m + 1
public List<Integer> findAnagrams(String s, String p) { List<Integer> list = new ArrayList<>(); int n = s.length(); int m = p.length(); // 这种情况忘记了 if (n < m) { return list; } int[] sArr = new int[26]; int[] pArr = new int[26]; for (int i = 0; i < m; i++) { sArr[s.charAt(i) - 'a'] += 1; pArr[p.charAt(i) - 'a'] += 1; } if (Arrays.equals(sArr, pArr)) { list.add(0); } for (int i = m; i < n; i++) { sArr[s.charAt(i-m) - 'a'] -= 1; sArr[s.charAt(i) - 'a'] += 1; if (Arrays.equals(sArr, pArr)) { list.add(i-m+1); } } return list; }
-
-
q209 长度最小的子数组
涉及连续子数组的问题,我们通常有两种思路:一是滑动窗口、二是前缀和,这题用滑动窗口简单且时间复杂度低,这里用前缀和做一遍
// 目前的前缀和+target可以达到后面多少的前缀和 for (int i = 0; i < n; i++) { int s = target + sums[i]; int bound = Arrays.binarySearch(sums, s); if (bound < 0) { bound = -bound - 1; } if (bound <= n) { instance = Math.min(instance, bound - i); } } return instance == Integer.MAX_VALUE ? 0 : instance;
4.字符串操作
-
q14 最长公共前缀
没经过系统训练的一般都是选择纵向的暴力破解,一个个对比,但是我的暴力破解虽然成功了但是太复杂,后面的只要跟第一个字符串的对应字符相等就行了。而且第一个for循环条件结束条件可以暂时以第一个字符串长度为准,后面进入的时候在判断有没有超过此时比较的字符串长度
很多测试用例没有考虑到
- 非空判断
- 数组里面只有一个元素的判断
- 其它情况的判断
字符串的使用还是很多巧方法,比如startsWith,substring
public String longestCommonPrefix(String[] strs) { if (strs == null || strs.length == 0) { return ""; } int length = strs[0].length(); int count = strs.length; for (int i = 0; i < length; i++) { char c = strs[0].charAt(i); for (int j = 1; j < count; j++) { if (i == strs[j].length() || strs[j].charAt(i) != c) { return strs[0].substring(0, i); } } } return strs[0]; }
自己想到的方法也行
public String longestCommonPrefix(String[] strs) { if (strs.length == 0) return ""; String prefix = strs[0]; for (int i = 1; i < strs.length; i++) { // 开始比较 StringBuffer newPrefix = new StringBuffer(); for (int j = 0; j < prefix.length() && j < strs[i].length(); j++) { if (prefix.charAt(j) == strs[i].charAt(j)) { newPrefix.append(prefix.charAt(j)); } else break; } prefix = newPrefix.toString(); } return prefix; }
-
q763 划分字母区间
里面有一个思想需要着重学习,如果已经知道是字母或数字这种定长结构,可以不用申请Map这种类型,直接使用int数组: arr[S.charAt(i) - ‘a’],这样arr[0]对应a…
先得到对应字母出现的最后一个位置的int数组,在判断当地扫描到位置的字母最后出现位置是不是比当前的更大,如果是的话就更新扩大while循环的结束条件
int i=0,k; while (i < S.length()) { k = S.charAt(i)-'a'; int cur = i; while (i<=last[k]){ if(last[S.charAt(i)-'a']>last[k]){ k = S.charAt(i)-'a'; } i++; } list.add(i - cur); }
代码随想录代码,更容易理解
public List<Integer> partitionLabels(String s) { List<Integer> list = new ArrayList<>(); // 记录每个字母最后出现的位置 int[] edge = new int[26]; char[] ch = s.toCharArray(); for (int i = 0; i < ch.length; i++) { edge[ch[i] - 'a'] = i; } int end = 0; int start = -1; for (int i = 0; i < ch.length; i++) { end = Math.max(end, edge[ch[i] - 'a']); if (end == i) { list.add(end - start); start = end; } } return list; }
-
q6 Z字型变换
最简洁的解法实在是太脑筋急转弯了,没看过的怎么可能想得到,让flag参与运算,这种不知道numRows的不能定义多个list,list和stringbuilder的组合是最好的
public String convert(String s, int numRows) { if(numRows < 2) return s; List<StringBuilder> rows = new ArrayList<StringBuilder>(); for(int i = 0; i < numRows; i++){ rows.add(new StringBuilder()); } int i = 0, flag = -1; for(char c : s.toCharArray()) { rows.get(i).append(c); if(i == 0 || i == numRows -1) flag = - flag; i += flag; } StringBuilder res = new StringBuilder(); for(StringBuilder row : rows) res.append(row); return res.toString(); }
我一开始不想定义Arraylist,想使用StringBuider数组,结果发现数组初始值为null的没法使用.append方法,改为String数组,但是第一轮要先赋值为空字符串,不然使用“”+c的话第一位是null值,报错。这个可以成功但是String数组增减的开销太大了,还不如list
-
剑指t17 打印从1到最大的n位数
- n可能很大,数字类型会移除,改用字符串,大数问题
- 优化快速判断是否到最大位,用O(1)方法,优化输出代码
- 输出也要注意,为了用户体验,前面的零不用输出
- 全排列(有疑问)
-
q3 无重复字符的最长子串
可以用set和map
//在滑动窗口中,i是开头,end是结尾 for (int i = 0; i < len; i++) { while (end<len && !occ.contains(s.charAt(end))){ occ.add(s.charAt(end)); if((end-i+1)>max){ max = end-i+1; } end++; } occ.remove(s.charAt(i)); }
犯错
- 之前忽略了end是不会往前走的,全清空set复杂度高
- while中长度判断应该放前面防止越界,第二次又犯了
不如套模板
public int lengthOfLongestSubstring(String s) { Map<Character, Integer> map = new HashMap<>(); int left = 0, right = 0; int res = 0; // 两个while,且里面对称 while (right < s.length()) { char c = s.charAt(right); map.put(c, map.getOrDefault(c, 0) + 1); right++; while (map.get(c) > 1) { char d = s.charAt(left); map.put(d, map.get(d) - 1); left++; } res = Math.max(res, right - left); } return res; }
-
q394字符串编码
-
栈操作,细节很多,但是想明白后不难。遇到 ‘[’ 入栈,遇到 ‘]’ 开始把这一段进行解码,详细看注释
总结:
- StringBuilder可以方便的操作单个字符,不需要再定义大的集合
- Character.isLetter判断是否为字符的api
public String decodeString(String s) { // 先扫描一遍,除']'以外全部入栈 Stack<Character> stack = new Stack<>(); for (char c : s.toCharArray()) { if (c != ']') { stack.push(c); } else { //找字母位 StringBuilder sb = new StringBuilder(); while (!stack.isEmpty() && Character.isLetter(stack.peek())) { sb.insert(0, stack.pop()); } stack.pop(); // 去除'[' String sub = sb.toString(); //找数字位 sb = new StringBuilder(); while (!stack.isEmpty() && Character.isDigit(stack.peek())) { sb.insert(0, stack.pop()); } // StringBuild => String => Integer int count = Integer.valueOf(sb.toString()); while (count > 0) { for (char ch : sub.toCharArray()) { stack.push(ch); } count--; } } } // 把栈里所有字母的转化为String StringBuilder retv = new StringBuilder(); while (!stack.isEmpty()) { retv.insert(0, stack.pop()); } return retv.toString(); }
以上是国际版高赞代码,确实比中文网官方解析更好理解
也可以用一个数字栈,一个字符串栈(包括字母和括号),遇到右括号时弹出一个数字栈,字符串栈弹出到左括号为止,解码后再入字符串栈,以下是自己写的
public String decodeString(String s) { // 建立两个栈,一个存次数,一个存字符和左括号 Deque<Integer> counts = new LinkedList<>(); Deque<Character> strs = new LinkedList<>(); int i = 0; while (i < s.length()) { char c = s.charAt(i); if ((c - 'a' >= 0 && c - 'a' <=26) || c == '[') { strs.push(c); i++; } else if (c == ']') { StringBuffer sb = new StringBuffer(); while (strs.peek() != '[') { sb.append(strs.pop()); } int time = counts.pop(); sb.reverse(); strs.pop(); // StringBuffer可以append char类型的 StringBuffer str = new StringBuffer(); while (time > 0) { str.append(sb); time--; } for (char d : str.toString().toCharArray()) { strs.push(d); } i++; } else { int v = s.charAt(i) - '0'; i++; while (i < s.length() && s.charAt(i) - '0' >= 0 && s.charAt(i) - '0' <=9) { v = v * 10 + (s.charAt(i) - '0'); i++; } counts.push(v); } } StringBuffer res = new StringBuffer(); while (!strs.isEmpty()) { res.append(strs.pop()); } return res.reverse().toString(); }
-
-
q647 回文子串
从中间向两边遍历,判断符合条件的数量,奇数和偶数都要考虑
public int countSubstrings(String s) { // 大小从1开始,位置从1开始 int n = s.length(); int res = 0; // i是中间节点 for (int i = 0; i < n; i++) { // 奇数和偶数,以下两个可以合并为一个for循环 int left = i, right = i; while (left >= 0 && right < n && s.charAt(left--) == s.charAt(right++)) res++; left = i; right = i + 1; while (left >= 0 && right < n && s.charAt(left--) == s.charAt(right++)) res++; } return res; }
不建议把判断回文串单独抽出来定义一个函数,string的切割非常耗时
我第二遍时间复杂度达到了n三次方,不必要,中心扩散就行了。
-
剑指t45 把数组排成最小的数
先把数字转为字符串数组,再使用java的campareTo方法和快排对字符串数组进行排序
public String minNumber(int[] nums) { // 先把nums数组转化为string数组 String[] strs = new String[nums.length]; for (int i = 0; i < nums.length; i++) { strs[i] = String.valueOf(nums[i]); } quickSort(strs, 0, nums.length-1); // 不建议用String拼接,要全部重新复制,速度慢 StringBuilder res = new StringBuilder(); for (String str : strs) { res.append(str); } return res.toString(); } // 快排的标准写法 private void quickSort(String[] strs, int low, int high) { if (low < high) { // 每一轮快排后确定一个数的位置 int middle = getMiddle(strs, low, high); quickSort(strs, low, middle - 1); quickSort(strs, middle + 1, high); } } private int getMiddle(String[] strs, int i, int j) { // 选取左边界作为枢轴 String tmp = strs[i]; while (i < j) { while (i < j && (strs[j] + tmp).compareTo(tmp + strs[j]) >= 0) j--; strs[i] = strs[j]; while (i < j && (strs[i] + tmp).compareTo(tmp + strs[i]) <= 0) i++; strs[j] = strs[i]; } strs[i] = tmp; return i; }
-
剑指t67 把字符串转换为整数
public int strToInt(String str) { // 排除前面的空字符 char[] c = str.trim().toCharArray(); if (c.length == 0) return 0; int res = 0, sign = 1, start = 0, border = Integer.MAX_VALUE / 10; if (c[0] == '-' || c[0] == '+') { sign = c[0] == '-' ? -1 : 1; start++; } for (int i = start; i < c.length; i++) { if (c[i] < '0' || c[i] > '9') break; // 两种越界的条件 if (res > border || res ==border && c[i] > '7') { // 如果是'8',且为负数的情况下没有越界,不过结果还是负最大值,正数确实越界了,所以直接截断为正最大值 return sign == -1 ? Integer.MIN_VALUE : Integer.MAX_VALUE; } res = res * 10 + (c[i] - '0'); } return sign * res; }
-
q32 最长有效括号
之前以为只要配对就行,其实是要最长连续
-
动态规划
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tvxFB4Ho-1657459118120)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211202114153494.png)]
int[] dp = new int[s.length()]; int max = 0; for (int i = 1; i < s.length(); i++) { if (s.charAt(i) == ')') { if (s.charAt(i-1) == '(') { dp[i] = dp[i-1] + 2; } if (i-dp[i-1] > 0 && s.charAt(i-dp[i-1]-1) == '(') { dp[i] = dp[i-1] + ((i - dp[i-1]) >= 2 ? dp[i - dp[i-1] - 2] : 0) + 2; } } max = Math.max(max, dp[i]); } return max;
-
前后扫描(很巧,想不到)
int left = 0, right = 0, max = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') left++; else right++; if (right == left) { max = Math.max(max, left * 2); } else if (right > left) { left = right = 0; } } left = right = 0; for (int i = s.length()-1; i > 0; i--) { if (s.charAt(i) == ')') right++; else left++; if (left == right) { max = Math.max(max, left * 2); } else if (left > right) { left = right = 0; } } return max;
-
使用栈,反常规思维使用栈
// 使用栈 int max = 0; Deque<Integer> stack = new LinkedList<>(); // 保证栈的最低层为最后一个未匹配到的')'的下标 stack.push(-1); for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { stack.push(i); } else { stack.pop(); // 先弹出z if (stack.isEmpty()) { stack.push(i); } else { max = Math.max(max, i - stack.peek()); } } } return max;
-
-
q76 最小覆盖子串
比较目标字符串和双指针中间同样字符的字符串有两种方法
- 两个都放入map中,因为map是无序的,所以得格外定义函数比较双指针中间的map字符个数是否>=目标map
- 通过int数组,字母对应下标,次数对应值,length>=目标的length就行
class Solution { Map<Character, Integer> targets = new HashMap<>(); Map<Character, Integer> currents = new HashMap<>(); public String minWindow(String s, String t) { // 把t中的所有存入hash表 for (int i = 0; i < t.length(); i++) { char c = t.charAt(i); targets.put(c, targets.getOrDefault(c, 0) + 1); } int sLen = s.length(), len = Integer.MAX_VALUE; int l = 0, r = -1; int ansL = -1, ansR = -1; while (r < sLen) { r++; // 添加进currents // while循环里面,如果里面有操作要改动循环里的条件,那么还需要再次判断在循环中 if (r < sLen && targets.containsKey(s.charAt(r))) { currents.put(s.charAt(r), currents.getOrDefault(s.charAt(r), 0) + 1); } while (fit() && l <= r) { if (r - l + 1< len) { len = r - l + 1; ansL = l; ansR = l + len; } // 尝试从左边删除,不能全部删除,必须是目标字符且次数只能一个一个降! if (targets.containsKey(s.charAt(l))) { currents.put(s.charAt(l), currents.getOrDefault(s.charAt(l), 0) - 1); } l++; } } return ansL == -1 ? "" : s.substring(ansL, ansR); } private boolean fit() { // 下面改为增强for循环更简洁 // for (Map.Entry<Character, Integer> entry : targets.entrySet()) { Iterator<Map.Entry<Character, Integer>> iter = targets.entrySet().iterator(); while (iter.hasNext()) { Map.Entry<Character, Integer> entry = iter.next(); Character key = entry.getKey(); Integer value = entry.getValue(); if (!currents.containsKey(key) || currents.get(key) < value) { return false; } } return true; } } // 套用滑动窗口模板 public String minWindow(String s, String t) { Map<Character, Integer> need = new HashMap<>(); Map<Character, Integer> window = new HashMap<>(); for (char c : t.toCharArray()) { need.put(c, need.getOrDefault(c, 0)+1); } int start = 0, len = Integer.MAX_VALUE; int valid = 0; // valid 变量表示窗口中满足 need 条件的字符个数,如果 valid 和 need.size 的大小相同,则说明窗口已满足条件,已经完全覆盖了串 T。也可以格外定义一个函数判断,这种方法背一下 int left = 0, right = 0; while (right < s.length()) { char c = s.charAt(right); right++; if (need.containsKey(c)) { window.put(c, window.getOrDefault(c, 0)+1); // 字符的比较也用equals,如果用==,长字符串测试用例通不过 if (window.get(c).equals(need.get(c))) valid++; } while (valid == need.size()) { if (right-left < len) { start = left; len = right-left; } char d = s.charAt(left); left++; if (need.containsKey(d)) { if (window.get(d).equals(need.get(d))) valid--; window.put(d, window.get(d)-1);// 与上面对称 } } } return len == Integer.MAX_VALUE ? "" : s.substring(start, start+len); }
-
q151 翻转字符串里的单词
分三步走,定义三个方法,后两个直接在自己身上操作,不用再拼接了
- 把每个多余的空格去掉
- 全部翻转一下
- 每个单词单个翻转一下
-
q1190 反转每对括号间的子串
我用stack,一个个模拟到char,时间复杂度太高了,应该string类型的栈和stringbuffer一起配合的。然后主要是字符串的各种处理,要熟悉Deque和stringbuffer的各种操作,注意Deque的push和pop操作都是在last这边进行操作的
-
q2024 考试的最大困扰度
我自己写的滑动窗口太复杂了,正向思维,统计的是本次应该统计的最长字符类型,实际上统计另一个类型更好写,总数超过了滑动左边就行了
-
华为机试 蛇形字符串
我有几个地方思想不行,所以这么费时
- 尽量不要在中间加flag判断,有没有其他替代的判断是否有改变的判断方法
- 数据预处理方面做的不好,BW和SW同一索引都保留min值就行了,达到统一,not found的情况也可以一开始就判断好
- 要多利用库,我用O(n²)的时间复杂度从长到短找,何必呢,全放到List中sort一下就行了
public static void main(String[] args) throws IOException { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { String str = sc.next(); int[] BW = new int[26]; int[] SW = new int[26]; // 也可以大小写全都存在的才++,结束条件是全都为0,更简单 for (char c : str.toCharArray()) { if (c - 'A' >= 0 && c - 'A' < 26) { BW[c-'A']++; } else if (c - 'a' >= 0 && c - 'a' < 26) { SW[c-'a']++; } } boolean flag = true; int W[] = Arrays.copyOf(BW, 26); while (flag) { int index = 0; boolean count = false; StringBuffer sb = new StringBuffer(); // 找连续最长的 int start = 0, finalStart = 0; int maxLen = 0; while (index < 26) { if (BW[index] > 0 && SW[index] > 0) { start = index; count = true; while (index < 26 && BW[index] > 0 && SW[index] > 0) index++; if (index - start > maxLen) { maxLen = index - start; finalStart = start; } } else index++; } // 从finalStart开始添加到sb中 if (maxLen != 0) { for (int i = finalStart; i < finalStart + maxLen; i++) { sb.append((char)(i+65)).append((char)(i+97)); BW[i]--; SW[i]--; } System.out.println(sb); } // 至始至终没有改变 if (Arrays.equals(W, BW)) { System.out.println("Not Found"); flag = false; break; } // 最终没有蛇形字符对了 if (count == false) { flag = false; break; } } } }
5.数字操作
-
q7 整数反转(我的弱项)
要有一些前置知识,java类型的int是32位的,负数可以比正数多显示一位。要用到Interger.MAX_VALUE = 2147483647
每一步都要判断是不是超过限制大小了。中间不要保存成数组形式,直接rev = rev * 10 + pop
如果可以用long,就不用这么麻烦了,因为反正也不会溢出,可以得出结果后再与Interger.MAX_VALUE比较
-
q9回文数
要精通数字的反转代码,并且要了解所有的特殊情况
特殊情况:最后一个数字是0,是奇数还是偶数,负数,反转过程中超过int最大值,只有一个个位数
这题是反转一半
while (x>revertedNumber){ int k = x % 10; revertedNumber = revertedNumber *10 + k; x /= 10; }
-
剑指t16 数值的整数次方
- 自以为题目简单的解法,没考虑到很多特殊情况
- 考虑特殊情况,异常传递到全局变量
- 递归,16次方可认为是8次方再开方
-
q31下一个排列
-
从后往前遍历,找到第一个非降序的数i
-
判断i是否>0,否则直接执行5(我这里处理出错)
-
从后往前遍历,找到第一个大于i的数j
-
交换i,j
-
反转i+1之后的数
-
代码优化
while (i>=0){ if(nums[i+1] > nums[i]){ break; } i--; } // 可以优化为 while (i>=0 && nums[i] >= nums[i + 1]){ i--; }
-
-
q33搜索旋转排序数组
要使事件复杂度为O(log n),就是用二分查找,二分后有一半是有序的,要注意边界问题
代码:
public int search(int[] nums, int target) { int n = nums.length; int l = 0, r = n - 1; if(n == 0){ return -1; } if(n == 1){ return nums[0]==target?0:-1; } // 注意边界条件,什么时候小于,什么时候小于等于 while (l <= r){ int mid = (l+r)/2; if(nums[mid] == target){ return mid; } // 如果左边是顺序的;必须加上等于,因为在只有两个数的情况下,0==mid if(nums[0]<=nums[mid]){ // 这个等号必须加,比如数组[3,1]找1 // 这里是左闭右开 if(nums[l]<=target && target<nums[mid]){ r = mid-1; } else { l = mid+1; // 大胆的+1,因为如果mid就是答案,已经return返回结果了 } } // 如果右边是顺序的,从右边开始排查 else { if(nums[mid]<target && target<=nums[n-1]){ l = mid+1; } else { r = mid-1; } } } return -1; }
总结:
- 求中间数,下标一般是(length-1)/2,在偶数情况下习惯前半部分吃点亏,但是如果只分左右的话,一般中间数会分给左边
- 左右指针循环退出条件是left<=right
- 注意什么时候小于,什么时候小于等于,不能放过任何一种情况
- 这个题其实就是复杂一点的二分查找,if else里面再套个if else
-
q34 在排序数组中查找元素的第一个和最后一个位置
同样是用二分法,用改编的二分查找第一个大于target的元素下标
int leftIdx = binarySearch(nums, target-1); int rightIdx = binarySearch(nums, target)-1;
// 二分查找,找到第一个大于target的数字下标 public int binarySearch(int[] nums, int target){ int left = 0, right = nums.length -1, ans = nums.length; //ans = nums.length防止不经过ans=mid while (left <= right){ int mid = (left+right)/2; // 为什么没有等于,因为是找大于的,所以等于放在下面的else里面 if(nums[mid]>target){ right = mid-1; ans = mid; } else { left = mid + 1; } } return ans; }
可以不用ans,以后用这种模板
private int getIndex(int[] nums, int target) { int low = 0, high = nums.length - 1; while (low <= high) { int mid = low + (high - low) / 2; if (nums[mid] > target) { high = mid - 1; } else { // 如果等于target,那么是low是mid+1 low = mid + 1; } } return low; }
-
q48旋转图像
要求是原地旋转,不能返回临时数组
最简单的就是把临时数组覆盖原数组,但是肯定不好
最好的解法就是,先水平轴旋转,再主对角线旋转,或者先主对角线,再竖直轴旋转(过程中别全部都旋转一遍,那样值不会变)
-
q75颜色分类
要求原地且一遍得到结果,是快排的特殊情况,1是中间数,如果没有要求只遍历一遍,直接先找到1作为哨兵。
总结写快速排序:选取一个flag,每次遍历一遍后,比flag小的都在它的左边,比flag大的都在它的右边,flag位于自己的最终位置。左右再分治递归得到结果
-
循环不变量的概念,找到这题的循环不变量,最好写作注释
//all in [p0,i) = 0 //all in [i,p2) = 1 //all in [p2,len-1] = 2
-
单指针要遍历两遍,双指针可以只一遍
while (i <= p2) { // 必须加上等于 if (nums[i] == 0) { //交换过来的不可能是0 swap(nums, i++, p0); p0++; } else if (nums[i] == 1) { //1不用管 i++; } else if (nums[i] == 2) { //交换过来的可能是2,所以i不能加一 swap(nums, i, p2); p2--; } }
-
-
q128最长连续序列
要求线性时间复杂度,这里必须记下结论,自己想很难想
- 必须先把原数组放入hashset中,顺便排重
- 从头开始判断,拿到每一位都要不断再往比它大一位的直到末尾再遍历一次是否存在,存在则数字加一
- 由于内遍历还是O(n²),所以要在外遍历中间再判断,只有当前数的小一位存在才进入内遍历
public int longestConsecutive(int[] nums) { // 存入hashset Set<Integer> num_set = new HashSet<>(); for (int num : nums) { num_set.add(num); } int longestStreak = 0; for (int num : num_set) { // 这里不要遍历原数组,有很多重复元素,遍历set集合就行了 if (!num_set.contains(num-1)) { int currentStreak = num; int currentNum = 1; while (num_set.contains(currentStreak + 1)) { currentStreak+=1; currentNum+=1; } longestStreak = Math.max(currentNum, longestStreak); } } return longestStreak; }
-
q169多数元素
-
哈希表存次数
-
先排序,最后中位数元素是结果
-
随机化,每次随机在中间抽取一个数,遍历一遍是否count占据长度的一半以上
-
分治,自顶而下求众数
-
投票算法,之前有一点印象。可以证明
投票算法证明:
- 如果候选人不是maj 则 maj,会和其他非候选人一起反对 会反对候选人,所以候选人一定会下台(maj==0时发生换届选举)
- 如果候选人是maj , 则maj 会支持自己,其他候选人会反对,同样因为maj 票数超过一半,所以maj 一定会成功当选
nums: [7, 7, 5, 7, 5, 1 | 5, 7 | 5, 5, 7, 7 | 7, 7, 7, 7]
candidate: 7 7 7 7 7 7 5 5 5 5 5 5 7 7 7 7
count: 1 2 1 2 1 0 1 0 1 2 1 0 1 2 3 4if (nums[i] == candidate) { count++; } else { count--; if (count < 0) { candidate = nums[i]; count = 1; } }
优化代码为
if (count == 0) { candidate = nums[i]; } count += nums[i] == candidate ? 1 : -1;
-
-
q202 快乐数
只有两种情况,一:是快乐数;二:无限循环;第三种情况:无穷大 不可能,因为各位的平方和会降级
- 使用set集合,判断是否到了循环
- 龟兔赛跑,getNext函数得到n的各位的平方和
-
q50 快速幂,第二遍了
主要考虑越界问题
-
递归法
n要转化为long类型,不然通不过MIN的测试用例
public double myPow(double x, int n) { long N = n; return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N); } public double quickMul(double x, long n) { if (n == 0) return 1.0; double temp = quickMul(x, n/2); return n%2 == 1 ? x * temp * temp : temp * temp; }
-
迭代法
public double myPow(double x, int n) { if (x == 0.0f) return 0.0d; long N = n; double res = 1.0; if (N < 0) { x = 1/x; N = -N; } while (N > 0) { if ((N&1) == 1) res *= x; x *= x; N >>= 1; } return res; }
-
-
q532 数组中的k-diff数对
自己只想到暴力,看提示想到了排序加二分,时间复杂度最低的还是借助Set集合
但是不知道如何借助Set,需要两个,一个去重,一个遍历,在同一个for循环里,这里有一个思想,边遍历遍加入Set集合,后面的先不用管,而不是先使用一个遍历然后全部加入Set集合
6.数组操作
-
q238除自身以外数组的乘积
要求不能用除法,且要在O(n)内完成,用最低空间复杂度
定义两个数组,一个是当前索引左边所有数字的乘积,一个是右边所有数字的乘积,两个数组的特点是不包括当前点的乘积,端点值为1,结果数组是两个数组相乘,这个结论直接记住吧
第二个数组可以在第一个数组的位置上操作
public int[] productExceptSelf(int[] nums) { int len = nums.length; if (len == 0) { return null; } int res[] = new int[len]; res[0] = 1; for (int i = 1; i < len; i++) { res[i] = res[i-1] * nums[i-1]; } int R = 1; for (int i = len - 1; i >= 0; i--) { res[i] = res[i] * R; R *= nums[i]; } return res; }
-
q240搜索二维矩阵
-
对每一行都使用二分查找
for (int[] row : matrix)
二分查找的循环条件是while (left <= right)
-
Z字形
从右上角开始,大了往左移,小了往右移
-
-
q4寻找两个有序数组的中位数
-
分析
先排序再顺序查找,事件复杂度O(m+n),要求log(m+n),那只能使用二分,一小半一小半的排除
-
步骤
- 找到假如两个排序后,中位数应该是第k小(偶数是中间两个数取平均),定义一个函数专门找两个数组中的第k小的数
- 比较两个数组中滑动窗口序号最后一个,每次排除小的那个的k/2个数,k刷新
-
总结
- 注意边界条件,k=1或者序号到了最后,可以直接得到答案
- 以后这种排除一半的,一般是k/2-1,在排除尽可能多的情况下保证安全
- 最后答案是int计算成double类型,最后除2.0
-
-
q56合并区间
这题好多东西都要学
-
二维数组排序,使用重写Comparator接口的方法
-
普通方法
//先把二维数组按照左边界大小排序 for (int i = 0; i < intervals.length-1; i++) { if(intervals[i][0]>intervals[i+1][0]){ for (int j = 0; j < 2; j++) { } } }
-
好的方法
Arrays.sort(intervals, new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { return o1[0]-o2[0]; } }); ambda表达式 Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0]));
中括号里的0代表第0列,左减右代表升序,没有中括号的是普通一维数组排序
-
-
数组与集合之间的转换
-
数组转集合最好for循环一个个add(asList方法如果是数组必须得是引用类型)
-
集合转数组可以用.toArray方法,带上需要转化为的参数(不带参数是Object类型)
-
如果有数组来接,那么arrays初始化的大小随意,如果直接内部转化,那么一定要初始化为list.size(),否则数组长度会延申,后面会补零,详细见以下源码
public <T> T[] toArray(T[] a) { if (a.length < size) // Make a new array of a's runtime type, but my contents: return (T[]) Arrays.copyOf(elementData, size, a.getClass()); System.arraycopy(elementData, 0, a, 0, size); if (a.length > size) a[size] = null; return a; }
-
int[] nums = new int[3]; List<Integer> list = new ArrayList<Integer>(){}; list.add(1); list.add(2); list.add(3); // Integer[] arrays = new Integer[0]; // Integer[] we = list.toArray(arrays); // System.out.println(we[2]); Integer[] arrays = new Integer[list.size()]; list.toArray(arrays); System.out.println(arrays[3]);
- 不能将Object[] 转化为String[],转化的话只能是取出每一个元素再转化。java中的强制类型转换只是针对单个对象的,想要偷懒将整个数组转换成另外一种类型的数组是不行的,这和数组初始化时需要一个个来也是类似的。
-
-
代码
没有想到,可以先把暂时的结果放入list集合中,再从顶开始比较,list集合操作不熟悉,get可以根据索引值直接修改
public int[][] merge(int[][] intervals) { Arrays.sort(intervals, (o1, o2) -> o1[0] - o2[0]); int len = intervals.length; LinkedList<int[]> list = new LinkedList<>(); for (int i = 0; i < len; i++) { int L = intervals[i][0]; int R = intervals[i][1]; if (list.size() == 0 || list.get(list.size()-1)[1] < L) { list.add(new int[]{L, R}); } else { // 不用删除再添加,直接修改就行 list.get(list.size()-1)[1] = Math.max(list.get(list.size()-1)[1], R); } } return list.toArray(new int[list.size()][]); }
我自己写的很烂
public int[][] merge(int[][] intervals) { if (intervals.length == 0) { return new int[0][0]; } Arrays.sort(intervals, (o1, o2) -> (o1[0] - o2[0])); LinkedList<int[]> list = new LinkedList<>(); list.add(new int[]{intervals[0][0], intervals[0][1]}); for (int i = 1; i < intervals.length; i++) { int n = list.size(); if (intervals[i][0] <= list.get(n-1)[1]) { // list修改值可以直接改的,直接用set方法也行 int[] arr = list.get(n - 1); arr[1] = Math.max(arr[1], intervals[i][1]); list.removeLast(); list.add(arr); } else { // intervals[i]多次使用可以直接在本次循环里定义为常数的 list.add(new int[]{intervals[i][0], intervals[i][1]}); } } return list.toArray(new int[list.size()][]); }
-
-
q283移动零
简单题都不会做了,就是双指针,两个指针都从0位置开始,右指针负责找非零的数,左指针负责指向固定顺序的最后一个位置
public void moveZeroes(int[] nums) { int i = 0, j = 0; while (j < nums.length) { if (nums[j] != 0) { swap(nums, i, j); i++; } j++; } }
-
q287寻找重复数组
-
我的方法就是创建一个同样长度的数组,遍历一个值,把以它为数组索引的值赋1,如果再次找到这个索引值,发现不为0了就是答案。但是这个不是常数量级的空间
public int findDuplicate(int[] nums) { int n = nums.length; int[] ind = new int[n]; for (int i = 0; i < n; i++) { if (ind[nums[i]-1] != 0) { return nums[i]; } ind[nums[i]-1] = 1; } return 0; }
-
快慢指针
对nums 数组建图,每个位置 i 连一条 i→nums[i] 的边。由于存在的重复的数字target,因此target 这个位置一定有起码两条指向它的值的边,因此整张图一定存在环,然后用之前环形链表的方法找到环的入口
-
二分法
暂时不太理解,当它就是一个排好序的数组, 直接取left = 1, right = len - 1,然后算mid,其结果相当于排次序再算mid?
-
-
q406根据身高重建队列
先安装从矮到高排序,如果一样高,就按照位置大到小排序,因为高的总是忽略矮的,矮的先排好不影响高的"大于等于"位置,一样高的情况下,先排"大于等于"值大的,这样也不会影响该值小的,否则每次还要判断大小,很麻烦
根据spaces值和person[1]值判断放置在二维数组中的位置
public int[][] reconstructQueue(int[][] people) { // 先排序 Arrays.sort(people, (person1, person2) -> { // 如果两个相同,先把后面的确定,前面才不会受影响 if (person1[0] == person2[0]) { return person2[1] - person1[1]; } else { return person1[0] - person2[0]; } }); // 申请数组 int n = people.length; int[][] res = new int[n][]; for (int[] person : people) { int spaces = person[1] + 1; for (int i = 0; i < n; i++) { if (res[i] == null) { spaces--; } if (spaces == 0) { res[i] = person; break; } } } return res; }
从高往矮排,先把高的相对位置确定,次矮的往里面插,最后插矮的。通过第二位(索引)
public int[][] reconstructQueue(int[][] people) { Arrays.sort(people, (o1, o2) -> { if (o1[0] != o2[0]) { return o2[0] - o1[0]; } else return o1[1] - o2[1]; }); List<int[]> list = new ArrayList<>(); for (int[] person : people) { list.add(person[1], person); } // list类型数组转为二维数组的方式,死背住 return list.toArray(new int[list.size()][]); }
-
q448 找到所有数组中消失的数字
由于要求要原地,那么就只能利用原数组,最后看要求是否要还原
这种思路学习一下,遍历一遍数组,把每个值作为下标找到位置后加n,一遍过后,没消失的数字下标位都超过了n,没有超过的下标就是答案,注意下标计算是:int x = (nums[i] - 1) % n;
-
q494 目标和
-
暴力回溯,每个元素的正负都来一遍
int count = 0; public int findTargetSumWays(int[] nums, int target) { // 暴力回溯 dfs(nums, target, 0, 0); return count; } // 这里类似于二叉树的遍历代码,为什么没有for呢,其实可以有,for是在选择集合里选择,这里的是+还是-就是选择,写了两个dfs,相当于for (int i = 0; i < 2; i++) {if (i == 0, "+") if (i == 1, "-")} private void dfs(int[] nums, int target, int index, int sum) { if (index == nums.length) { if (sum == target) { count++; } } dfs(nums, target, index+1, sum - nums[index]); dfs(nums, target, index+1, sum + nums[index]); }
-
转化为背包问题
添加 - 号的元素之和为neg => (sum−neg)−neg=sum−2⋅neg=target
neg=(sum−target)/2
行为数组中的值,列为组合成的数,最大到neg
边界条件:dp[0] [0] = 1
我的三个问题见如下注解
public int findTargetSumWays(int[] nums, int target) { int sum = 0; for (int i = 0; i < nums.length; i++) { sum += nums[i]; } int dif = sum - target; // 1.忘记加上这个判断 if (dif < 0 || dif % 2 == 1){ return 0; } int neg = dif / 2; int[][] dp = new int[nums.length + 1][neg + 1]; // 边界条件 dp[0][0] = 1; for (int i = 1; i <= nums.length; i++) { for (int j = 0; j <= neg; j++) { // 从0开始 // 2.忽略了 -1 if (nums[i-1] > j) { // 3.不记得+= dp[i][j] += dp[i-1][j]; // 不用这个物体 } else { dp[i][j] += dp[i-1][j] + dp[i-1][j-nums[i-1]]; // 用与不同总共 } } } return dp[nums.length][neg]; }
两种状态,背包容量和物体数量,两种选择,选或不选合二为一
优化为一维数组
// 边界条件 dp[0] = 1; for (int num : nums) { // 从后往前遍历,因为后面的要用前面的推算,防止前面的先改变 // 是>=num ,不是>0 for (int j = neg; j >= num; j--) { dp[j] += dp[j-num]; } } return dp[neg];
-
-
q581 最短无序连续子数组
-
先排序好数组,再比较中间哪些地方要排序
-
双指针
从右到左找left,小于min就更新min,大于min就标记为left,最后一个left就是真正的left,找right同理
-
分三部分
-
-
q739每日温度
-
暴力,详细见注释
public int[] dailyTemperatures(int[] temperatures) { // next[]:每个温度第一次出现的下标,下标代表温度,值代表第一次出现的下标 // index:比它温度高且离它最近下标; int n = temperatures.length; int[] next = new int[101]; int[] ans = new int[n]; Arrays.fill(next, Integer.MAX_VALUE); // 从后往前遍历 for (int i = n - 1; i >= 0; i--) { // 找到next中它后面的元素第一个比他大的,如temperatures[i] == 76°,从77°找起 // 找到所有比他大的,然后再从中找一个离它最近的 int index = Integer.MAX_VALUE; for (int j = temperatures[i] + 1; j < 101; j++) { if (next[j] < index) { index = next[j]; } } // 之前忘记加上这个如果后面没有比高的温度的情况了 if (index < Integer.MAX_VALUE) { ans[i] = index - i; } next[temperatures[i]] = i; } return ans; }
-
单调栈,保证栈里面的数是单调递减, 如果想入栈的元素比栈顶元素大,那么就把比他小的元素全部出栈,并计算距离得到结果
public int[] dailyTemperatures(int[] temperatures) { int n = temperatures.length; int[] ans = new int[n]; Deque<Integer> stack = new LinkedList<>(); for (int i = 0; i < n; i++) { // 比较遍历到的元素和栈顶元素的大小 while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) { int pop = stack.pop(); ans[pop] = i - pop; } stack.push(i); } return ans; } // 倒叙更好理解(还是记正序吧) public int[] dailyTemperatures(int[] temperatures) { int n = temperatures.length; int[] res = new int[n]; Deque<Integer> stack = new LinkedList<>(); for (int i = n-1; i >= 0; i--) { while (!stack.isEmpty() && temperatures[i] >= temperatures[stack.peek()]) { // 矮的都出栈 stack.pop(); } // 栈里的都是比i高的 res[i] = stack.isEmpty() ? 0 : (stack.peek()-i); stack.push(i); } return res; }
-
-
剑指t11 旋转数组的最小值,详细见注释
public int minArray(int[] numbers) { int left = 0, right = numbers.length - 1; while (left < right) { // 这样算中点不会溢出 int mid = left + (right - left) / 2; if (numbers[mid] < numbers[right]) { right = mid; } else if (numbers[mid] > numbers[right]){ // 这个mid就不可能是最小值,所以可以排除 left = mid + 1; } else { // mid和right相同,可以排除一个right right--; } } return numbers[left]; }
-
q42 接雨水
-
动态规划
这里的动态绘画不是直接求结果的,而是转一道弯,只要找到某个点两边界的较低一个就行了,这里动态规划是求两边的边界
public int trap(int[] height) { int ans = 0; int n = height.length; int[] maxLeft = new int[n]; maxLeft[0] = height[0]; for (int i = 1; i< n; i++) { maxLeft[i] = Math.max(height[i], maxLeft[i-1]); } int[] maxRight = new int[n]; maxRight[n-1] = height[n-1]; for (int i = n-2; i >= 0; i--) { maxRight[i] = Math.max(height[i], maxRight[i+1]); } for (int i = 1; i < n-1; i++) { ans += (Math.min(maxLeft[i], maxRight[i]) - height[i]); } return ans; }
-
单调栈
int ans = 0; Deque<Integer> stack = new LinkedList<>(); for (int i = 0; i < height.length; i++) { while (!stack.isEmpty() && height[stack.peek()] < height[i]) { // 记录此时最低的底 int top = stack.pop(); if (stack.isEmpty()) break; // 左边界的索引 int left = stack.peek(); int currWidth = i - left - 1; // 左边和右边最低点减去中间点,高低高必可接到雨水 int currHeight = Math.min(height[left], height[i]) - height[top]; ans += currWidth * currHeight; } // 不管height[stack.peek()]、height[i]谁大谁小,最终都要push(i) stack.push(i); } return ans;
-
双指针,对动态规划的优化,一次遍历就行了,期间一大一小,max大的减小的
int ans = 0; int left = 0, right = height.length-1, leftMax = height[left], rightMax = height[right]; while (left < right) { //如果只要一次遍历,通过这个while循环就行了,不需要在里面再循环 leftMax = Math.max(height[left], leftMax); rightMax = Math.max(height[right], rightMax); if (height[left] < height[right]) { ans += leftMax - height[left]; left++; } else { ans += rightMax - height[right]; right--; } } return ans;
-
-
q74 搜索二维矩阵
两个二分查找,第一个二分是找左边界,第二个二分是经典的求值
总结:以下完美模板,验证成功
四种情况 可以拿 1 2 2 2 3 target = 2举例 左边界是1,右边界是3,严格小的最大是0(可以等于则是3), 严格大的最小是4(可以等于则是1) 如果是 1 2 4 5 target = 3 左边界是2,右边界是1,严格小的最大是1,严格大的最小是2 // 找左侧边界 int left_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0; int right = nums.length; // 注意 while (left < right) { // 注意 int mid = left + (right - left) / 2; if (nums[mid] == target) { right = mid; } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; // 注意 } } return left; } // 找右侧边界 int right_bound(int[] nums, int target) { if (nums.length == 0) return -1; int left = 0, right = nums.length; while (left < right) { int mid = left + (right - left) / 2; if (nums[mid] == target) { left = mid + 1; // 注意 } else if (nums[mid] < target) { left = mid + 1; } else if (nums[mid] > target) { right = mid; } } return left - 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 越界的情况,按照力扣34题是这么防止越界,不同的要求可以改为不同的判断方法,比如可以return nums.length 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; // 注意 } // 找严格比target小的最大索引 static int search1(int[] nums, int target) { int low = -1; // 注意,最左可以到-1 int high = nums.length-1; while (low < high) { int mid = (low + high + 1) / 2; // 向着右边倾向 if (nums[mid] < target) { // 如果加上等于,就是小于等于target的最大索引 low = mid; } else { high = mid - 1; } } return low; } // 找严格比target大的最小索引 static int search2(int[] nums, int target) { int low = 0; int high = nums.length; // 注意,最右可以到nums.length while (low < high) { int mid = (low + high) / 2; if (nums[mid] <= target) { // 如果去掉等于,就是大于等于target的最小索引 low = mid + 1; } else { high = mid; } } return low; }
-
q162 寻找峰值
原来无序的数组在特定的条件也可以用二分,mid和mid+1哪个高往哪走
int low = 0, high = nums.length - 1; while (low < high) { int mid = low + (high - low) / 2; if (nums[mid] < nums[mid + 1]) { low = mid + 1; } else high = mid; } return low;
-
q384 打乱数组
为了使打乱的概率相同,而且每次抽取的数不重复,O(n)时间复杂度使用到的是洗牌算法
public int[] shuffle() { // 洗牌算法,随机选取一个[0,i+1)的数,与第i个数j for (int i = nums.length-1; i >= 0; i--) { Random random = new Random(); int j = random.nextInt(i+1); int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } return nums; }
两个库函数:
System.arraycopy:把一个数组的部分元素复制给另一个数组的部分位置
random.nextInt(i):选取范围是[0, i)
-
q59 q54 螺旋矩阵
- 剑指解法,左闭右闭,按层一个个计数,到最后一个数截至,适用于任何
- 代码随想录,左闭右开,按层,走n/2圈,最后如果n是奇数的话再补上最后一个数,如果不是方阵,那么最后一个数要单独处理
- labuladong,左闭右闭,for循环外套一个垂直方向是否满足的判断
-
hj03 明明的随机数
总体想法是正确的,具体实施出了大问题,关键问题在数据int数组去重,我犯了用新定义的两个双指针比较相等的错误,刻舟求剑了,应该是一个指向去重好的最新位置,一个指向正在遍历中的位置;而且也根本也不用这么麻烦重造个数组,在原数组的基础上就能直接输出答案
补充一个库,怎么从一个数组中截取一定长度的元素放在新数组中
newData = Arrays.copyOfRange(data, 2 , 7 );
-
华为2020秋招 分礼物
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zTXne1o-1657459118122)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20220324162833879.png)]
-
输入
// 这种是不行的,第一个nextLine读不到 Scanner sc = new Scanner(System.in); while (sc.hasNext()) { int count = sc.nextInt(); for (int i = 0; i < count; i++) { String str = sc.nextLine(); } } // 使用这种方式,或者BufferedReader while (sc.hasNext()) { int count = sc.nextInt(); int[] num = new int[count]; int[] ab = new int[count]; for (int i = 0; i < count; i++) { num[i] = sc.nextInt(); ab[i] = sc.nextInt(); } }
-
排序
TreeMap天然按照key的大小排序,如果需要改变这种规则,就在定义的时候指定key的排序方式(只能对key排序)
如果想要map想要自由定义排序规则,需要先放在List里面,转为Map.Entry,通过Collections排序,Set集合应该也是类似的方法
LinkedList<Map.Entry<Integer, Integer>> list = new LinkedList<Map.Entry<Integer, Integer>>(map.entrySet()); // 重新按照序号排序 Collections.sort(list1, new Comparator<Map.Entry<Integer, Integer>>() { @Override public int compare(Map.Entry<Integer, Integer> o1, Map.Entry<Integer, Integer> o2) { return o1.getValue() - o2.getValue(); } });
-
对于Set的操作
// 取这一步不能放在while里面,每次重新获取迭代器,迭代器位置不变,永远为true Iterator<Integer> it = integers.iterator(); // 取Integer迭代器的最后一个值 while (it.hasNext()) { // 迭代器往后移一位 a = it.next(); }
-
-
q435 无重叠区间
关键是左右指针的移动,我半天都没想到在循环外固定一个,在循坏内根据条件再移动它
-
q1024 视频拼接 (重点,且是我的缺陷)
我虽然知道画图做,但是不会转化为代码啊,这点真是太伤了,再好好训练这题和类似的题目!!!
public int videoStitching(int[][] clips, int time) { int res = 0; // 根据开头排序 Arrays.sort(clips, new Comparator<int[]>() { public int compare(int[] a, int[] b) { return a[0] != b[0] ? a[0] - b[0] : b[1] - a[1]; // 其实第二列无所谓 } }); int curEnd = 0, nextEnd = 0; int i = 0; while (i < clips.length && clips[i][0] <= curEnd) { // 如果没有后面这个条件,就要在中间补充一个子while,end有没有变化,如果没有变化可以直接返回-1了 while (i < clips.length && clips[i][0] <= curEnd) { nextEnd = Math.max(nextEnd, clips[i][1]); i++; } res++; curEnd = nextEnd; if (curEnd >= time) { // 注意,大于也行 return res; } } return -1; }
这种需要找到满足要求的,交替使用start和end的,就使用两个while
如果只要记录一个就可以得到这轮答案, 使用for循环和外面定义一个端点就行,参考力扣q435 无重叠区间
-
q134加油站
有点类似于骑士救公主,其实不用这么复杂的动态规划,要么暴力(超时),要么找到规律就行
要找到最小值,定义一个sum,定义一个minSum和其对应的index
-
剑指t60 n个骰子的点数
完全不记得怎么做了,看图才懂
public double[] dicesProbability(int n) { double[] dp = new double[6]; Arrays.fill(dp, 1.0 / 6.0); // 注意必须要带.0,或者后面加个d for (int i = 2; i <= n; i++) { double[] tmp = new double[i * 5 + 1]; for (int j = 0; j < 6; j++) { for (int k = 0; k < dp.length; k++) { tmp[j + k] += dp[k] / 6.0; } } dp = tmp; // 数组类似指针,原数组长度不一致也可以 } return dp; }
7.树操作
-
剑指t27 树的子结构
- 递归遍历主树,查找根节点值一样的节点
- 如果根节点一样就开始进入两棵树是否一样的递归遍历
-
q94 二叉数的中序遍历
在里面再定义一个无返回值的,和直接用本有返回值的方法,是一样的道理,有返回值,但是不接收,也相当于没有,最后有就行
使用统一迭代的方法
public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Deque<TreeNode> stack = new LinkedList<>(); if (root != null) stack.push(root); // 空也是东西 while (!stack.isEmpty()) { TreeNode node = stack.pop(); if (node != null) { // 左中右 if (node.right != null) stack.push(node.right); stack.push(node); stack.push(null); if (node.left != null) stack.push(node.left); } else { res.add(stack.pop().val); } } return res; }
其他的只要换一两行的顺序就行了
-
q98验证二叉搜索树
-
遍历
public boolean isValidBST(TreeNode node, long lower, long upper){ if(node == null){ return true; } if(node.val <= lower || node.val >= upper){ return false; } return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper); }
为什么要用long,因为测试用例单节点最大值为lnt类型的最大值
分析的时候最好明白简写之前的写法
if (root == null) return true; // 左中右 boolean left = isValidBST(root.left); if (value < (long)root.val) value = (long)root.val; else return false; boolean right = isValidBST(root.right); return left && right;
这种题目其实也要做过记住结论最好,自己想的方法是参数是布尔类型的是否大于以及上一层的根节点值
-
中序遍历
二叉搜索树的中序遍历就是递增的,如果想时间复杂度更低,使用栈
判断集合是否为空,不能用 == null,而是 .isEmpty 或 .size == 0
public boolean isValidBST(TreeNode root) { Deque<TreeNode> stack = new LinkedList<TreeNode>(); long inorder = Long.MIN_VALUE; while (!stack.isEmpty() || root != null){ while (root != null){ stack.push(root); root = root.left; } root = stack.pop(); if(root.val <= inorder){ return false; } inorder = root.val; root = root.right; } return true; }
-
-
q101对称二叉树
-
我是用的根左右和根右左的遍历顺序,再比较值,这是典型的错误,同样的结果可能树不对称,比如只有左子树的1,2,3
-
方法一就是递归,一边往左走,一边往又走
public boolean isSymmetric(TreeNode root) { if(root == null) { return true; } return helper(root.left, root.right); } public boolean helper(TreeNode p, TreeNode q) { if(p == null && q == null) { return true; } if(p == null || q == null) { return false; } //如果有个一个是false,那么就是false,全是true,才是true return p.val == q.val && helper(p.left, q.right) && helper(p.right, q.left); }
如果第一次传入helper的值全是root,那么比较次数要翻倍
其实不建议如上这么简介的写,总结递归三部曲
-
确定递归函数的参数和返回值
bool compare(TreeNode* left, TreeNode* right)
-
确定终止条件
if (left == NULL && right != NULL) return false; else if (left != NULL && right == NULL) return false; else if (left == NULL && right == NULL) return true; else if (left->val != right->val) return false; // 注意这里没有使用else,后面都是在相同的基础上操作
-
确定单层递归的逻辑
bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右 bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左 bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理) return isSame;
-
-
方法二是迭代,使用队列比较,左边入队和右边入队,两个连续相同才行
public boolean check(TreeNode u, TreeNode v) { Queue<TreeNode> q = new LinkedList<TreeNode>(); q.offer(u); q.offer(v); while (!q.isEmpty()) { u = q.poll(); v = q.poll(); if (u == null && v == null) { continue; } if ((u == null || v == null) || (u.val != v.val)) { return false; } q.offer(u.left); q.offer(v.right); q.offer(u.right); q.offer(v.left); } return true; }
如果不是递归,那么true和false就是一锤子买卖
-
-
q102二叉树的层次遍历
使用队列,主要是要找到是哪一层的,要不传值的时候就记录下每个节点和它的层次,其实也可以通过在出队把子入队之前先算出来当前层的节点数,了解到这一点后不看官方代码也写出来了
写代码的一个好方法:一开始不要想尽善尽美,可以先不加上循环,试着按顺序写一遍,后面按照循环逻辑再加入循环,这里的while循环就是后面再加的
通过测试用例改了两个东西
- 左右阶段先判断不能为空才能入队
- 根节点入队之前也要先判断是否为空
public List<List<Integer>> levelOrder(TreeNode root) { Queue<TreeNode> queue = new LinkedList<>(); List<List<Integer>> res = new ArrayList<>(); List<Integer> list = new ArrayList<>(); if (root == null){ return res; } queue.offer(root); while (!queue.isEmpty()) { int size = queue.size(); for (int i = 0; i < size; i++) { TreeNode p = queue.poll(); list.add(p.val); if (p.left != null) { queue.offer(p.left); } if (p.right != null) { queue.offer(p.right); } } res.add(new ArrayList<>(list)); list.clear(); } return res; }
可以再优化一下, List list = new ArrayList<>();改到while循环的开始,那样就只用res.add(list);也不用clear了
isEmpty()如果分配了空间,里面是空值,那么就是Empty,不是null,连空间都没有分配,那就是null了
-
q104二叉树的深度
-
一开始我是写的先序遍历,但是不记得记录最深层了,g
-
记录了最深层,但是java的基本类型是值传递,不会影响main函数的基本类型值,必须定义一个全局的max,传值的时候this.max,g
应该像下面这样写(回溯写法),求树的深度(根的高度),先序遍历
public int maxDepth(TreeNode root) { if (root == null) return 0; depth(root, 1); return maxDepth; } public void depth(TreeNode node, int depth) { if (node == null) return; maxDepth = depth > maxDepth ? depth : maxDepth; // 写法1.1: depth++; depth(node.left, depth); depth(node.right, depth); // 为什么这里不用再--,因为返回上一个递归的时候,本轮递归改变的值无效,进去一次,出来两次 // 下面需要再--,进去一次,出来一次,还是本轮递归 // 写法1.2: depth(node.left, depth+1); //不能写depth++ or ++depth depth(node.right, depth+1); //depth++不会带来改变,++depth还会影响下面的递归 // 写法2.1: if (node.left != null) { depth++; depth(node.left, depth); depth--; } if (node.right != null) { depth++; depth(node.right, depth); depth--; } // 写法2.2 if (node.left != null) { depth(node.left, depth+1); } if (node.right != null) { depth(node.right, depth+1); } }
-
普通递归,递归三部曲,求根的高度,用后序遍历
public int maxDepth(TreeNode root) { return getDepth(root, 0); } public int getDepth(TreeNode node, int depth) { if(node == null){ return depth; } return Math.max(getDepth(node.left, depth+1), getDepth(node.right, depth+1)); }
官方题解
public int maxDepth(TreeNode root) { if (root == null) { return 0; } else { int leftHeight = maxDepth(root.left); int rightHeight = maxDepth(root.right); return Math.max(leftHeight, rightHeight) + 1; } }
-
也可以用层次遍历,返回最后的层数
-
二刷想到的方法(注释掉的),实际一行就行了
class Solution { // int max = 1; public int maxDepth(TreeNode root) { return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; // if (root == null) return 0; // dfs(root, 1); // return max; } // private void dfs(TreeNode root, int deep) { // if (root == null) return; // max = Math.max(max, deep); // dfs(root.left, deep + 1); // dfs(root.right, deep + 1); // } }
-
q559 N叉树的最大深度
两个问题,1 不要把depth作为全局变量,2 加一不要写在for循环的递归函数里面
-
总结
第一个不带返回值,在递归过程中改变参数的是回溯;第二个是普通递归,左右根,有递归三部曲
看labuladong归纳,才真正意识到了所谓回溯(不带返回值)和递归(带返回值)的真正区别,这两类思路分别对应着回溯算法核心框架和动态规划核心框架,后者可以避免遍历和辅助函数。动态规划系列问题有「最优子结构」和「重叠子问题」两个特性,而且大多是让你求最值的。很多算法虽然不属于动态规划,但也符合分解问题的思维模式。动态规划一般求最值,过程不关心,所以求最值等情况效率高,回溯所有过程都要便利的到,虽然效率低但是可以得到过程。递归优化通过备忘录存储和改为自定而上就变为了动态规划
先序遍历代码的逻辑其实是求的根节点的高度,而根节点的高度就是这颗树的最大深度,所以才可以使用后序遍历
-
-
剑指t55 平衡二叉树
平衡二叉树是递归判断的,不是简单的最高减最低
返回值是boolean类型的递归函数,返回的时候大多是&& ||这种结构
- 自上而下,前序遍历,上面得到结果的基础上遍历到下面,这里要多次调用计算深度的函数,时间上有浪费
public boolean isBalanced(TreeNode root) { // int maxDeep = getMaxDeep(root); // int minDeep = getMinDeep(root); // if ((maxDeep - minDeep) > 1) { // return false; // } // return true; if (root == null) { return true; } return Math.abs(getMaxDeep(root.left) - getMaxDeep(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right); } public int getMaxDeep(TreeNode root) { if (root == null) { return 0; } return Math.max(getMaxDeep(root.left), getMaxDeep(root.right)) + 1; } // 搞错了定义,不是最高减最低 // public int getMinDeep(TreeNode root) { // if (root == null) { // return 0; // } // if (root.left == null && root.right == null) { // return 1; // } // return Math.min(getMinDeep(root.left), getMinDeep(root.right)) + 1; // }
- 自下而上,后序遍历,上面的结果由下面决定
public int getDepth(TreeNode root) { if (root == null) return 0; int leftHeight = getDepth(root.left); if (leftHeight == -1) return -1; int rightHeight = getDepth(root.right); if (rightHeight == -1) return -1; int leftDepth = getDepth(root.left); int rightDepth = getDepth(root.right); if(Math.abs(leftDepth - rightDepth) > 1) { return -1; } else { return Math.max(leftDepth, rightDepth) + 1; } }
-
q105 从前序和中序遍历序列构造二叉树
我只会分析,手工画图求出来,写代码无从下手
看题解知道了可以用递归
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HU424gwJ-1657459118129)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211021200146164.png)]
在构建一颗二叉树上(同类问题都可以这么思考),从宏观上思考,先得到根节点,再递归得到根节点的左节点,右节点,递归函数的区间不断缩小,直到小到为一,得到最深栈的根节点(叶子节点),甚至过头了,返回null后开始退出递归栈
这题的关键就在于确定递归函数的参数,左右端点的索引,闭区间,不需要根节点索引
public TreeNode buildTree(int[] preorder, int[] inorder) { int n = inorder.length; Map<Integer, Integer> map = new HashMap<>(); for (int i = 0; i < n; i++) { map.put(inorder[i], i); } return myBuildTree(preorder, 0, n-1, map, 0, n-1); } private TreeNode myBuildTree(int[] preorder, int preLeft, int preRight, Map<Integer, Integer> map, int inLeft, int inRight) { if(preLeft > preRight || inLeft > inRight){ return null; } int rootVal = preorder[preLeft]; TreeNode root = new TreeNode(rootVal); int pIndex = map.get(rootVal); root.left = myBuildTree(preorder, preLeft+1, pIndex-inLeft+preLeft, map, inLeft, pIndex-1); root.right = myBuildTree(preorder, pIndex-inLeft+preLeft+1, preRight, map, pIndex+1, inRight); return root; }
这里通过先序遍历序列找到的根节点数值,找其在中序遍历序列对应的索引,空间换时间使用了hash表
递归中要注意切割的标准,以上是左闭右闭,也可以左闭右开等,总之注意循环不变量
-
q114二叉树展开为链表
-
先学习先序遍历的迭代写法
二叉树的前序遍历,迭代实现 根-左-右
思路:
1、 借用栈的结构
2、 先push(root)
3、 node = pop()
3.1、list.add( node.val )
3.1、push( node.right )
3.3、push( node.left )
4、循环步骤3直到栈空 -
使用先序遍历,遍历顺序的节点存入List中,再根据原根目录地址创建新的链表,因为TreeNode newRoot = root; newRoot指向的地址也是root指向的地址,所以也可以算是原地。java打印指针一般都是输出指针指向的地址
-
采用分解问题的思维
public void flatten(TreeNode root) { if (root == null) return; flatten(root.left); flatten(root.right); TreeNode tmp = root.right; root.right = root.left; root.left = null; TreeNode p = root; while (p.right != null) p = p.right; p.right = tmp; }
这就是递归的魅力,你说
flatten
函数是怎么把左右子树拉平的?不容易说清楚,但是只要知道
flatten
的定义如此并利用这个定义,让每一个节点做它该做的事情,然后flatten
函数就会按照定义工作了解到用后序遍历解之后,后面我竟然自己写出来了!
-
-
q208实现前缀树
代码Trie[] children = new Trie[26]; 的意思是给Trie结点创建26个同自己类型的子节点,下一个子节点就是node.children[i]
在学习一下类定义的方式
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26]; // 这里不能用Trie()
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char c = word.charAt(i);
int index = c - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
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 c = prefix.charAt(i);
int index = c - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}
-
q236 二叉树的最近公共祖先(多种解法)
- 递归
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yob1RqKr-1657459118130)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211130153627595.png)]
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null || root == p || root == q) { return root; } TreeNode left = lowestCommonAncestor(root.left, p, q); TreeNode right = lowestCommonAncestor(root.right, p ,q); if (left == null) { return right; } if (right == null) { return left; } return root; }
-
两次遍历,得到从根节点到两个结点的路径,再从路径找到第一个next不同的结点
-
遍历过程中存储父节点
-
如果是二叉搜索树,可以使用特性,父节点小于两个结点,就到左子树
// 以下是非递归方法,也可以使用递归 TreeNode node = root; while (true) { if (node.val < p.val && node.val < q.val) { node = node.right; } else if (node.val > p.val && node.val > q.val) { node = node.left; } else break; } return node;
-
q337打家劫舍Ⅲ
我们可以用 f(o)表示选择 o 节点的情况下,o 节点的子树上被选择的节点的最大权值和;g(o)表示不选择 o 节点的情况下,o 节点的子树上被选择的节点的最大权值和;l 和 r 代表 o 的左右孩子。
当 o 被选中时,o 的左右孩子都不能被选中,故 o 被选中情况下子树上被选中点的最大权值和为 l 和 r 不被选中的最大权值和相加,即 f(o) = g(l) + g®
当 o 不被选中时,o 的左右孩子可以被选中,也可以不被选中。对于 o 的某个具体的孩子 x,它对 o 的贡献是 x 被选中和不被选中情况下权值和的较大值。故 g(o) =max{f(l),g(l)}+max{f®,g®}-
用递归的定义
Map<TreeNode, Integer> map = new HashMap<>(); public int rob(TreeNode root) { // 选择当前根节点,两个子节点就不能选了 if (root == null) return 0; if (map.containsKey(root)) return map.get(root); int cRoot = root.val; if (root.left != null && root.right != null) { cRoot += rob(root.left.left) + rob(root.left.right) + rob(root.right.left) + rob(root.right.right); } else if (root.left != null) { cRoot += rob(root.left.left) + rob(root.left.right); } else if (root.right != null) { cRoot += rob(root.right.left) + rob(root.right.right); } // 不选根节点,选两个子节点的最大值 int nRoot = rob(root.left) + rob(root.right); int res = Math.max(cRoot, nRoot); map.put(root, res); return res; }
-
省去hash数组,只用两个长度为2的数组分别表示左右,0表示选,1表示不选
public int rob(TreeNode root) { int[] res = dfs(root); return Math.max(res[0], res[1]); } private int[] dfs(TreeNode node) { if (node != null) { int[] l = dfs(node.left); int[] r = dfs(node.right); int selected = node.val + l[1] + r[1]; int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); return new int[]{selected, notSelected}; } else { return new int[]{0, 0}; } }
-
-
q437路径总和Ⅲ
-
暴力法,每个位置都来一次dfs,dfs套dfs
public int pathSum(TreeNode root, int targetSum) { // 从头开始遍历 int res = 0; if (root == null) return 0; res = rootSum(root, targetSum); res += pathSum(root.left, targetSum); res += pathSum(root.right, targetSum); return res; } public int rootSum(TreeNode root, int targetSum) { if (root == null) return 0; int sum = 0; if (root.val == targetSum) { sum++; } sum += rootSum(root.left, targetSum - root.val); sum += rootSum(root.right, targetSum - root.val); return sum; }
-
前缀,不太懂,继续看
/** * ---算法思路分析 *要求符合的路径的数目,很显然,需要考虑到所有路径的情况(遍历树) * 深度遍历树,每访问一个结点,求出根到当前结点的路径节点值之和 cur;(也就是该节点的前缀和,节点前缀和包括自己) * 此时,HashMap已经纪录了 到该结点之前 的所有结点的前缀和(HashMap<前缀和,数量>) * 通过查找 cur-targetSum,就知道了 根到当前结点路径上,以该节点结尾的解的数量;(遍历树的过程,统计以每个访问节点为路径结尾的解的数量,这样就求得了全部解) * HahsMap.put(cur ,HashMap.get(当前前缀和)+1) * HashMap 弹出发生在,当前节点递归返回时 * ---算法结构设计 * * 对每个结点 * 往下遍历时,先更新HashMap * 递归返回时,更新(还原)HashMap * 求解当前节点解 * */ public class SoPathSum { int res=0; Map<Integer,Integer> map = new HashMap<>(); public int pathSum(TreeNode root,int targetSum){ map.put(0,1);//每个结点自身值=targetSum 的情况 traverse(root,0,targetSum); return res; } private void traverse(TreeNode node,int cur,int targetSum){ if(node == null){ return ; } cur+=node.val; map.put(cur,map.getOrDefault(cur,0)+1); traverse(node.left,cur,targetSum); traverse(node.right,cur,targetSum); map.put(cur,map.get(cur)-1); //处理当前结点 res+= map.getOrDefault(cur-targetSum,0); } }
-
-
q538把二叉搜索树转化为累加树
就是反中序遍历,原来有返回值也可以当作无返回值用
这样也行
class Solution { // 其实更建议不要定义全局遍历,定义一个递归函数跟随进去 int sum = 0; public TreeNode convertBST(TreeNode root) { if (root != null) { convertBST(root.right); sum += root.val; root.val = sum; convertBST(root.left); } } }
-
q543二叉树的直径
通过递归求深度的方法,答案=max(左子树深度,右子树深度),答案不进入递归栈中,只是利用递归的过程数据。前序遍历只能获取父节点的信息,后序遍历还可以先获取到子节点的信息,这里求深度用后续更高
int ans = 0; public int diameterOfBinaryTree(TreeNode root) { depth(root); return ans; } private int depth(TreeNode node) { if (node == null) { return 0; } int L = depth(node.left); int R = depth(node.right); ans = Math.max(ans, L + R); return Math.max(L, R) + 1; }
前序一开始得不到深度,只能重复算
第二次没思路,按照评论的提示是每个结点都重新求一遍深度,前序和后序都没用都是重复计算,这样时间复杂度很高,利用不到上面遍历过程中子节点的高度信息,除非加上备忘录
// 遍历二叉树 void traverse(TreeNode root) { if (root == null) { return; } // 对每个节点计算直径 int leftMax = maxDepth(root.left); int rightMax = maxDepth(root.right); int myDiameter = leftMax + rightMax; // 更新全局最大直径 maxDiameter = Math.max(maxDiameter, myDiameter); traverse(root.left); traverse(root.right); }
-
q617 合并二叉树
简单的带返回值的dfs还是不会写,只能看懂
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { if (root1 == null) { return root2; } if (root2 == null) { return root1; } TreeNode root = new TreeNode(root1.val + root2.val); root.left = mergeTrees(root1.left, root2.left); root.right = mergeTrees(root1.right, root2.right); return root; }
-
剑指t26 树的子结构
两个dfs,先在主树上找到与子树根节点相同的字节的,再开始dfs比较
如果某相同结点,a的左子树还有,b的左子树为空,那么就证明左子树ok了,反之则反
本题重点是要知道第一个递归,同时为空就是false,第二个递归,b为空是true,b不为空a为空才是false
public boolean isSubStructure(TreeNode A, TreeNode B) { // 遍历A的节点,直到与B的根节点相同的 // 这个result在很多返回值为bool类型的递归中都很有用,这里适用于有一个成功结果就成功 boolean result = false; if (A != null && B != null) { if (A.val == B.val) result = reCur(A, B); if (!result) result = isSubStructure(A.left, B); // 再给机会 if (!result) result = isSubStructure(A.right, B); // 继续给机会,反正有一个成就行了 } return result; } private boolean reCur(TreeNode a, TreeNode b) { if (b == null) return true; if (a == null) return false; if (a.val != b.val) return false; return reCur(a.left, b.left) && reCur(a.right, b.right); }
不用boolean记录结果,就直接返回或上两个子数组递归的结果
if (A == null || B == null) return false; // 从根开始找与broot值相同的 if (A.val == B.val && helper(A.left, B.left) && helper(A.right, B.right)) { // 开始进行比较 return true; } return isSubStructure(A.left, B) || isSubStructure(A.right, B); if (A == null || B == null) return false; // 从根开始找与broot值相同的 res |= helper(A, B); res |= isSubStructure(A.left, B); res |= isSubStructure(A.right, B); return res;
-
剑指t27二叉树的镜像
public TreeNode mirrorTree(TreeNode root) { if (root == null) { return null; } // 我的方法也可以 // TreeNode node = root.left; // root.left = root.right; // root.right = node; // mirrorTree(root.left); // mirrorTree(root.right); // 这个可以当作二叉树有返回值递归的模板 TreeNode left = mirrorTree(root.left); TreeNode right = mirrorTree(root.right); root.left = right; root.right = left; return root; }
-
剑指t28 对称二叉树
自己写出来了,不过不够优美,注释的是标准答案
以后二叉树同时比较的题目,都是两个参数同时递归
public boolean isSymmetric(TreeNode root) { // return root == null ? true : recur(root.left, root.right); return isSymmetric(root, root); } private boolean isSymmetric(TreeNode root1, TreeNode root2) { if (root1 == null && root2 != null) return false; if (root1 != null && root2 == null) return false; if (root1 == null && root2 == null) return true; if (root1.val != root2.val) return false; // if (root1 == null && root2 == null) return true; // if (root1 == null || root2 == null || root1.val != root2.val) return false; return isSymmetric(root1.left, root2.right) && isSymmetric(root1.right, root2.left); }
-
剑指t54 二叉搜索树的第k大结点
这种题目,不适合带参数的dfs,还是用void好
class Solution { // 不用k,用一个count计数也行 int ans ,k; public int kthLargest(TreeNode root, int k) { this.k = k; dfs(root); return ans; } // 不能把k放在递归中,值传递,回溯后不影响原值 void dfs(TreeNode node) { if (node == null) return; dfs(node.right); k--; if (0 == k) { ans = node.val; } dfs(node.left); } }
-
q124 二叉树中的最大路径和
class Solution { int maxSum = Integer.MIN_VALUE; public int maxPathSum(TreeNode root) { maxGain(root); // 虽然不需要得到他的返回值,但是函数里面要y return maxSum; } // 最后返回的结果是根节点+最大子节点的值,但是题目的结果在这个过程中计算 public int maxGain(TreeNode root) { if (root == null) { return 0; } // 不要担心下层有负数被选中,递归到上层的时候都已经处理好了 int maxLeft = Math.max(maxGain(root.left), 0); // 如果这里可以是负数,要多次判断,比较麻烦 int maxRight = Math.max(maxGain(root.right), 0); maxSum = Math.max(root.val + maxLeft + maxRight, maxSum); // 只能走和最大的一条路,root.val是负数也行,不能直接排除,否则会断层 return root.val + Math.max(maxLeft, maxRight); } }
-
q297 二叉树的序列化与反序列化
-
dfs
这里使用先序遍历,只要空指针也保留,只有先序遍历也可还原
可以用普通的无返回值先序遍历方法,多一个值传递的参数作为结果
public class Codec { // Encodes a tree to a single string. public String serialize(TreeNode root) { return rserialize(root, ""); } // Decodes your encoded data to tree. public TreeNode deserialize(String data) { String[] strings = data.split(","); List<String> list = new LinkedList<>(Arrays.asList(strings)); return rdeserialize(list); } public String rserialize(TreeNode root, String str) { if (root == null) { str += "null,"; } else { str += str.valueOf(root.val) + ","; str = rserialize(root.left, str); str = rserialize(root.right, str); } return str; } private TreeNode rdeserialize(List<String> list) { if (list.get(0).equals("null")) { list.remove(0); return null; } TreeNode root = new TreeNode(Integer.valueOf(list.get(0))); list.remove(0); // 没想到左右都可以通过递归函数得到 root.left = rdeserialize(list); root.right = rdeserialize(list); return root; } }
-
层序遍历,剑指
-
-
q117 填充每个节点的下一个右侧节点指针II
-
层次遍历
while (!queue.isEmpty()) { int n = queue.size(); Node last = null; // 妙处1 for (int i = 0; i < n; i++) { Node node = queue.poll(); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } // 妙处2 if (i != 0) last.next = node; last = node; } }
-
利用next指针,空间复杂度降为1
// last是处理过程中的指针,newStart记录下一层的第一个节点 Node last = null, newStart = null; public Node connect(Node root) { if (root == null) return null; Node start = root; while (start != null) { last = null; newStart = null; for (Node p = start; p!= null; p = p.next) { if (p.left != null) { handle(p.left); } if (p.right != null) { handle(p.right); } } start = newStart; } return root; } public void handle(Node p) { if (last != null) { last.next = p; } if (newStart == null) { newStart = p; } last = p; }
-
dfs操作
Map<Integer, Node> map = new HashMap<>(); public Node connect(Node root) { helper(root, 0); return root; } void helper(Node node, int deepth){ if(node == null) return; if(map.containsKey(deepth)){ map.get(deepth).next = node; } map.put(deepth, node); helper(node.left, deepth + 1); helper(node.right, deepth + 1); }
-
-
q572 另一棵树的子树
-
暴力法,两次递归,很考验对&true和|ture的处理
public boolean isSubtree(TreeNode root, TreeNode subRoot) { if (root == null) return false; return search(root, subRoot) || isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot); } private boolean search(TreeNode root, TreeNode subRoot) { if (root == null && subRoot == null) return true; if (root == null || subRoot == null) return false; if (root.val != subRoot.val) return false; // 可与上合并 return search(root.left, subRoot.left) && search(root.right, subRoot.right); }
-
kmp 先用dfs转化为字符串,无子节点用null填充
-
-
q222 完全二叉树的节点个数
-
使用对待普通二叉树的方法
public int nodesNum(TreeNode root) { if (root == null) return 0; int leftNum = nodesNum(root.left); int rightNum = nodesNum(root.right); return leftNum + rightNum + 1; }
-
利用完全二叉树性质
public int countNodes(TreeNode root) { if (root == null) return 0; int leftDepth = getDepth(root.left); // 一直往左孩子找 int rightDepth = getDepth(root.right); if (leftDepth == rightDepth) { // 左边是完全二叉树 return (1<<leftDepth) + countNodes(root.right); } else { // 右边是完全二叉树 return (1<<rightDepth) + countNodes(root.left); } }
-
上面这个太难理解了吧,还是这个好理解,出口处利用满二叉树的性质加快计算速度
public int countNodes(TreeNode root) { TreeNode l = root, r = root; int lh = 0,rh = 0; while (l != null) { lh++; l = l.left; } while (r != null) { rh++; r = r.right; } if (lh == rh) return (int)Math.pow(2, lh) - 1; return 1 + countNodes(root.left) + countNodes(root.right); // 能够利用到完全二叉树的性质 }
-
-
q501 二叉搜索树中的众数
-
递归法
相邻两个元素作比较,使用pre指针中序遍历记录上一个节点值,处理方法写在中间
为了只遍历一遍,计算本数出现的频率,有与之前最大频率相等和大于的情况,做相应处理
int pre = -1; List<Integer> mode = new ArrayList<>(); int count = 0; int maxTime = 0; public int[] findMode(TreeNode root) { traversal(root); int[] ans = new int[mode.size()]; for (int i = 0; i < mode.size(); i++) { ans[i] = mode.get(i); } return ans; } public void traversal(TreeNode root) { if (root == null) return; traversal(root.left); if (pre == -1 || root.val != pre) { count = 1; } else count++; if (count > maxTime) { maxTime = count; mode.clear(); mode.add(root.val); } else if (count == maxTime) { mode.add(root.val); } pre = root.val; traversal(root.right); }
-
迭代法
只需要在中序遍历的统一写法的else里面加入处理就行了,直接粘贴过来就行
-
-
q669 修剪二叉搜索树
二叉树的递归分为遍历和分解问题两种思维模式,这里用到分解问题的思维,所以用到递归三部曲
每层的操作,都要想着自己是root,我应该如何判断与返回。本题的函数是每轮修剪完后
本题用前后中序都可以成功,只是对先后中不敏感的习惯性写前序
8.图操作
-
q200岛屿数量
-
使用dfs,每次遍历到之后都置为’0’
高和宽可以不跟随递归函数进入,因为每次都可以获得,遍历到的位置是否在图内,可以递归之后再判断,不用每次递归之前判断
public int numIslands(char[][] grid) { if (grid == null || grid.length == 0) { return 0; } int h = grid.length; int l = grid[0].length; int num_islands = 0; for (int i = 0; i < h; i++) { for (int j = 0; j < l; j++) { if (grid[i][j] == '1') { num_islands++; dfs(grid, i, j); } } } return num_islands; } private void dfs(char[][] grid, int i, int j) { // 右下左上,判断是否在边界内 int h = grid.length; int l = grid[0].length; if (i >= 0 && i < h && j >= 0 && j < l && grid[i][j] != '0') { grid[i][j] = '0'; // 这个赋值为'0'不会影响后面回退的,因为这一步已经过去了 dfs(grid, i, j + 1); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i - 1, j); } }
-
使用bfs,除了遍历方法不同,其他与dfs一样
栈用Deque,队用Quere,都是LinkedList
由于这里的遍历是二维数组每个位置开头都要遍历一次,所以两重for循环,先把第一个节点入队,之后如果队不为空,每次都先出队判断有没有附近为’1’的节点,有则入队,这里加入的节点是二维数组的一维数值,可以还原为二维坐标
for (int i = 0; i < h; i++) { for (int j = 0; j < l; j++) { if (grid[i][j] == '1') { num_islands++; grid[i][j] = '0'; Queue<Integer> queue = new LinkedList<>(); queue.add(i * l + j); while (!queue.isEmpty()) { int id = queue.remove(); int row = id / l; int col = id % l; if (row - 1 >= 0 && grid[row-1][col] == '1') { grid[row-1][col] = '0'; queue.add((row-1)*l+col); } if (row + 1 < h && grid[row+1][col] == '1') { grid[row+1][col] = '0'; queue.add((row+1)*l+col); } if (col - 1 >= 0 && grid[row][col-1] == '1') { grid[row][col-1] = '0'; queue.add(row * l + col - 1); } if (col + 1 < l && grid[row][col+1] == '1') { grid[row][col+1] = '0'; queue.add(row * l + col + 1); } } } } }
-
-
q207 课程表
同样可以使用两种递归方法完成
我一开始的思维被限制住了,以为就通过二维数组操作,太复杂了,实际上自己根据二维数组转化为图,再进一步解题,结果就是这个有向图有没有环(是不是拓扑图)
思维:
- 基本数据类型不能够跟随函数的变化而变化,所以bool值直接当作全局变量,不能跟随递归改变
- 其实课程默认是连续递增的,所以 edges.get(item[1]).add(item[0])正好使用numCourses空间
- 三种状态,没访问过0,正在访问1,访问完成==2(只要不是01就行),加了一个中间状态1,因为可能有环,其他没换的就只要两种状态就行
- 这种题目涉及到图的遍历,必须要先建立图的数据结构,邻接表或者邻接矩阵,再使用图的遍历方法
boolean valid = true; public boolean canFinish(int numCourses, int[][] prerequisites) { List<List<Integer>> edges = new ArrayList<>(); int[] visited = new int[numCourses]; for (int i = 0; i < numCourses; i++) { edges.add(new ArrayList<>());// 所有节点个数 } for (int[] item : prerequisites) { edges.get(item[1]).add(item[0]); } for (int i = 0; i < numCourses && valid; i++) { if (visited[i] == 0) { dfs(edges, visited, i); } } return valid; } public void dfs(List<List<Integer>> edges, int[] visited, int i) { visited[i] = 1; List<Integer> list = edges.get(i); for (int u : list) { if (visited[u] == 0) { dfs(edges, visited, u); // 下面三行可不加,加了更快 if(!valid) { return; } } if (visited[u] == 1) { valid = false; return; } } visited[i] = 2; }
如果有多个if else, 那么写把每个if 的条件写出来,再看后面能不能优化,把公共的提出来
bfs方法,正向思维,先求出入度为0的节点,拿掉,之后每次拿掉入度为0的节点,最后没有节点了就证明是拓扑图
在代码中就是
使用一个队列来进行广度优先搜索。初始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。
在广度优先搜索的每一步中,我们取出队首的节点 uu:
我们将 uu 放入答案中;
我们移除 uu 的所有出边,也就是将 uu 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0,那么我们就将 v 放入队列中。
在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点,那么我们就找到了一种拓扑排序,否则说明图中存在环,也就不存在拓扑排序了。
-
q79单词搜索
-
由于不要求找到路径,只要结果,所以dfs最好定义为一个返回值为bool类型的函数
-
如果没找到结果,所有位置的点都要作为起点遍历一次,遍历前面起点的过程中,如果得到false那么不处理,如果有一个是true,那么有解
-
题解定义了一个用于计算方位的函数,如果走的方向比较多最好还是要方位函数,我的思想是从哪边走才判断哪边
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (int[] dir : directions) {
int newi = i + dir[0], newj = j + dir[1];
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) -
注意离开前,一定要used【i][j] = false;
-
递归函数返回值是bool类型时,如果想返回false不管,返回ture就成功的时候,就用这种方法
if(dfs(board, chars, used, ni, nj, k+1)) return true;
public boolean exist(char[][] board, String word) { int m = board.length; if(m == 0){ return false; } int n = board[0].length; //标记是否走过 boolean[][] used = new boolean[m][n]; char[] chars = word.toCharArray(); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if(dfs(board, chars, used, i, j, 0)){ return true; // 等价于 res || dfs() } } } return false; } public boolean dfs(char[][] board, char[] chars, boolean[][] used, int i, int j, int k){ //右左上x if(k == chars.length-1){ return board[i][j] == chars[k]; } if (board[i][j] == chars[k]){ int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; used[i][j] = true; // 这个不能放在for里面使用ni和nj,否则第一个位置赋值不到 for (int[] direction : directions) { int ni = i + direction[0]; int nj = j + direction[1]; //判断是否仍然在矩阵区域内,且下一块区域没访问过 // 这个if不能放在递归出口,因为出界的话取不到值 if(ni >= 0 && ni < board.length && nj >= 0 && nj < board[0].length && used[ni][nj] == false){ if(dfs(board, chars, used, ni, nj, k+1)){ return true; } } } used[i][j] = false; } return false; }
-
-
剑指t13 机器人的运动范围
-
不带返回值的dfs,我写的
int count = 0; public int movingCount(int m, int n, int k) { // dfs,每次都判断一下是否出界以及数位和是否超过k // 如果访问过的位置,结果+1,也不再恢复成未访问 // 用bfs做一下 boolean[][] vis = new boolean[m][n]; bfs(m, n, 0, 0, k, vis); return count; } private void bfs(int m, int n, int i, int j, int k, boolean[][] vis) { // 不记得写vis[i][j] == false条件导致超时 if (i < m && j < n && (singleSum(i) + singleSum(j)) <= k && vis[i][j] == false) { count++; vis[i][j] = true; bfs(m, n, i+1, j, k ,vis); bfs(m, n, i, j+1, k ,vis); } } private int singleSum(int i) { int sum = 0; while (i != 0) { sum += i % 10; i /= 10; } return sum; }
-
带返回值的dfs,清华大佬写的
public int movingCount(int m, int n, int k) { boolean[][] visited = new boolean[m][n]; return dfs(0, 0, m, n, k, visited); } public int dfs(int i, int j, int m, int n, int k, boolean[][] visited) { if(i >= m || j >= n || k < getSum(i) + getSum(j) || visited[i][j]) { return 0; } visited[i][j] = true; return 1 + dfs(i + 1, j, m, n, k, visited) + dfs(i, j + 1, m, n, k, visited); }
-
bfs,类似于树的层次遍历
public int movingCount(int m, int n, int k) { int count = 0; boolean[][] vis = new boolean[m][n]; int[][] directions = new int[][]{{0, 1}, {1, 0}}; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{0, 0}); vis[0][0] = true; count++; while (!queue.isEmpty()) { int[] poll = queue.poll(); int i = poll[0]; int j = poll[1]; for (int[] direction : directions) { int ni = i + direction[0]; int nj = j + direction[1]; if (ni < m && nj < n && !vis[ni][nj] && (singleSum(ni) + singleSum(nj) <= k)) { queue.offer(new int[]{ni, nj}); vis[ni][nj] = true; count++; } } } return count; }
-
-
剑指t29 顺时针打印矩阵
-
定义一个是否访问过的二维数组,每次都改变行列左边
int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; int directionIndex = 0; for (int i = 0; i < total; i++) { order[i] = matrix[row][column]; visited[row][column] = true; int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1]; if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns || visited[nextRow][nextColumn]) { directionIndex = (directionIndex + 1) % 4; } row += directions[directionIndex][0]; column += directions[directionIndex][1]; }
-
按层搜索
注意:
- 数组等于null和长度等于0不一样,先判断是否为null
- 每层进行第3、4次搜索的时候先看一下是不是只有一行或一列了,不然会重复搜索
public int[] spiralOrder(int[][] matrix) { if (matrix == null || matrix.length == 0 || matrix[0].length == 0) { return new int[0]; } int rows = matrix.length; int columns = matrix[0].length; int[] ans = new int[rows * columns]; int index = 0; int left = 0, right = columns-1, top = 0, bottom = rows-1; while (left <= right && top <= bottom) { // 四个方向依次搜索 for (int i = left; i <= right; i++) { ans[index++] = matrix[top][i]; } for (int j = top+1; j <= bottom; j++) { ans[index++] = matrix[j][right]; } // 防止只有一行或只有一列的情况,会重复搜索 if (left < right && top < bottom) { for (int i = right-1; i >= left; i--) { ans[index++] = matrix[bottom][i]; } for (int j = bottom-1; j > top; j--) { ans[index++] = matrix[j][left]; } } left++; right--; top++; bottom--; } return ans; }
-
-
q547 省份数量
矩阵isConnected为图的邻接矩阵,注意与岛屿数量不同
-
dfs与bfs
public int findCircleNum(int[][] isConnected) { int r = isConnected.length; boolean[] visited = new boolean[r]; int sum = 0; Queue<Integer> queue = new LinkedList<>(); for (int i = 0; i < r; i++) { if (!visited[i]) { sum++; // dfs(isConnected, visited, i, r); queue.offer(i); while (!queue.isEmpty()) { int a = queue.poll(); for (int j = 0; j < r; j++) { if (isConnected[a][j] == 1 && !visited[j]) { visited[j] = true; queue.offer(j); } } } } } return sum; } public void dfs(int[][] isConnected, boolean[] visited, int i, int r) { for (int j = 0; j < r; j++) { if (isConnected[i][j] == 1 && !visited[j]) { visited[j] = true; dfs(isConnected, visited, j, r); } } }
-
并查集
-
-
q1091 二进制矩阵中的最短路径
用dfs和bfs的情况
如果只是要找到某一个结果是否存在,那么DFS会更高效,如果是要找所有可能结果中最短的,那么BFS会更高效
bfs模板
if (grid == null || grid.length == 0 || grid[0].length == 0 || grid[0][0] == 1) { return -1; } int len = grid.length; Queue<int[]> queue = new LinkedList<>(); queue.offer(new int[]{0, 0}); grid[0][0] = 1; int path = 1; int[][] positions = new int[][]{{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1}}; while (!queue.isEmpty()) { int size = queue.size(); // 遍历一层 for (int i = 0; i < size; i++) { int[] cur = queue.poll(); int x = cur[0]; int y = cur[1]; if (x == len-1 && y == len - 1) return path; // 八个方向 for (int[] position : positions) { int nx = x + position[0]; int ny = y + position[1]; // 判断界限 if (nx < 0 || nx >= len || ny < 0 || ny >= len || grid[nx][ny] == 1) continue; queue.offer(new int[]{nx, ny}); grid[nx][ny] = 1; } } path++; } return -1;
-
q130 被围绕的区域
既然与边缘相连的部分都不算被围绕,就从四个边开始遍历,并做一个标记,这里没有格外创建一个数组做标记,是在原数组的基础上新增一个其它的字符标记,最后只要根据字符判断填充就行了
用bfs也是一样的道理,先四个边的’O’一起入队
if (board == null || board.length == 0 || board[0].length == 0) return; int r = board.length, c = board[0].length; for (int i = 0; i < r; i++) { dfs(board, i, 0); dfs(board, i, c-1); } // 注意过滤两列 for (int j = 1; j < c-1; j++) { dfs(board, 0, j); dfs(board, r-1, j); } // 上面两个已经dfs过了,现在只要填充就行了 for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { if (board[i][j] == 'A') board[i][j] = 'O'; else if (board[i][j] == 'O') { board[i][j] = 'X'; } } }
-
华为机试 迷宫问题
看起来很复杂,但是实际理解之后编码还是不难
import java.util.*; public class Main { public static int[][] directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 一个栈保存路程 public static Deque<int[]> path; public static Deque<int[]> minPath; public static int[][] matrix; public static boolean[][] visited; public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { // 不能用sc.next(),空格直接判断为ji String[] arr1 = sc.nextLine().split(" "); int row = Integer.parseInt(arr1[0]); int col = Integer.parseInt(arr1[1]); path = new LinkedList<>(); minPath = new LinkedList<>(); matrix = new int[row][col]; visited = new boolean[row][col]; for (int i = 0; i < row; i++) { String[] arr2 = sc.nextLine().split(" "); for (int j = 0; j < col; j++) { matrix[i][j] = Integer.parseInt(arr2[j]); } } dfs(0, 0); StringBuffer sb = new StringBuffer(); for (int res[] : minPath) { sb.append('(').append(res[0]).append(',').append(res[1]).append(")\n"); } System.out.println(sb.toString()); } } private static void dfs(int i, int j) { if (i < 0 || i >= matrix.length || j < 0 || j >= matrix[0].length || visited[i][j] || matrix[i][j] == 1 || (!minPath.isEmpty() && path.size() >= minPath.size())) { return; } path.add(new int[]{i, j}); visited[i][j] = true; if (i == matrix.length-1 && j == matrix[0].length-1) { minPath = new LinkedList<>(path); path.removeLast(); return; } for (int[] direction : directions) { dfs(i+direction[0], j+direction[1]); } visited[i][j] = false; path.removeLast(); } }
-
打开转盘锁
只要求最少的步骤,用BFS,直接套模板。
public int openLock(String[] deadends, String target) {
Queue<String> queue = new LinkedList<>();
HashSet<String> visited = new HashSet<String>(); visited可以和死亡数字合二为一
queue.add("0000");
for (String str : deadends) {
if (str.equals("0000")) return -1;
visited.add(str);
}
visited.add("0000");
int time = 0;
while (!queue.isEmpty()) {
int n = queue.size();
for (int i = 0; i < n; i++) {
String s = queue.poll(); // 别写道for上面去了
// 找到了正确密码
if (s.equals(target)) return time; // 只能用equals,不能用==
// 判断是否已经访问或者是死亡数字
// if (visited.contains(s)) continue;
// 八种可能
for (int j = 0; j < 4; j++) {
String ns = plus(s, j); // char[] 转String 不能用toString,用new String()
if (!visited.contains(ns)) { // 模板是在这里判断
queue.offer(ns);
visited.add(ns);
}
String ms = sub(s, j);
if (!visited.contains(ms)) {
queue.offer(ms);
visited.add(ms);
}
}
}
time++;
}
return -1;
}
9.栈相关
-
q20 有效的括号
为什么这里的栈是用的双端队列?
Deque<Character> stack = new LinkedList<Character>();
由于Vector由于效率问题已经被弃用,因此继承Vector的Stack也存在效率问题,故不推荐使用。
再一个原因是Deque双端队列可以实现多种数据结构,完全可以模拟成栈的结构。Deque上进上出,上进下出,甚至下进上出,非常上流,只有你想不到,没有我Deque做不到的。
了解Java的集合框架,以及多态(替父从军)
这里利用Map代码更加简洁
Map<Character, Character> pairs = new HashMap<Character, Character>() {{ put(')', '('); put(']', '['); put('}', '{'); }};
-
q232用栈实现队列
没有我想象的复杂,出栈直接从右边的栈出,入栈判断左边的栈有没有满,再入左边。
与用队列实现栈差不多,主要都是处理入栈入队的操作
-
q155最小栈
我一开始以为就是简单的每次记录一下当前栈的最小值或者指向最小值的位置,但是忽略了出栈之后最小值是可能会还原的,所以必须把每次的最小值都记录下来,要开辟统一的一个栈空间保留每个位置栈的最小值,入栈的不是最小值就不管
或者另一个栈空间保存非严格递减的元素(不推荐)
注意:在Math.min(Integer.MIN_VALUE, Integer.MAX_VALUE)运算中,Integer.MAX_VALUE是最小值
-
剑指t31 栈的压入弹出序列
之前犯的两个错误
- 出栈不记得要while
- while中stack.peek之前要保证栈不为空,不然是空指针异常
两个编译器帮我优化的地方
- 最后返回的时候,不用再if(stack.isEmpty())和else了,直接return就行了
- 如果for循环内部不用用到循环索引值,改用增强for更简洁
public boolean validateStackSequences(int[] pushed, int[] popped) { // 模拟一个栈,按照push和pop操作是否最后为空 Deque<Integer> stack = new LinkedList<>(); int i = 0; for (int k : pushed) { stack.push(k); while (!stack.isEmpty() && stack.peek() == popped[i]) { stack.pop(); i++; } } return stack.isEmpty(); }
-
q84 柱状图中最大的矩形
在一维数组中对每一个数找到第一个比自己小的元素。这类“在一维数组中找第一个满足某种条件的数”的场景就是典型的单调栈应用场景
left数组记录每个位置左边第一个比他小的数的索引,right数组记录右边第一个比他小的数的索引,这里用到单调递增的栈
我丢三落四
- right[]如果栈中没有元素是n
- 把stack.peek()当作大小
- 再次使用时不记得clear栈
public int largestRectangleArea(int[] heights) { // 单调栈 int n = heights.length; Deque<Integer> stack = new LinkedList<>(); int[] left = new int[n]; int[] right = new int[n]; Arrays.fill(right, n); for (int i = 0; i < n; i++) { // 入栈之前先比较,判断是否需要出栈 while (!stack.isEmpty() && heights[stack.peek()] > heights[i]) { // 出栈之前可以更新右边界 // 最后还有一个位置没有出栈,要不格外全部赋值为n,要不在right数组初始化的时候全部先赋值为n // 有一个小细节,这里得到的right是小于而不是小于等于,但是不影响最终结果,因为如果包含等于,到最后一个i也能得到正确答案 right[stack.peek()] = i; stack.pop(); } left[i] = stack.isEmpty()? -1 : stack.peek(); stack.push(i); } // stack.clear(); // for (int i = n-1; i >= 0; i--) { // while (!stack.isEmpty() && heights[stack.peek()] >= heights[i]) { // stack.pop(); // } // right[i] = stack.isEmpty()? n : stack.peek(); // stack.push(i); // } int ans = 0; for (int i = 0; i < n; i++) { ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]); } return ans; }
优化:在出栈的时候,其实已经可以更新有边界了,此时的右边界就可以当作现在的i
-
q316 去除重复字母
自己要先模拟一遍,看是什么样的逻辑,使用单调栈,最优的情况是abcde… 用int[26]存储每个字母最后出现的位置(或者出现的次数),用boolean[26]表示此时这个字母是否在栈中。各种字符数字的转化比较麻烦
开始遍历,如果字母已经在栈中了就跳过本轮,遇到大的就入栈,如果此时的字母比栈顶的小,那么就准备把栈中比他大的全出栈,但是独苗不能出栈,使用stringbuffer模拟单调栈
10.队相关
-
q225 用队列模仿栈
只有push操作特殊,建立两个队列,每入队一个元素,都要把另一个队列的所有元素加到这个队里面,加到这个队里的所有元素实际上以及栈化了。在交互两个队列,因为队的特新,入“栈”只能一个一个处理
class MyStack {
Queue queue1;
Queue queue2;class MyStack { Queue<Integer> queue1; Queue<Integer> queue2; /** Initialize your data structure here. */ public MyStack() { queue1 = new LinkedList<Integer>(); queue2 = new LinkedList<Integer>(); } /** Push element x onto stack. */ public void push(int x) { queue2.offer(x); while (!queue1.isEmpty()) { queue2.offer(queue1.poll()); } Queue<Integer> temp = queue1; queue1 = queue2; queue2 = temp; } /** Removes the element on top of the stack and returns that element. */ public int pop() { return queue1.poll(); } /** Get the top element. */ public int top() { return queue1.peek(); } /** Returns whether the stack is empty. */ public boolean empty() { return queue1.isEmpty(); } }
-
q347前K个高频元素,同类型:剑指t40
-
使用hash+小顶堆的方式,hash想到了,不知道怎么用小顶堆优化
- 避免使用大根堆,因为你得把所有元素压入堆,复杂度是 nlogn,而且还浪费内存。如果是海量元素,那就挂了。
PriorityQueue是java中堆的api,在hash中遍历每个数出现的次数,如果比小顶堆还大,那就替换
public int[] topKFrequent(int[] nums, int k) { // 把每个元素及其出现的次数存入hash表中 Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>(); for (int num : nums) { occurrences.put(num, occurrences.getOrDefault(num, 0) + 1); } // 建立小根堆 PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() { @Override public int compare(int[] m, int[] n) { return m[1] - n[1]; } }); // 维护堆的值 for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) { int num = entry.getKey(), count = entry.getValue(); if (queue.size() == k) { if (count > queue.peek()[1]) { queue.poll(); queue.offer(new int[]{num, count}); } } else { queue.offer(new int[]{num, count}); } } // 返回结果数组 int[] res = new int[k]; for (int i = 0; i < k; i++) { res[i] = queue.poll()[0]; } return res; } // 学一下Map.Entry格式的堆 // 转化为entrySet Set<Map.Entry<Integer, Integer>> entries = map.entrySet(); // 建立小根堆 PriorityQueue<Map.Entry<Integer, Integer>> queue = new PriorityQueue<>((o1, o2) -> o1.getValue() - o2.getValue() ); // 遍历 for (Map.Entry<Integer, Integer> entry : entries) { queue.offer(entry); if (queue.size() > k) { queue.poll(); } } for (int i = 0; i < k; i++) { res[i] = queue.poll().getKey(); } return res;
-
基于快速排序的方法后面再看,大致意思懂了
-
-
剑指t49 丑数
-
使用堆,最关键的是取出x,然后将 2x, 3x, 5x 加入堆,重复的排除,第n次取出的x是正确答案,用long类型转化防止大数
public int nthUglyNumber(int n) { // 建立小顶堆 int[] factors = {2,3,5}; PriorityQueue<Long> queue = new PriorityQueue<>(); queue.add(1L); Set<Long> set = new HashSet<>(); int ugly = 0; for (int i = 0; i < n; i++) { long poll = queue.poll(); ugly = (int) poll; for (int factor : factors) { long next = factor * poll; if (set.add(next)) { queue.add(next); } } } return ugly; }
-
动态规划,计算所有可能的丑数,然后取最小的,这样计算经证明不会漏掉任何一个丑数
public int nthUglyNumber(int n) { int p2 = 1, p3 = 1, p5 = 1; int[] dp = new int[n+1]; dp[1] = 1; for (int i = 2; i <= n; i++) { int num2 = dp[p2] * 2, num3 = dp[p3] * 3, num5 = dp[p5] * 5; dp[i] = Math.min(Math.min(num2, num3), num5); if (num2 == dp[i]) p2++; if (num3 == dp[i]) p3++; if (num5 == dp[i]) p5++; } return dp[n]; }
-
-
剑指t59 队列的最大值
与栈的最大值差不多,都是用一个格外的空间存储此时的最大值,不过与栈有一点区别
本算法基于问题的一个重要性质:当一个元素进入队列的时候,它前面所有比它小的元素就不会再对答案产生影响。
Deque的左右要搞清楚,到底从左出还是往右边出
-
q239 滑动窗口最大值
-
使用堆,每次计算k个中间的最大值,滑动窗口推进的时候不急着出堆,找到候选最大值的时候再比较出堆
public int[] maxSlidingWindow(int[] nums, int k) { // 创建大根堆 PriorityQueue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { return o1[0] != o2[0] ? o2[0] - o1[0] : o2[1] - o1[1]; } }); for (int i = 0; i < k; i++) { queue.offer(new int[]{nums[i], i}); } int[] ans = new int[nums.length - k + 1]; ans[0] = queue.peek()[0]; for (int i = k; i < nums.length; i++) { queue.offer(new int[]{nums[i], i}); // 不急着出堆,如果预选答案在滑动窗口外就出堆 while (queue.peek()[1] <= i - k) { queue.poll(); } ans[i - k + 1] = queue.peek()[0]; } return ans; }
-
使用单调队列,维护一个随索引增大单调递减的序列,如果遇到大的就一个个出队,因为这些不可能为最大值,出队与方法一也是一样的判断方法
双端队列的操作不要与栈和队的一起用,会弄混
小故事:单调队列真是一种让人感到五味杂陈的数据结构,它的维护过程更是如此…就拿此题来说,队头最大,往队尾方向单调…有机会站在队头的老大永远心狠手辣,当它从队尾杀进去的时候,如果它发现这里面没一个够自己打的,它会毫无人性地屠城,把原先队里的人头全部丢出去,转身建立起自己的政权,野心勃勃地准备开创一个新的王朝…这时候,它的人格竟发生了一百八十度大反转,它变成了一位胸怀宽广的慈父!它热情地请那些新来的“小个子”们入住自己的王国…然而,这些小个子似乎天性都是一样的——嫉妒心强,倘若见到比自己还小的居然更早入住王国,它们会心狠手辣地找一个夜晚把它们通通干掉,好让自己享受更大的“蛋糕”;当然,遇到比自己强大的,它们也没辙,乖乖夹起尾巴做人。像这样的暗杀事件每天都在上演,虽然王国里日益笼罩上白色恐怖,但是好在没有后来者强大到足以干翻国王,江山还算能稳住。直到有一天,闯进来了一位真正厉害的角色,就像当年打江山的国王一样,手段狠辣,野心膨胀,于是又是大屠城…历史总是轮回的。
public int[] maxSlidingWindow(int[] nums, int k) { Deque<Integer> deque = new LinkedList<>(); for (int i = 0; i < k - 1; i++) { while (!deque.isEmpty() && nums[i] >= nums[deque.getLast()]) deque.removeLast(); deque.addLast(i); } int[] res = new int[nums.length - k + 1]; for (int i = k-1; i < nums.length; i++) { while (!deque.isEmpty() && nums[i] >= nums[deque.getLast()]) deque.removeLast(); deque.addLast(i); if (i - deque.getFirst() >= k) deque.removeFirst(); // 别忘了太远的要移除 res[i - k + 1] = nums[deque.getFirst()]; } return res; }
- 单调栈,单调队,要保存坐标,而不是具体的值,保存坐标不仅可以得到位置信息,还可以得到值。
-
11.动态规划
-
可以用动态规格求解的特点
- 可优化
- 可分解成子问题
- 子问题可分解为更小子问题
- 从上到下分析问题,从下到上解决问题
- 把已经解决的小问题存储下来
- 若可以用数学证明一种情况是最优解,可用贪婪
-
q5 最长回文子串
-
写出解决问题的状态转移方程个边界条件
nums[i-dp[i-1]-1]状态转移方程是求最长有效括号的
- P(i,j)=P(i+1,j−1)∧(Si==Sj)
- {P(i,i)=true;P(i,i+1)=(Si==Si+1)
-
思路
-
初始化
for (int i = 0; i < len; i++) { dp[i][i] = true; }
-
循环方法,边界大小从小到大循环
注意!!! 遍历过程不能从左到右,要长度从小到大,因为从左到右,中间还没得到结果
//从子串长度为2开始迭代 for(int L=2; L <= len; L++){ // 左边界 for (int i = 0; i < len; i++) { // 右边界 int j = i+L-1;
-
结果,dp值为true且左右边界最大
if (dp[i][j] && j - i + 1 > maxLen) { maxLen = j - i + 1; begin = i; }
-
-
可以用中心扩展算法,每次都从中间(边界条件)向两边扩展
这个虽然时间复杂度也是O(n²),速度比动态规划快很多,也更容易想到
-
-
q55 跳跃游戏
不看解析写的是动态规划算法,申请了一个nums长度的bool类型数组,两个for循环遍历,能到达的置true,返回最后一个位置的bool值,实际上贪心就能做,维护一个能到达的最远下标max就行了,注意i<=max
public boolean canJump(int[] nums) { int len = nums.length; int max = 0; for (int i = 0; i < Math.min(len, max+1); i++) { max = Math.max(max, i+nums[i]); if(max>=len-1){ return true; } } return false; }
跳跃游戏Ⅱ,求最短能到达的跳跃次数,也是贪心,但不知道如何代码实现
并不需要知道下一轮具体跳到哪个索引位置,只要知道这轮能够最远到哪里就行了
public int jump(int[] nums) { int n = nums.length; // 这个end很巧妙 int end = 0; int step = 0; int maxDis = 0; for (int i = 0; i < n - 1; i++) { // 注意这里不能等于,最后一个位置不要跳 maxDis = Math.max(maxDis, i + nums[i]); // 这轮能跳到的最远距离 if (end == i) { // 本轮结束 end = maxDis; step++; } } return step; }
用我自己的方法,每轮到定位具体的索引也能做,但是时间复杂度太高。同样,用递归也能做
-
q96不同的二叉搜索树
-
标准动态规划
先了解二叉搜索树的定义:它或者是一棵空树,或者是具有下列性质的二叉树: 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树。
符合动态规划解法,找到状态转移方程。由于状态转移方程里也必须要一个for循环,所以动态规划要两个for循环
G(n)=i=1∑n**F(i,n) (1)
F(i,n)=G(i−1)⋅G(n−i) (2)
G(n)=i=1∑n**G(i−1)⋅G(n−i) (3)
数组要多申请一位
-
递归
需要带备忘录,否则会重复计算,最巧妙的是递归函数形式参数是左右两个边界,闭区间
int rem[][]; public int numTrees(int n) { rem = new int[n+1][n+1]; return count(1, n); } public int count(int lo, int hi) { if (lo >= hi) return 1; int res = 0; // 查询备忘录放在递归函数出口,基本不要改变原函数 if (rem[lo][hi] != 0) { return rem[lo][hi]; } for (int i = lo; i <= hi; i++) { // 其实也可以在这里赋值备忘录,让其必有缓存 int left = count(lo, i-1); int right = count(i+1, hi); res += left * right; } // 将结果在返回之前存入备忘录 rem[lo][hi] = res; return res; }
-
-
q139单词拆分
一个跳跃版的dp问题,其实解决方法也一样的,但是这个一维数组要两个for循环才能确定,知道这题用dp解就好说了
dp[i]=dp[j] && check(s[j…i−1])
dp[0] = true; // 边界条件,如果没有一个单词,那么是true for (int i = 1; i < s.length() + 1; i++) { for (int j = 0; j < i; j++) { if (dp[j] && wordDictSet.contains(s.substring(j, i))) { dp[i] = true; break; } } }
-
q152乘积最大子数组
犯了典型错误:这个题目当前位置的最优解未必是由前一个位置的最优解转移得到的,如[-2, 3, -4]
每一次遍历都要记录以当前节点结尾的最大值以及最小值,极端思想,要么最大,要么最小,不要写多个if else判断大于还是小于,max和min函数一把梭哈
max(num[i], num[i]*mx, num[i]*mn)
public int maxProduct(int[] nums) { int[] maxf = new int[nums.length]; int[] minf = new int[nums.length]; maxf[0] = nums[0]; minf[0] = nums[0]; int max = nums[0]; for (int i = 1; i < nums.length; i++) { maxf[i] = Math.max(Math.max((nums[i] * minf[i-1]), nums[i]), Math.max((nums[i] * maxf[i-1]), nums[i])); minf[i] = Math.min(Math.min((nums[i] * minf[i-1]), nums[i]), Math.min((nums[i] * maxf[i-1]), nums[i])); max = Math.max(max, maxf[i]); } return max; // 下面的写法可以把空间复杂度降为O(1),由于第i个状态只和第i−1个状态相关,根据「滚动数组」思想,我们可以只用两个变量来维护i−1 时刻的状态,一个维护 fmax,一个维护 fmin,其他类似的动态规划问题也可以这样优化 int maxf = nums[0]; int minf = nums[0]; int max = nums[0]; for (int i = 1; i < nums.length; i++) { int mx = maxf, mn = minf; maxf = Math.max(mx * nums[i], Math.max(nums[i], mn * nums[i])); minf = Math.min(mn * nums[i], Math.min(nums[i], mx * nums[i])); max = Math.max(maxf, max); } return max; }
-
q198打家劫舍
原来dp数组存的是最大的,而不一定必须包括当前数
所以状态转移公式就是dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1])
public int rob(int[] nums) { int len = nums.length; if(len == 0) { return 0; } if(len == 1) { return nums[0]; } int[] dp = new int[len]; dp[0] = nums[0]; dp[1] = Math.max(nums[0], nums[1]); for (int i = 2; i < len; i++) { dp[i] = Math.max(dp[i-2] + nums[i], dp[i-1]); } return Math.max(dp[len - 1], dp[len-2]); }
动态规划往往可以优化空间复杂度,可以使用滚动数组,在每个时刻只需要存储前两间房屋的最高总金额
int left = nums[0]; int right = Math.max(nums[0], nums[1]); for (int i = 2; i < len; i++) { int new_right = Math.max(left + nums[i], right); left = right; right = new_right; } return right;
打家劫舍Ⅱ增加了房屋前后相接的情况,两遍动态规划,范围不同
-
q221最大正方形
没有想到他的状态转移方程:dp(i,j)=min(dp(i−1,j),dp(i−1,j−1),dp(i,j−1))+1
只有=='1’的时候才有这个状态转移方程
这里边界条件不用格外考虑,if (i == 0 || j == 0)就是边界条件
结果不是dp的最后一个,而是其中的最大值的平方
java初始化int数组默认全零
for (int i = 0; i < r; i++) { for (int j = 0; j < c; j++) { if (matrix[i][j] == '1') { if (i == 0 || j == 0) { dp[i][j] = 1; } else { dp[i][j] = Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]) + 1; } maxSide = Math.max(maxSide, dp[i][j]); } } }
-
279完全平方数
我想到了状态转移方程,但是不会转化为编码
不知道怎么找到平方和仅次于它的完全平方数,一个for循环不断找就行了,只要比i小
public int numSquares(int n) { int dp[] = new int[n + 1]; for (int i = 0; i <= n; i++) { dp[i] = i; // 比如dp[12] = min(dp[8], dp[3]) + 1,dp[8]反而更小都要算一遍 for (int j = 0; j * j <= i; j++) { dp[i] = Math.min(dp[i], dp[i - j * j] + 1); } } return dp[n]; }
-
q300最长递增子序列
-
普通dp
dp[i]为考虑前 i个元素,以第 i个数字结尾的最长上升子序列的长度,nums[i]必须被选取,这一分析是关键
dp[i]=max(dp[j])+1,其中0≤j<i且num[j]<num[i]
dp每个位置的最小值是1,没必要定义为负无穷
dp[i] = Math.max(dp[i], dp[j] + 1);
-
可以用贪心+二分优化
用d[]维持一个单调递增序列,往后扫描,每次新加进来的数尽可能小,如果nums[i] > d[len] 则更新 len = len + 1,否则用二分查找d[]中前面的有序数,找满足 d[i - 1] < nums[j] < d[i] 的下标 i,并更新 d[i] = nums[j]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ht74i5S3-1657459118132)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211103111050368.png)]
二分查找可不止用来找确定的数,用在这里也行
public int lengthOfLIS(int[] nums) { int len = 1, n = nums.length; if (n == 0) { return 0; } // 同样的数会替换同样的 int[] d = new int[n + 1]; d[1] = nums[0]; for (int i = 1; i < n; i++) { if (nums[i] > d[len]) { d[++len] = nums[i]; } else { int l = 1, r = len, pos = 0; //开始二分查最接近的小于它的位置 while (l <= r) { int mid = (l + r) / 2; if (nums[i] > d[mid]) { pos = mid; l = mid + 1; } else { r = mid - 1; } } d[pos + 1] = nums[i]; } } return len; }
升级版:q673,同样两种方法,一个复杂度高一个复杂度低
方法一:本题动态规划的复杂化,添加一个dp数组的辅助数组cnt,代表以nums[i]结尾的最长递增子序列的个数,最后根据两个数组计算出最终值,画下图就清晰了
-
-
最佳买卖股票时机含冷冻期
用 f[i]表示第i 天结束之后的「累计最大收益」,有三种状态
-
我们目前持有一支股票,对应的「累计最大收益」记为 f[i] [0];
-
我们目前不持有任何股票,并且处于冷冻期中,对应的「累计最大收益」记为 f[i] [1];
-
我们目前不持有任何股票,并且不处于冷冻期中,对应的「累计最大收益」记为 f[i] [2]。
f[i] [0] = max(f[i-1] [0], f[i-1] [2] - prices[i])
昨天持有一支股票不卖 或 昨天不在冷冻期,今天买了
f[i] [1] = f[i-1] [0]+prices[i]
就是昨天卖了,所以在冷冻期
f[i] [2] = max(f[i-1] [1], f[i-1] [2])
昨天刚卖 或 昨天之前就卖了
最终答案是max(f[n-1] [1], f[n-1] [2]),最后一天持有股票无意义,冷冻期代表这天也没意义,所以就是最后一个
public int maxProfit(int[] prices) { if (prices.length == 0) { return 0; } int n = prices.length; // f[i][0]: 手上持有股票的最大收益 // f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益 // f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益 int[][] f = new int[n][3]; f[0][0] = -prices[0]; for (int i = 1; i < n; ++i) { f[i][0] = Math.max(f[i - 1][0], f[i - 1][2] - prices[i]); f[i][1] = f[i - 1][0] + prices[i]; f[i][2] = Math.max(f[i - 1][1], f[i - 1][2]); } return Math.max(f[n - 1][1], f[n - 1][2]); }
空间优化
f[i] [?]只与f[i-1] [?]有关,而与 f[i-2] [?]及之前的所有状态都无关,所以只需要将f[i-1] [?]存放在三个变量中就行了
int n = prices.length; int f0 = -prices[0]; int f1 = 0; int f2 = 0; for (int i = 1; i < n; ++i) { int newf0 = Math.max(f0, f2 - prices[i]); int newf1 = f0 + prices[i]; int newf2 = Math.max(f1, f2); f0 = newf0; f1 = newf1; f2 = newf2; } return Math.max(f1, f2);
- 动态规划,空间都可以优化一维
-
-
q322零钱兑换
-
直接使用回溯会超时,需要借助数组做记忆化搜索,属于优化的自上而下
状态转移方程F(S)=F(S−C)+1
public int coinChange(int[] coins, int amount) { return coinChange(coins, amount, new int[amount]); } // 递归就是借债,让子孙还 private int coinChange(int[] coins, int amount, int[] count) { if (amount < 0) { return -1; } if (amount == 0) { return 0; } if (count[amount-1] != 0) { return count[amount-1]; } int min = Integer.MAX_VALUE; for (int coin : coins) { int res = coinChange(coins, amount - coin, count); if (res >= 0 && res < min) { min = res + 1; } } count[amount-1] = min == Integer.MAX_VALUE ? -1 : min; return count[amount-1]; // 这是不做记忆化搜索的情况 // return min == Integer.MAX_VALUE ? -1 : min; }
-
自下而上的动态规划
F(i)=minF(i−cj)+1 j=0…n−1
我的写法,第二次还是这个写法
public int coinChange(int[] coins, int amount) { int[] dp = new int[amount + 1]; Arrays.fill(dp, -1); dp[0] = 0; for (int i = 1; i <= amount; i++) { int minCoin = Integer.MAX_VALUE; for (int coin : coins) { if (i - coin >= 0 && dp[i - coin] >= 0) { minCoin = Math.min(minCoin, dp[i - coin] + 1); } } dp[i] = minCoin == Integer.MAX_VALUE ? -1 : minCoin; } return dp[amount]; }
官方题解更巧妙,数组初始就赋值为amount + 1,方便找最小值
public int coinChange(int[] coins, int amount) { int max = amount + 1; int[] dp = new int[amount + 1]; Arrays.fill(dp, max); dp[0] = 0; for (int i = 1; i <= amount; i++) { for (int j = 0; j < coins.length; j++) { if (coins[j] <= i) { dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1); } } } return dp[amount] > amount ? -1 : dp[amount]; }
-
-
q416分割等和子集
其实可以转化为01背包问题,里面的数可以组合为恰好总和的一半,本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。dp中,行 为选或不选这个数,列 为总和
dp初始化的时候,容量什么时候给n位,什么时候给n+1位,这里的行给n,列给n+1,因为n+1方便运算
返回false的情况
- 有一个数超过了总和的一半
- 只有1或0个数
- 总和为奇数
判断当前数是否大于总和的一半可以少判断一些 if (j >= nums[i])
public boolean canPartition(int[] nums) { // 先相加,判断奇偶 int sum = 0; for (int num : nums) { sum += num; } if (sum % 2 == 1) return false; int target = sum / 2; int[][] dp = new int[nums.length+1][target+1]; int n = nums.length; // 背包问题模板,最大可以放入的价值,可以认为重量等于价值 for (int i = 1; i <= n; i++) { // 选择的重量 for (int j = 1; j <= target; j++) { // 背包容量 if (nums[i-1] > j) { dp[i][j] = dp[i-1][j]; } else { dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-nums[i-1]] + nums[i-1]); } } } return dp[n][target] == target; }
-
剑指t46 把数字翻译为字符串,类似于爬楼梯
-
常规dp写法
String s = String.valueOf(num); int[] dp = new int[s.length()+1]; dp[0] = 1; dp[1] = 1; for (int i = 2; i <= s.length(); i++) { String tmp = s.substring(i-2, i); // 这样比较字符串大小 if (tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0) { dp[i] = dp[i-2] + dp[i-1]; } else dp[i] = dp[i-1]; } return dp[s.length()];
-
dp优化写法
String s = String.valueOf(num); //a相当于dp[i-2] b相当于dp[i-1] c相当于dp[i] int a = 1,b = 1,c; for (int i = 2; i <= s.length(); i++) { String tmp = s.substring(i-2, i); c = (tmp.compareTo("10") >= 0 && tmp.compareTo("25") <= 0) ? a + b : b; a = b; b = c; } return b;
-
dfs写法,再研究一下,我的弱项
写法一: public int translateNum(int num) { //将字符串转化为数字 String str = String.valueOf(num); //dfs遍历字符串求解 return dfs(str, 0); } //表示从index位置开始有好多种翻译方法 public int dfs(String str, int index){ //如果当前的下标大于等于字符串的长度 - 1,则说明当前位置是由上一次跳到此处的,则直接返回1 if(index >= str.length() - 1) return 1; //先求解每一次都翻译一个字符的方法数 int res = dfs(str, index + 1); //以当前字符的下标为开始,截取两位,判断这位组成的数字是否在10~25之间 //如果在这一次就可以直接翻译两个字符,然后从两个字符后面的位置开始翻译 String temp = str.substring(index, index + 2); if(temp.compareTo("10") >= 0 && temp.compareTo("25") <= 0) res += dfs(str, index + 2); return res; } 写法二: int translateNum(int num) { return f(num); } int f(int num) { if (num < 10) { return 1; } if (num % 100 < 26 && num % 100 > 9) { return f(num / 10) + f(num / 100); } else { return f(num / 10); } }
-
-
剑指t60 n个骰子的点数
-
分析:找到状态转移方程,当只有两个骰子时,总和为4的概率f(2,4) = f(1,3)/6+f(1,2)/6+f(1,1)/6
运算为double,整数必须带上.0
还有一种非常规正向递归的写法
-
常规dp
public double[] dicesProbability(int n) { // 总和范围:(n, 6*n) double[] res = new double[5*n+1]; double[][] dp = new double[n+1][6*n+1]; for (int i = 1; i <= 6; i++) { dp[1][i] = 1.0/6; } // i为骰子数,j为总和,k为这次骰子的点数 for (int i = 2; i <= n; i++) { for (int j = i; j <= 6 * i; j++) { for (int k = 1; k <= 6; k++) { if ((j-k) > 0) { dp[i][j] += dp[i-1][j-k] / 6; } else break; } } } for (int i = 0; i < 5 * n + 1; i++) { res[i] = dp[n][n+i]; } return res; }
-
优化为一维数组
// 优化为一维数组,运算时只保留上一行的结果 double[] res = new double[5*n+1]; // 数组最长度不变 double[] dp = new double[6*n+1]; for (int i = 1; i <= 6; i++) { dp[i] = 1.0/6.0; } for (int i = 2; i <= n; i++) { for (int j = i * 6; j >= i; j--) { // 必须先清0,不然影响结果 dp[j] = 0; // 从后向前,可以不覆盖 for (int k = 1; k <= 6; k++) { if ((j-k) >= i-1) { dp[j] += dp[j-k] / 6.0; } else break; } } } for (int i = 0; i < 5 * n + 1; i++) { res[i] = dp[n+i]; } return res;
-
-
q72 编辑距离
太抽象了,dp[i] [j]表示word1的前i位到word2的前j位的编辑距离
public int minDistance(String word1, String word2) { // dp[i][j] word1的前i位到word2的前j位的编辑距离 int n = word1.length(); int m = word2.length(); if (n == 0 || m == 0) return n + m; // 必须要保留0索引,因为第一个索引就有可能相同 int[][] dp = new int[n+1][m+1]; for (int i = 0; i <= n; i++) { dp[i][0] = i; } for (int i = 0; i <= m; i++) { dp[0][i] = i; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { // 状态转移方程 if (word1.charAt(i-1) == word2.charAt(j-1)) { dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1] - 1); } else dp[i][j] = 1 + Math.min(Math.min(dp[i-1][j], dp[i][j-1]), dp[i-1][j-1]); } } return dp[n][m]; }
-
戳气球
逆向思维,可以当作把气球放进去
状态转移方程我自己是绝对想不到的,现在还有点不理解,为什么是i,j 不是k-1,k+1
为什么i要从n-1开始?如果i和j都从左边开始,后面的rec还是初始值
也可以使用递归的方法,自上而下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h7MpGtFP-1657459118133)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211207101137046.png)]
public int maxCoins(int[] nums) { int n = nums.length; int[][] rec = new int[n+2][n+2]; int[] val = new int[n+2]; for (int i = 0; i < n; i++) { val[i+1] = nums[i]; } // 补充边界 val[0] = val[n+1] = 1; for (int i = n - 1; i >= 0; i--) { for (int j = i+2; j <= n+1; j++) { for (int k = i+1; k < j; k++) { int sum = val[i] * val[k] * val[j]; sum += rec[i][k] + rec[k][j]; rec[i][j] = Math.max(sum, rec[i][j]); } } } return rec[0][n+1]; }
-
q413 等差数列划分
暴力法的时间复杂度为O(n²),优化思路:以 1 2 3 4 为例
1 2 3 是等差数列,结果+1,因为遍历到后面4-3 == 3-2,所以结果不仅包括前一种状态(1 2 3),还包括前一种状态加一(2 3 4 与 1 2 3 4) => 1+(1+1) = 3
if (nums.length < 3) return 0; int ans = 0, t = 0; // t就相当于dp的压缩 int d = nums[1] - nums[0]; for (int i = 2; i < nums.length; i++) { if (nums[i] - nums[i-1] == d) { t++; } else { t = 0; d = nums[i] - nums[i-1]; } ans += t; } return ans;
-
q91解码方法
想不到这个状态转移方程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXfTEbWa-1657459118133)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211221154125816.png)]
public int numDecodings(String s) { int len = s.length(); int[] dp = new int[len + 1]; dp[0] = 1; // 这一这个边界条件,不然测试用例 "12" 会有问题 for (int i = 1; i <= len; i++) { if (s.charAt(i-1) != '0') { dp[i] += dp[i-1]; // 这个+可以不带 } if (i > 1 && s.charAt(i-2) != '0' && (s.charAt(i-2) - '0') * 10 + (s.charAt(i-1) - '0') <= 26) { dp[i] += dp[i-2]; } } return dp[len]; }
使用三个变量可以优化为一维
-
q1143 最长公共子序列
其实只要稍微画下图,就知道这个状态转移方程了
二维数组多一行一列可以不用初始化第一行和第一列,这是一个小技巧
可以优化为i层和i-1层两个一维数组,注意数组交替不可以pre = cur,数组是引用传递,结果跟只有一个数组一样
-
q583 两个字符串的删除操作
编辑举例的弱化版,也可以按照q1143的方法直接求得,return m - lcs + n - lcs;
也可以使用动态规划,画个图找下规律,状态转移方程就出来了
-
q516 最长回文子序列
状态转移方程遍历方式是从下到上,从左到右
dp数组可以二维压缩到一维,保存好二维中左下的值
-
q174 地下城游戏
这题最小路径和的区别就是,中间任何一个状态都不能让血量为负,所以如果把dp[i] [j]定义为从0->(i,j)需要耗费的最小血量行不通,因为过程中就可能已经寄了,所以必须重新定义为(i,j) 到终点所需要的最小血量,公主奔赴到骑士。
初始化这里也是重点,因为求最小的,所以初始值可以定义为Integer.MAX_VALUE,但是终点很关键,dp[n-1] [m-1]需格外定义
dp[n-1][m-1] = dungeon[n-1][m-1] < 0 ? -dungeon[n-1][m-1] + 1 : 1;
计算过程中,如果小于等于0了,就直接赋值为1,因为不能小于1
-
q787 K站中转内最便宜的航班
使用递归,由于可以消除重叠子问题,优化为自下而上的动态规划
递归把问题分解,通过终点的入度,一步步前进到起点
常量尽量定义为全局的
由于要通过结点找入度和权值,把节点当作key,入度节点和权值当作value,方便快速查找、
public int dp(int s, int k) { if (s == src) { return 0; } if (k == 0) { return -1; } int res = Integer.MAX_VALUE; if (map.containsKey(s)) { // 找到所有入度 for (int[] a : map.get(s)) { int from = a[0]; int price = a[1]; int all = dp(from, k-1); if (all != -1) { res = Math.min(res, all + price); // price是这次的,all是之前所有的 } } } return res == Integer.MAX_VALUE ? -1 : res; }
-
q10 正则表达式匹配
比较复杂的动态规划,用递归的方法分析更容易点,一步步分解问题,更容易找到状态转移方程
这里从前往后递归,符合人正向思维的逻辑,匹配过程中相同有两种情况,不同也有两种情况(labuladong题解)
如果从后往前也行,状态转移方程就要改一下,更方便写出从下往上的动态规划(leetcode题解)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v0vrqDAj-1657459118133)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20220518122452622.png)]
-
q926 将字符串翻转到单调递增
这题的第一反应是动态规划或者回溯,然后想到的是解密码锁的做法,但BFS超时了
数组直接复制的话是不行的,一改具改,需要使用copyOf方法
BFS如果需要记录层数,poll方法需要在while里的for循环里面
动态规划方程真想不到
public int minFlipsMonoIncr(String s) { int n = s.length(); //多加一列初始状态 int[][] dp = new int[n + 1][2]; char[] chars = s.toCharArray(); //计算变换次数 for (int i = 1; i <= n; i++) { if (s.charAt(i - 1) == '0') { dp[i][0] = dp[i - 1][0]; dp[i][1] = Math.min(dp[i - 1][1], dp[i - 1][0]) + 1; // '0'转为'1'要变换一次 } else { dp[i][0] = dp[i - 1][0] + 1; dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][1]); } } return Math.min(dp[n][0], dp[n][1]); }
还有一种更巧妙的方法,利用前缀和,找到一个点,前面1的个数和后面0的个数最少,就是答案。
public int minFlipsMonoIncr(String s) { int n = s.length(); // 这个点不是正好对应哪个数,而是两个数之间的点 int[] cnt1 = new int[n + 1]; int[] cnt2 = new int[n + 1]; int count1 = 0; int count0 = 0; for (int i = 0; i < n; i++) { if (s.charAt(i) == '1') { count1++; } if (s.charAt(n - i - 1) == '0') { count0++; } cnt1[i + 1] = count1; cnt2[n - i - 1] = count0; } int res = Integer.MAX_VALUE; for (int i = 0; i <= n; i++) { res = Math.min(res, cnt1[i] + cnt2[i]); } return res; }
12.位运算
-
二进制中1的个数
- 把数字右移后与1比较
- 用1比较后1左移再比较(防止负数)
- -1再与自身相与,直到为0
-
q128只出现一次的数字
使用异或的性质
(a1⊕a1)⊕(a2⊕a2)⊕⋯⊕(a**m⊕a**m)⊕a**m+1
0⊕0⊕⋯⊕0⊕a**m+1=a**m+1
-
q338比特位计数
-
如果 i 为偶数,那么f(i) = f(i/2)
如果 i 为奇数,那么f(i) = f(i - 1) + 1 -
Brian Kernighan 算法
while (x > 0){x &= (x - 1);ones++;}
-
最高比特位
比如s(13) = s(8) + s(5), 8是13的最高比特位,里面的1只有一个,我没想到快速找到最高有效位的方法,还以为每次要格外计算,其实可以依赖于遍历过程,用到了2的算法
public int[] countBits(int n) { int[] dp = new int[n+1]; dp[0] = 0; int highBit = 0; for (int i = 1; i < n + 1; i++) { // 找到i的最高有效位 if ((i & (i-1)) == 0) { highBit = i; } dp[i] = dp[i - highBit] + 1; } return dp; }
-
最低比特位
如果 xx 是偶数,则bits[x] = bits[x/2]
如果 xx 是奇数,则bits[x] = bits[x/2]+1
bits[i] = bits[i >> 1] + (i & 1);
-
最低设置位,本质上也是2方法的算法
i & (i - 1)可以去掉i最右边的一个1(如果有),因此 i & (i - 1)是比 i 小的,而且i & (i - 1)的1的个数已经在前面算过了,所以i的1的个数就是 i & (i - 1)的1的个数加上1
y = x & (x-1) => bits[x] = bits[y] + 1
bits[x] = bits[x&(x-1)] + 1
-
-
q461 汉明距离
不会做的原因就是因为不知道有按位与运算符,这是这题的基础z = x ^ y,只有只要求z的1的位数就行了。不管是调轮子还是Brian Kernighan 算法
-
剑指t16 数组的整数次方
这题考的是各种特殊值的考虑以及算法加速优化
-
递归方法
public double myPow(double x, int n) { if (x == 0 && n < 0) { return 0; } long absEx = n; if (n < 0) absEx = -absEx; // 不能用absEx = -n,类型不同 double res = getPow(x, absEx); if (n < 0) { res = 1 / res; } return res; } private double getPow(double x, long absEx) { //递归出口 if (absEx == 0) { return 1; } if (absEx == 1) { return x; } double ans = getPow(x, absEx>>1); ans *= ans; // absEx 奇数或偶数 if ((absEx & 1) == 1) { ans *= x; } return ans; }
-
快速幂解析
double res = 1.0; while (absEx > 0) { if ((absEx & 1) == 1) { res *= x; } x *= x; absEx >>= 1; } return res;
-
-
剑指t56 数组中数字出现的次数
如果只有一个数字只出现一次,其它都出现两次,那么全部异或得到的就是这个只出现一次的数字,如果有两个数字只出现一次,那么就要分组,分组的原则时这两个数字不在一个组中,而且同一组其它数字必须出现两次,那么就使用一个巧妙的方法区分,给数组全部异或,得到两个只出现一次的数字的异或值,任选一个位,作为所有数的区分,分开两个组,分别异或就是结果
-
HJ33 整数与IP地址间的转换
我的想法是一步步转,ip转为二进制,二进制拼接,再转为十进制;十进制转为二进制,二进制切割,再转为ip;太麻烦
实际上都不用转,ip切分为十进制后直接循环*256+就行了;十进制直接%256+就行了
public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { String s = sc.next(); if (s.contains(".")) { String[] str = s.split("\\."); long result = 0; for (int i = 0; i < 4; i++) { result = result*256 + Integer.parseInt(str[i]); } System.out.println(""+result); } else { long a = Long.parseLong(s); String result = ""; while (a > 0) { result = a % 256 + "." + result; a /= 256; } System.out.println(result.substring(0, result.length()-1)); } } }
13.回溯
-
回溯、深度优先、递归、动态规划
-
回溯通常采用最简单的递归。「回溯算法」强调了「深度优先遍历」思想的用途,用一个 不断变化 的变量,在尝试各种可能的过程中,搜索需要的结果。强调了 回退 操作对于搜索的合理性。
-
深度优先搜索:当结点
v
的所在边都己被探寻过,搜索将 回溯 到发现结点v
的那条边的起始结点。 -
动态规划只需要求我们评估最优解是多少,最优解对应的具体解是什么并不要求。因此很适合应用于评估一个方案的效果
-
什么时候使用used数组,什么时候使用begin变量
排列问题,讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为不同列表时),需要记录哪些数字已经使用过,此时用 used 数组;
组合问题,不讲究顺序(即 [2, 2, 3] 与 [2, 3, 2] 视为相同列表时),需要按照某种顺序搜索,此时使用 begin 变量。
-
-
q22 括号生成
-
最简单的就是暴力法,从用2*n个’('开始,每次满了都判断一下是否合理
- 合理的判断函数为:遇到左括号加一,右括号减一,如果中途小于零就不合理,最后结果为零为合理
-
回溯法
如果左括号数量不大于 n,我们可以放一个左括号。如果右括号数量小于左括号的数量,我们可以放一个右括号。不用再格外判断
public void backtrack(List<String> res, StringBuilder cur, int open, int close, int max){ // 得到某一结果 if(cur.length() == 2*max){ res.add(cur.toString()); return; } if(open < max){ cur.append('('); backtrack(res, cur, open+1, close, max); cur.deleteCharAt(cur.length()-1); } if(close < open){ cur.append(')'); backtrack(res, cur, open, close+1, max); cur.deleteCharAt(cur.length()-1); } }
以上是在if条件里回溯,递归出口可以少一些判断,如果直接回溯,那么递归出口处要多些判断return
-
-
q39数组总和
这题的关键思想就是如何画这个回溯树,把target当树根节点,减去某个candidates的值当子节点,结果为零的叶子节点为一个解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-01uif0Iw-1657459118134)(https://pic.leetcode-cn.com/1598091943-GPoHAJ-file_1598091940246)]
代码
public List<List<Integer>> combinationSum(int[] candidates, int target) { int len = candidates.length; List<List<Integer>> res = new ArrayList<>(); if(len == 0){ return res; } Deque<Integer> path = new ArrayDeque<>(); dfs(candidates, 0, len, target, path, res); return res; } /** * @param candidates 候选数组 * @param begin 搜索起点 * @param len 冗余变量,是 candidates 里的属性,可以不传 * @param target 每减去一个元素,目标值变小 * @param path 从根结点到叶子结点的路径,是一个栈 * @param res 结果集列表 */ private void dfs(int[] candidates, int start, int len, int target, Deque<Integer> path, List<List<Integer>> res) { if(target<0){ return; } if(target==0){ res.add(new ArrayList<>(path)); return; } // 重点理解这里从 begin 开始搜索的语意 for (int i = start; i < len; i++) { path.addLast(candidates[i]); // 注意:由于每一个元素可以重复使用,下一轮搜索的起点依然是 i,这里非常容易弄错 dfs(candidates, i, len, target-candidates[i], path, res); path.removeLast(); } } }
剪枝:
先排序,在for循环开始检测
if(target-candidates[i]<0){ break; }
target小于0的递归出口就可以删除了
如果元素不能重复选取,而且结果必须唯一,那么只要在for循环加两句
if(i > cur && candidates[i] == candidates[i-1]) continue; // i>cur证明此时的i,i-1只能是同一层
backtrack(candidates, target - candidates[i], i + 1, res, path); //i改为i+1
-
q46全排列
递归传递的参数有
候选数组、数组长度(也可以由候选数组得出)、递归深度、递归中的路径、结果集列表
如果有重复元素,两个方法去重,一是加入结果集之前判重(剑指t38这样做超时了),二是先排序,如果跟之前这个相同那么就跳过
遵循上一题的递归套路写法,我主要有两个问题
- 递归出来后,路径不记得remove最后一个
- res.add(path); 的写法有问题,应写为res.add(new ArrayList<>(path)); 详情见以下:
java知识点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DYFWheVx-1657459118134)(https://gitee.com/chenzhijain/picgo/raw/master/pic/image-20211012200830758.png)]
以上的ls是一个新的指针指向了list1,类似于
ArrayList ls = new ArrayList();
ls = list1
path进入参数后,表示不同的指针指向同一个地址,每次add进去都是指向的同一个地址,所以一但那个地址中的值改变,res中所有的值会一起改变,最后回到根节点后会变为全空数组
还有一种思考方式,剑指offer的38题官方解析,交互start后一位和后面的数,一定不重复,递归的时候原char[]数组变化
class Solution { List<String> rec; boolean[] used; public String[] permutation(String s) { char[] ch = s.toCharArray(); StringBuilder str = new StringBuilder(); rec = new ArrayList<>(); used = new boolean[s.length()]; Arrays.sort(ch); dfs(ch, 0, str); String[] ans = new String[rec.size()]; for (int i = 0; i < rec.size(); i++) { ans[i] = rec.get(i); } return ans; } private void dfs(char[] ch, int deep, StringBuilder str) { int length = ch.length; if (deep == length) { String s = new String(ch); if (!rec.contains(s)) { rec.add(s); } } for (int i = deep; i < length; i++) { swap(ch, deep, i); dfs(ch, deep+1, str); swap(ch, deep, i); // if (used[i] || (i > 0 && !used[i - 1] && ch[i - 1] == ch[i])) { // continue; // } // used[i] = true; // str.append(ch[i]); // dfs(ch, deep+1, str); // used[i] = false; // str.deleteCharAt(str.length()-1); } } private void swap(char[] ch, int i, int j) { char temp = ch[i]; ch[i] = ch[j]; ch[j] = temp; } }
如果nums可包含重复数字,先把nums排序,添加!visited[i-1])条件跳过的原因是,如果访问过前一个相同的元素了,证明是子节点,那么可以继续在后面追加,如果没有访问过,证明是同一层,可以跳过
if ((i != 0 && nums[i] == nums[i-1] && !visited[i-1]) || visited[i]) { continue; }
额,搞不懂,这么写不是最简单吗
List<List<Integer>> res = new LinkedList<>(); LinkedList<Integer> list = new LinkedList<>(); public List<List<Integer>> permute(int[] nums) { // 没有start backtracking(nums); return res; } public void backtracking(int[] nums) { if (list.size() == nums.length) { res.add(new ArrayList<>(list)); return; } for (int i = 0; i < nums.length; i++) { if (list.contains(nums[i])) continue; list.add(nums[i]); backtracking(nums); list.removeLast(); } }
在排列组合问题上,start和used是两大法宝
-
q53最大子序和
一个简单的动态规划题目,关键是如何找到状态转移方程
选取子问题要无后效性
- 子问题 1:以 -2 结尾的连续子数组的最大和是多少;
- 子问题 2:以 -1 结尾的连续子数组的最大和是多少;
- …
这些子问题之间就有了联系
之前有一个问题没想通,以为以某个数结尾的连续子数组的最大和是前面的最大值,其实如果这个数小于零,那么一定小于前面的数
本题可以用分治法,后面再尝试写
-
q78子集
首先想到的是递归,可以套q46全排列的模板,官方题解有点不一样。官方题解的思路:一个数字有选与不选两种状态
List<List<Integer>> ans = new ArrayList<>(); List<Integer> t = new ArrayList<>(); public List<List<Integer>> subsets(int[] nums) { dfs(0, nums); return ans; } private void dfs(int cur, int[] nums){ if (cur == nums.length) { ans.add(new ArrayList<Integer>(t)); return; } // 考虑选择当前位置 t.add(nums[cur]); dfs(cur + 1, nums); // 考虑不选择当前位置 t.remove(t.size() - 1); dfs(cur + 1, nums); }
中间走的路径和结果集遍历放在了全局,len在递归里面再计算,所以递归里面少写了三个变量
区别是,不用for循环,但是多写一个递归函数,更难理解
常规递归写法,可以看作是一颗树,int i = cur是为了不重复,遍历到树的任何一个节点都add,for是树的同一层,dfs是子树
public List<List<Integer>> subsets(int[] nums) { List<List<Integer>> ans = new ArrayList<>(); dfs(0, nums, new ArrayList<Integer>(), ans); return ans; } private void dfs(int cur, int[] nums, ArrayList<Integer> t, List<List<Integer>> ans){ ans.add(new ArrayList<Integer>(t)); for (int i = cur; i < nums.length; i++) { t.add(nums[i]); dfs(i + 1, nums, t, ans); t.remove(t.size() - 1); } }
还有一种符合这题的解题思路,就是利用二进制
以123为例,011–‘23’ 101–‘13’ …
如果nums中可能含有重复元素,三种去重的方法
- 先排序,比较nums[i]==nums[i-1]
- 先排序,如果需要重复就移除,并return
- 先排序,直接用set
为什么都要先排序,因为就算是set集合,如果list是两个含有相同元素的无序列表,也是认为是不重复的
-
q301 删除无效的括号
这题纯暴力倒还好,我DFS和BFS都能写出来,但是剪枝比较难
-
dfs,最好一开始就计算好左右括号分别需要去掉多少个
class Solution { private List<String> res = new ArrayList<>(); public List<String> removeInvalidParentheses(String s) { // 计算分别需要去掉的左右括号数 int lremove = 0; int rremove = 0; // 可以计算出左右括号分别需求删除多少个 for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') lremove++; else if (s.charAt(i) == ')') { if (lremove > 0) { lremove--; } else rremove++; } } helper(s, 0, lremove, rremove); return res; } // 递归函数 private void helper(String str, int start, int lremove, int rremove) { if (lremove == 0 && rremove == 0 && isValid(str)) { res.add(str); return; } // 这个for循环转化为了普通dfs模板 for (int i = start; i < str.length(); i++) { // 剪枝 if (i != start && str.charAt(i) == str.charAt(i-1)) continue; // for循环下同一层的剪枝 if (lremove + rremove > str.length() - i) return; // 尝试去掉一个左括号 if (lremove > 0 && str.charAt(i) == '(') { // 为什么下面start参数传的是i而不是i+1,因为str少了一位,实际上就相当于i+1 helper(str.substring(0, i) + str.substring(i + 1), i, lremove - 1, rremove); } if (rremove > 0 && str.charAt(i) == ')') { helper(str.substring(0, i) + str.substring(i+1), i, lremove, rremove-1); } } } // 判断括号是否正确,不需要用到栈 boolean isValid(String s) { int cnt = 0; for (int i = 0; i < s.length(); i++) { if (s.charAt(i) == '(') { cnt++; } else if (s.charAt(i) == ')') { cnt--; } if (cnt < 0) return false; } return cnt == 0; } }
-
bfs
List<String> res = new ArrayList<>(); Set<String> currSet = new HashSet<>(); currSet.add(s); while (true) { for (String str : currSet) { if (isValid(str)) res.add(str); } // 求最短的,肯定是最后一次遍历得到所有结果 if (res.size() > 0) return res; Set<String> nextSet = new HashSet<>(); for (String str : currSet) { for (int i = 0; i < str.length(); i++) { if (i > 0 && str.charAt(i) == str.charAt(i-1)) continue; if (str.charAt(i) == '(' || str.charAt(i) == ')') { nextSet.add(str.substring(0, i) + str.substring(i+1)); } } } currSet = nextSet; }
-
-
q257 二叉树的所有路径
数字+箭头不太好处理, 不是一对一的,把最后一个数字单独处理
public List<String> binaryTreePaths(TreeNode root) { List<String> res = new LinkedList<>(); List<Integer> path = new ArrayList<>(); if (root == null) return null; traversal(root, path, res); return res; } void traversal(TreeNode root, List<Integer> path, List<String> res) { path.add(root.val); if (root.left == null && root.right == null) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < path.size() - 1; i++) { // 一个数字,一个箭头 sb.append(path.get(i)); sb.append("->"); } sb.append(path.get(path.size()-1)); res.add(sb.toString()); } if (root.left != null) { traversal(root.left, path, res); path.remove(path.size()-1); } if (root.right != null) { traversal(root.right, path, res); path.remove(path.size()-1); } }
本题是递归加回溯,递归和回溯是一一对应的关系,有递归就有回溯,所以写在同一个大括号内部
-
q404 左叶子之和
分别使用纯回溯以及递归的方法
int sum = 0; public int sumOfLeftLeaves(TreeNode root) { // if (root == null) return 0; // getSum(root); // return sum; if (root == null) return 0; int leftSum = sumOfLeftLeaves(root.left); int rightSum = sumOfLeftLeaves(root.right); int midSum = 0; if (root.left != null && root.left.left == null && root.left.right == null) { // 处理逻辑 midSum = root.left.val; } return midSum + leftSum + rightSum; } public void getSum(TreeNode root) { if (root == null) return; if (root.left != null && root.left.left == null && root.left.right == null) { // 处理逻辑 sum += root.left.val; } sumOfLeftLeaves(root.left); sumOfLeftLeaves(root.right); }
-
q77 组合
如何剪枝,如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
1. 已经选择的元素个数:path.size();
2. 还需要的元素个数为: k - path.size();
3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历
为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。
举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。
从2开始搜索都是合理的,可以是组合[2, 3, 4]。
这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。
所以优化之后的for循环是:
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
这种递归函数一般都有一个参数,这个参数用来记录本层递归的中,集合从哪里开始遍历,可以根据要求调整范围
List是没有removeLast方法的,LinkedList才有
-
q37 解数独
类似于n皇后,但是这个只要找到一个解就行了,所以可以给回溯函数加个返回值,为true直接递归出来
再总结了几种遍历方法
走有障碍物的迷宫,因为不用全部走到
int[][] directions = new int[][]{{0,1},{0,-1},{1,0},{-1,0}};
本题的全都要走一遍,有两种方法
一是用两个for,二是直接回溯后判断
回溯默认是先判断走这一步没问题再回溯,而不是回溯完再判断,回溯完再判断的是是否越界什么的
public boolean backTracking(char[][] board, int row, int col) { if (col == 9) return backTracking(board, row+1, 0); if (row == 9) return true; if (board[row][col] != '.') return backTracking(board, row, col+1); for (int i = 1; i <= board.length; i++) { if (!isValue(board, row, col, (char)(i+'0'))) continue; board[row][col] = (char)(i+'0'); if (backTracking(board, row, col+1)) return true; board[row][col] = '.'; } return false; } bool backtracking(vector<vector<char>>& board) { for (int i = 0; i < board.size(); i++) { // 遍历行 for (int j = 0; j < board[0].size(); j++) { // 遍历列 if (board[i][j] != '.') continue; for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适 if (isValid(i, j, k, board)) { board[i][j] = k; // 放置k if (backtracking(board)) return true; // 如果找到合适一组立刻返回 board[i][j] = '.'; // 回溯,撤销k } } return false; // 9个数都试完了,都不行,那么就返回false } } return true; // 遍历完没有返回false,说明找到了合适棋盘位置了 }
-
q17 电话号码的字母组合
几个月之后凭自己做出来了!!因为记住了几个方法
不变的参数尽量不要放在递归函数中,放在全局里面
画一遍递归树,for是同一层,递归函数是从父到子,心理想一下各自怎么实现。这里for中大小就是这个键的字母数,3或者4,函数进去一层就加一个字母
-
滴滴2023暑期实习笔试第一题
求数组里在不改变顺序的情况下是否可以划分为一个单调递增,一个单调递增的两批数
private static boolean dfs(int[] arr, int index, Stack<Integer> up, Stack<Integer> down) { if( arr.length == index ) return true; int num = arr[index]; if( up.isEmpty() || num > up.peek() ){ up.push( num ); if( dfs( arr, index + 1, up, down) ) return true; up.pop(); } if( down.isEmpty() || num < down.peek() ){ down.push( num ); if( dfs( arr, index + 1, up, down) ) return true; down.pop(); } return false; }
-
q47全排列Ⅱ
基本是与全排列Ⅰ一样,只是Ⅱ有重复元素,但是答案不能重复,所以要保证相同元素的相对位置固定
所以剪枝要加一个条件
// 新添加的剪枝逻辑,固定相同的元素在排列中的相对位置 if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) { // 如果前面的相邻相等元素没有用过,则跳过 continue; } // 选择 nums[i]
14.递归
-
剑指t33 二叉搜索树的后序遍历序列
根在最右边,左右子树的分界点是第一个比根大的数
public boolean verifyPostorder(int[] postorder) { return verify(postorder, 0, postorder.length - 1); } private boolean verify(int[] postorder, int i, int j) { if (i >= j) return true; int p = i; while (postorder[p] < postorder[j]) p++; int m = p; while (postorder[p] > postorder[j]) p++; // 让p一轮游,看是不是左边都比根小,右边都比根大 return p == j && verify(postorder, i, m - 1) && verify(postorder, m, j-1); // 这样的return值在左右子树需同时满足条件的时候很常用 } }
-
剑指t36 二叉搜索树与双向链表
-
正确答案
Node head, pre; public Node treeToDoublyList(Node root) { if (root == null) { return null; } treeToDoublyList1(root); pre.right = head; head.left =pre;//进行头节点和尾节点的相互指向,这两句的顺序也是可以颠倒的 return head; } // 中序遍历 private void dfs(Node cur) { if (cur == null) { return; } dfs(cur.left); // pre是当前结点cur的前一个,用于转化为双向链表 if (pre == null) { // 首部 head = cur; } else { pre.right = cur; cur.left = pre; } // 会自动更新到尾部 pre = cur; dfs(cur.right); }
-
我的错误答案
如果返回root,输出的结果会忽略掉左孩子的右子树,右孩子的左子树
public Node treeToDoublyList1(Node root) { if (root == null) { return null; } Node left = treeToDoublyList1(root.left); if (pre == null) { head = root; } if (left != null) { left.right = root; root.left = left; } pre = root; Node right = treeToDoublyList1(root.right); if (right != null) { root.right = right; right.left = root; } return root; }
-
-
剑指t62 圆圈中最后剩下的数字
-
分析:令f(n,m)为n
-
个数,每次去掉第n个,最后剩下的数字(坐标),f(1,m) = 0
f(n,m) = (m+f(n-1, m))%n,看了我一个小时,评论区分析
-
递归
public int lastRemaining(int n, int m) { return f(n, m); } private int f(int n, int m) { if (n == 1) { return 0; } int x = f(n-1, m); return (m + x)%n; } }
-
迭代
int a = 0; for (int i = 2; i <= n; i++) { a = (m + a) % i; // 是i不是n } return a;
-
-
q112 路径总和
分析:什么时候需要返回值
-
如果需要搜索整颗二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
-
如果需要搜索整颗二叉树且需要处理递归返回值,递归函数就需要返回值。(这种情况我们在236. 二叉树的最近公共祖先介绍)
-
搜索整棵树的写法
left = 递归函数(root->left); right = 递归函数(root->right); left与right的逻辑处理;
-
-
如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)
if (递归函数(root->left)) return ; if (递归函数(root->right)) return ;
public boolean hasPathSum(TreeNode root, int targetSum) { 对输入值的提前处理 if (root == null) return false; return traversal(root, targetSum - root.val); } public boolean traversal(TreeNode root, int count) { if (root.left == null && root.right == null && count == 0) return true; if (root.left == null && root.right == null && count != 0) return false; // 有子节点的情况,必须加这个判断,不然会导致空指针异常 if (root.left != null) { if (traversal(root.left, count - root.left.val)) { // 过滤掉返回值为false的情况 return true; } } if (root.right != null) { if (traversal(root.right, count - root.right.val)) { return true; } } return false; }
返回值为bool值的比较标准的模板
一步步简化
public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) return false; return traversal(root, targetSum); } public boolean traversal(TreeNode root, int count) { if (root == null) return false; if (root.left == null && root.right == null && count == root.val) return true; if (traversal(root.left, count - root.val)) { return true; } if (traversal(root.right, count - root.val)) { return true; } return false; }
public boolean hasPathSum(TreeNode root, int targetSum) { if (root == null) return false; if (root.left == null && root.right == null && targetSum == root.val) return true; return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); }
迭代法:使用两个栈,一个存节点,一个存到这个节点值的总和,遍历方式时迭代版的先序遍历
-
-
q236 二叉树的最近公共祖先
- 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。
- 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。
- 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。
q235 二叉搜索树的最近公共祖先,根据值可以直接向下走,使用只用搜索一条路径的方法
不要把自己的思维带入到递归过程里面去,需要有一种宏观的认识,只分析这一步的情况
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { if (root == null) return null; if (root == p || root == q) return root; // 左边是否有p || q,也可以认为是否得到了p,q的最近公共祖先,right有就是第一种可能,没有就是第二种可能 TreeNode left = lowestCommonAncestor(root.left, p, q); // 右边是否有p || q TreeNode right = lowestCommonAncestor(root.right, p, q); if (left == null && right == null) return null; else if (left == null) return right; else if (right == null) return left; else return root; }
-
q701 二叉搜索树中的插入操作
需要了解二叉搜索树的增删改查https://labuladong.github.io/algo/2/19/40/
虽然有很多种插入方法,不用管,只要到空节点就行了
-
递归
public TreeNode insertIntoBST(TreeNode root, int val) { if (root == null) // 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。 return new TreeNode(val); if (root.val < val){ root.right = insertIntoBST(root.right, val); // 递归创建右子树 }else if (root.val > val){ root.left = insertIntoBST(root.left, val); // 递归创建左子树 } return root; }
-
回溯,记录上一个节点
TreeNode pre; public TreeNode insertIntoBST(TreeNode root, int val) { if (root == null) return new TreeNode(val); traversal(root, val); return root; } public void traversal(TreeNode node, int val) { if (node == null) { if (val < pre.val) pre.left = new TreeNode(val); else pre.right = new TreeNode(val); return; } pre = node; if (val < node.val) traversal(node.left, val); if (val > node.val) traversal(node.right, val); }
-
-
q450 删除二叉搜索树中的节点
比上一题更复杂,有五种情况
-
迭代
public TreeNode deleteNode(TreeNode root, int key) { if (root == null) return root; // 先找到这个节点以及它的父节点 TreeNode pre = null; TreeNode node = root; while (node != null) { if (key == node.val) break; pre = node; if (key < node.val) node = node.left; else node = node.right; } if (pre == null) return deleteOneNode(node); // 如果没有找到或者就是头节点 if (pre.left != null && pre.left.val == key) pre.left = deleteOneNode(node); if (pre.right != null && pre.right.val == key) pre.right = deleteOneNode(node); return root; } public TreeNode deleteOneNode(TreeNode cur) { if (cur == null) return null; if (cur.right == null) return cur.left; TreeNode node = cur.right; while (node.left != null) { node = node.left; } node.left = cur.left; return cur.right; }
-
递归
public TreeNode deleteNode(TreeNode root, int key) { // 情况一:没有找到,直接返回 // 确定终止条件 if (root == null) return root; // 确定单层递归的逻辑 if (key < root.val) root.left = deleteNode(root.left, key); else if (key > root.val) root.right = deleteNode(root.right, key); else { // 情况二、三、四:返回空或另一个 if (root.right == null) return root.left; if (root.left == null) return root.right; // 情况五:左右孩子都有的修改逻辑 TreeNode node = root.right; while (node.left != null) { node = node.left; } node.left = root.left; return root.right; } return root; }
-
-
q24两两交换链表中的节点
迭代法比较简单,用递归法做一下
-
确定返回值和参数
递归中需要对返回值进行处理,所以需要返回值,参数是需要交互的双节点的头节点
-
确定终止条件
偶数是为空,奇数是next为空
-
确定单层递归的逻辑
public ListNode swapPairs(ListNode head) { // 最后递归停止的返回值 if (head == null || head.next == null) return head; ListNode newHead = head.next; // 递归到下一个双节点的头节点 head.next = swapPairs(newHead.next); newHead.next = head; // 这个return是与上面的head.next配合的 return newHead; }
-
-
q95 不同的二叉搜索树Ⅱ
不要把思维跳进递归栈中,会糊涂
public List<TreeNode> generateTrees(int lo, int hi) { List<TreeNode> res = new LinkedList<>(); if (lo > hi) { res.add(null); return res; } for (int i = lo; i <= hi; i++) { List<TreeNode> lefts = generateTrees(lo, i-1); List<TreeNode> rights = generateTrees(i+1, hi); for (TreeNode left : lefts) { // 左边有很多合法的二叉搜索树 for (TreeNode right : rights) { // 右边也有很多合法的二叉搜索树 TreeNode root = new TreeNode(i); // p root.left = left; root.right = right; res.add(root); } } } return res; }
-
q215 数组中的第K个最大元素
使用改造版的快排,不用每个区间都要递归排一遍,不过划分函数partition还是一样的
public int findKthLargest(int[] nums, int k) { // 自己实现快速排序,根左右 return quickSort(nums, 0, nums.length-1, nums.length - k); } public int quickSort(int[] nums, int left, int right, int k) { int p = partition(nums, left, right); if (p == k) return nums[p]; else if (p < k) { return quickSort(nums, p+1, right, k); } else { return quickSort(nums, left, p-1, k); } } // 这个方法连swap函数都不用定义 public int partition(int[] nums, int left, int right) { int head = nums[left]; while (left < right) { while (left < right && nums[right] >= head) right--; nums[left] = nums[right]; // 这个顺序别记反了,小的滚到左边去 while (left < right && nums[left] <= head) left++; nums[right] = nums[left]; } nums[left] = head; return left; }
-
滴滴笔试19真题
排列小球https://leetcode.cn/leetbook/read/didiglobal2/e7hh2i/
函数跟我想的差不多,但是细节方面没想到,如何去重?不用去重,因为小球是以数量的形式表示的。备忘录不加也可以,但是会超时,备忘录的key要是唯一的,所以给数字中间加空格。
public class Main { static Map<String, Long> map = new HashMap<>(); public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNext()) { int p = sc.nextInt(); int q = sc.nextInt(); int r = sc.nextInt(); System.out.println(backtracking(0,p, q, r)); } } public static long backtracking(int prev, int p, int q, int r) { if (p < 0 || q < 0 || r < 0) return 0; if (map.containsKey(prev + " " + p + " " + q + " " + r)) { return map.get(prev + " " + p + " " + q + " " + r); } if (p == 0 && q == 0 && r == 0) return 1; long res = 0L; if (prev == 0) { res = backtracking(1, p-1, q, r) + backtracking(2, p, q-1, r) + backtracking(3, p, q, r-1); } else if (prev == 1) { res = backtracking(2, p, q-1, r) + backtracking(3, p, q, r-1); } else if (prev == 2) { res = backtracking(1, p-1, q, r) + backtracking(3, p, q, r-1); } else res = backtracking(1, p-1, q, r) + backtracking(2, p, q-1, r); map.put(prev + " " + p + " " + q + " " + r, res); return res; } }