滑动窗口
1.无重复字符的最长子串
给定一个字符串 s,请你找出其中不含有重复字符的 最长子串的长度。 示例 1: 输入:s = "abcabcbb" 输出:3 解释:因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2: 输入:s="bbbbb" 输出:1 解释:因为无重复字符的最长子串是 “b",所以其长度为 1。
示例3: 输入:S ="pwwkew"" 输出:3 解释:因为无重复字符的最长子串是“wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度, "pwke" 是一个子序列,不是子串。
2.最小覆盖子串
给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串“”。 注意: •对于t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。 •如果s中存在这样的子串,我们保证它是唯一的答案。 示例1:
输入:S= "ADOBECODEBANC",t= "ABC"
输出:"BANC"
示例 2: 输入:S="a",t="a" 输出:"a"
示例3: 输入:s="a",t= "aa" 输出:"" 解释:t中两个字符'a'均应包含在s的子串中, 因此没有符合条件的子字符串,返回空字符串。
3.字符串的排列
给你两个字符串 s1 和 s2 ,写一个函数来判断 s2 是否包含 s1 的排列。如果是,返回 true ;否则,返回 false 。
换句话说,s1 的排列之一是 s2 的 子串 。
示例 1:
输入:s1 = "ab" s2 = "eidbaooo"
输出:true
解释:s2 包含 s1 的排列之一 ("ba").
示例 2:
输入:s1= "ab" s2 = "eidboaoo"
输出:false
提示:
1 <= s1.length, s2.length <= 104s1和s2仅包含小写字母
4.找到字符串中所有字母异位词
给定两个字符串 s 和 p,找到 s 中所有 p 的 异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
异位词 指由相同字母重排列形成的字符串(包括相同的字符串)。
示例 1:
输入: s = "cbaebabacd", p = "abc"
输出: [0,6]
解释:
起始索引等于 0 的子串是 "cba", 它是 "abc" 的异位词。
起始索引等于 6 的子串是 "bac", 它是 "abc" 的异位词。
示例 2:
输入: s = "abab", p = "ab"
输出: [0,1,2]
解释:
起始索引等于 0 的子串是 "ab", 它是 "ab" 的异位词。
起始索引等于 1 的子串是 "ba", 它是 "ab" 的异位词。
起始索引等于 2 的子串是 "ab", 它是 "ab" 的异位词。
提示:
1 <= s.length, p.length <= 3 * 104s和p仅包含小写字母
子串
1.和为 k 的子数组
给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。
子数组是数组中元素的连续非空序列。
示例 1:
输入:nums = [1,1,1], k = 2
输出:2
示例 2:
输入:nums = [1,2,3], k = 3
输出:2
提示:
1 <= nums.length <= 2 * 104-1000 <= nums[i] <= 1000-107 <= k <= 107
2.滑动窗口最大值-单调队列
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
示例 2:
输入:nums = [1], k = 1
输出:[1]
提示:
1 <= nums.length <= 105-104 <= nums[i] <= 1041 <= k <= nums.length
1.合并两个有序链表-双指针+虚拟头结点
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例 1:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
- 两个链表的节点数目范围是
[0, 50] -100 <= Node.val <= 100l1和l2均按 非递减顺序 排列
2.分隔链表
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]
示例 2:
输入:head = [2,1], x = 2
输出:[1,2]
3.合并 k 个升序链表-优先级队列
思路📢:合并链表中我们使用了双指针来解决问题,但是这道题是合并 k 个链表,使用 k 个指针不现实;
为了可以快速找到 k 个链表中最小的那个结点,可以采用优先级队列;
📢📢优先级队列的容量为 k,并且排序规则是升序;且 k 小于 1 的话,构造函数会报异常
📢📢offer(E e) 添加的元素 e 不能为 null,否则会报空指针异常;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
int k = lists.length;
if(k == 0){
return null;
}
//注意📢:这里k 小于 1 的话,构造函数会报异常
PriorityQueue<ListNode> queue = new PriorityQueue<>(k, (x,y) -> (x.val - y.val));//初始容量为 k,并且是小根堆哦
ListNode dummy = new ListNode(-1);//结果链表,虚拟头结点
ListNode p = dummy;//结果链表的指针
for(int i = 0; i < k; i++){
if(lists[i] != null){//注意这里需要判断结点不为 null 的时候在添加进优先级队列中
queue.offer(lists[i]);//插入结点到优先级队列中
}
}
while(!queue.isEmpty()){
ListNode node = queue.poll();//取出优先级队列中最小的结点
p.next = node;
p = p.next;//指针右移
//此节点是否为最后一个节点呢
if(node.next != null){
queue.offer(node.next);
}
}
return dummy.next;
}
}
4.返回单链表的倒数第 k 个结点
// 返回链表的倒数第 k 个节点
ListNode findFromEnd(ListNode head, int k) {
ListNode p1 = head;
// p1 先走 k 步
for (int i = 0; i < k; i++) {
p1 = p1.next;
}
ListNode p2 = head;
// p1 和 p2 同时走 n - k 步
while (p1 != null) {
p2 = p2.next;
p1 = p1.next;
}
// p2 现在指向第 n - k + 1 个节点,即倒数第 k 个节点
return p2;
}
5.删除链表的倒数第 N 个结点-双指针+虚拟头结点
思路📢:和上一题一毛一样,只不过这次是要删除倒数第 n 个结点,也就是要找到倒数第 n+1 个结点!
本题需要采用虚拟头结点,避免空指针异常,比如链表中只有一个元素,但是要删除倒数第一个元素,此时我们要找的就是倒数第二个元素,会发生空指针异常
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(-1);//虚拟头结点
dummy.next = head;
ListNode p1 = dummy;//前继指针
ListNode p2 = dummy;//后继指针
//因为需要删除倒数第 n 个结点,所以我们需要找到倒数第 n+1 个结点
while(n >= 0){//细节加=号
p1 = p1.next;
n--;
}
while(p1 != null){
p1 = p1.next;//右移
p2 = p2.next;//右移
}
//删除第 n 个结点
p2.next = p2.next.next;
return dummy.next;
}
}
6.单链表的中点-双指针
思路📢:
慢指针走一步,快指针走两步;
循环终止的条件fast != null && fast.next != null
如果结点个数为偶数的话,此方法返回的是第二个中间节点
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;//快慢双指针
while(fast != null && fast.next != null){//循环终止的条件
slow = slow.next;//慢指针走一步
fast = fast.next.next;//快指针走两步
}
return slow;//返回慢指针即为中间节点
}
}
7.判断单链表是否有环-快慢双指针
思路📢:采用快慢双指针,快指针走两步,慢指针走一步,若两者相遇,则说明有环
但是要注意📢📢📢循环终止条件fast != null && fast.next != null
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;//快慢指针
while(fast != null && fast.next != null){
slow = slow.next;//慢指针走一步
fast = fast.next.next;//快指针走两步
if(slow == fast){//两个指针相遇了,则说明有环
return true;
}
}
return false;//退出了循环,说明没环
}
}
8.环形链表II
思路📢📢:环形链表的进阶版本,1.判断是否有环 2.判断环的起点在哪里
当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。
理论部分:摘抄自labuladong.gitee.io/algo/di-yi-…
我们假设快慢指针相遇时,慢指针
slow走了k步,那么快指针fast一定走了2k步;
fast一定比slow多走了k步,这多走的k步其实就是fast指针在环里转圈圈,所以k的值就是环长度的「整数倍」;
/**
* Definition for singly-linked list.
* class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while(fast != null && fast.next != null){//退出循环的条件
slow = slow.next;//慢指针走一步
fast = fast.next.next;//快指针走两步
if(slow == fast){//证明有环
break;
}
}
//如果出现了 null,说明此链表没有环
if(fast == null || fast.next == null){
return null;
}
//两者相遇说明有环,此时在把慢指针放到 head 头结点处,等快慢指针下一次相遇的时候,就是环的起点
slow = head;
while(slow != fast){
slow = slow.next;//慢指针走一步
fast = fast.next;//快指针走两步
}
return slow;//返回环的起始结点
}
}
9.链表相交
注意📢:可以先让长的链表的指针先向右移动 gap 个距离,然后两个链表的指针在一起向右跑,直到找到相交的结点,否则就没有相交的节点!
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode pa = headA;
ListNode pb = headB;
int lenA = 0;
int lenB = 0;
while(pa != null){
lenA++;
pa = pa.next;
}
while(pb != null){
lenB++;
pb = pb.next;
}
int gap = Math.max(lenA,lenB) - Math.min(lenA,lenB);
pa = headA;
pb = headB;
if(lenA > lenB){
while(gap-- > 0){
pa = pa.next;
}
}else{
while(gap-- > 0){
pb = pb.next;
}
}
while(pa != pb){
pa = pa.next;
pb = pb.next;
}
return pa;
}
}
10.反转链表
思路:
迭代法
- 采用 3 个指针
- 逐步向后遍历
public ListNode reverseList(ListNode head) {
ListNode pre = null, cur = head, nxt = head;//前 中 后三个指针
while(cur != null){
nxt = cur.next;//临时指针先占位置
cur.next = pre;
pre = cur;//前 面指针右移
cur = nxt;//当前指针右移
}
return pre;
}
11.翻转部分链表
和反转链表类似
/** 反转区间 [a, b) 的元素,注意是左闭右开 */
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;
}
12.K 个一组翻转链表
思路:
可以仔细想一想,这是一个大问题,可以划分为一个个相似的小问题;
- 只需要先翻转第一组链表,得到新的头结点
- 然后将第一组的尾节点[也就是 head 结点]连接到后续链表的头部[
reverseKGroup(后续结点,k)]即可 - 注意退出条件也就是不够 k 个结点时
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
//先翻转前 k 个结点,然后在把头结点指向下一组结点的开头
public ListNode reverseKGroup(ListNode head, int k) {
ListNode cur = head;//指针
for(int i = 0; i < k; i++){
if(cur == null){
return head;//不够一组
}
cur = cur.next;//指针右移
}
ListNode newHead = reverse(head, cur);//翻转第一组结点,得到新的头结点
head.next = reverseKGroup(cur, k);//不要进入递归调用[你的脑袋能装几个栈呀],了解此方法的定义即可,递归调用会返回新的头结点
return newHead;
}
public ListNode reverse(ListNode a, ListNode b){//翻转[a,b)区间之内的结点
ListNode pre = a, cur = a.next, nxt = a;//前中后三个指针
while(cur != b){//循环终止条件
nxt = cur.next;//临时指针先记录下一个位置
cur.next = pre;//改变指向
pre = cur;//前面的指针后移
cur = nxt;//当前指针后移
}
return pre;//返回新的头结点
}
}
13.两两交换链表中的结点
思路:K 个一组翻转链表 的简化版,所以也可以采用递归法来解决,我们只需要处理好第一组即可,后面的交给递归
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
//采用递归吧,迭代需要考虑的条件太多了
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null){//如果没有节点或者只有一个节点时,直接返回结果
return head;
}
ListNode first = head, second = head.next, others = second.next;//定义三个指针
second.next = first;//翻转
//递归算法,我们只需要处理好第一次逻辑即可,千万不要进入递归算法的分析之中,我们的脑袋能装几次栈呢???
first.next = swapPairs(others);//采用递归,了解递归方法的定义即可,递归方法会返回新的头结点
return second;//返回 second 作为新的头结点
}
}
14.两数相加
思路:三个指针,一起向右遍历;
- 由于要新建链表,建议使用虚拟头结点;
- 只要p1、p2、carray还有内容,计算就不能停止;
- 每次计算都要记录进位的内容和本位的内容;
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode p1 = l1, p2 = l2;//两个指针
ListNode dummy = new ListNode(-1);//虚拟头结点
ListNode p = dummy;//结果链表的指针 p
int carray = 0;//表示进位
while(p1 != null || p2 != null || carray > 0){
int val = carray;//先记录上次的进位
if(p1 != null){
val += p1.val;
p1 = p1.next;
}
if(p2 != null){
val += p2.val;
p2 = p2.next;
}
carray = val / 10;//进位
val = val % 10;//本位的结果
ListNode node = new ListNode(val);
p.next = node;
p = p.next;//结果指针右移
}
return dummy.next;
}
}
11.8 二叉树
遇到一道二叉树的题目时的通用思考过程是:
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个 traverse 函数配合外部变量来实现。
二叉树的遍历框架:
void traverse(TreeNode root) { if (root == null) { return; } // 前序位置 traverse(root.left); // 中序位置 traverse(root.right); // 后序位置 }前序位置的代码在刚刚进入一个二叉树节点的时候执行;自顶向下
后序位置的代码在将要离开一个二叉树节点的时候执行;自底向上
中序位置的代码进入节点之后,离开节点之前的时候执行。
前序和后序的区不同:前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。
结论:一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
3、无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。
1.二叉树的最大深度
思路:
递归法:
- 一个根节点的最大深度=左节点的最大深度 与 右节点的最大深度 中取一个最大的 加一
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int maxDepth(TreeNode root) {
if(root == null){//结束条件
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return 1 + Math.max(leftDepth, rightDepth);
}
}
2.二叉树的中序遍历
思路:
遍历法:
public List<Integer> inorderTraversal(TreeNode root) {
ArrayList<Integer> res = new ArrayList<>();
midLoop(root, res);
return res;
}
public void midLoop(TreeNode root, List list){
if(root == null){
return;
}
midLoop(root.left, list);
list.add(root.val);
midLoop(root.right, list);
}
3.二叉树的层序遍历
思路:
遍历法:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//使用队列,一层一层的遍历
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();//结果
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<>();//队列,存放节点
queue.offer(root);//先放入一个根节点
while(!queue.isEmpty()){
int size = queue.size();
List<Integer> list = new ArrayList<>();//每一层的集合
while(size-- > 0){
TreeNode node = queue.poll();
list.add(node.val);
if(node.left != null){//放入左节点
queue.offer(node.left);
}
if(node.right != null){//放入右节点
queue.offer(node.right);
}
}
result.add(list);//更新每一层的结果
}
return result;
}
}
4.二叉树的直径
思路:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int result = 0;
public int diameterOfBinaryTree(TreeNode root) {
maxDepth(root);
return result;
}
public int maxDepth(TreeNode root){//求最大深度
if(root == null){
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
int depthSum = leftDepth + rightDepth;//记录左子树+右子树的 深度之和
result = Math.max(depthSum, result);//更新结果
return 1 + Math.max(leftDepth, rightDepth);
}
}
注意📢:result 只能是全局变量,如果用传参的方式,是不行的,因为普通类型进行传参时是值传递,方法内对参数的修改对外部变量是没有影响的。
5.翻转二叉树-狠狠递归
思路:
摘抄自东哥刷算法
这里采用递归的思路,没有采用遍历的思路,个人感觉递归更好理解一些,遍历的思路不是很懂,感觉他还是递归
- 先把左子树翻转,再把右子树翻转
- 最后把根节点的左右子树交换即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//采用递归算法
public TreeNode invertTree(TreeNode root) {
if(root == null){//终止条件
return root;
}
TreeNode left = invertTree(root.left);//翻转左节点这颗子树,并且返回头结点
TreeNode right = invertTree(root.right);//翻转右节点这颗子树,并且返回头结点
//交换根节点的左右子树
root.left = right;
root.right = left;
return root;
}
}
6.填充每个节点的下一个右侧节点指针-只能遍历了
7.将二叉树展开为链表–狠狠递归
思路📢:和翻转二叉树类似
- 先将左子树拉成单链表,在将右子树拉成单链表
- 最后将根节点和左子树的单链表和右子树的单链表连接起来即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//采用递归的思想
//flatten(TreeNode root)方法将 root 根节点拉成单链表
public void flatten(TreeNode root) {
if(root == null){
return;
}
TreeNode left = root.left;//左子树单链表的首节点
TreeNode right = root.right;//柚子树单链表的首节点
flatten(root.left);//将左子树拉成单链表
flatten(root.right);//将右子树拉成单链表
root.left = null;
root.right = left;//左子树生成的链表已经接到根节点的右侧
//接下来要做的就是把根节点的右子树生成的链表接到左子树链表的结尾处
TreeNode p = root;
while(p.right != null){//找到最后一个节点
p = p.right;
}
p.right = right;
}
}
8.最大二叉树
思路:构建二叉树,一般都是递归;
此题可以先构建根节点,在构建左右子树;
重点是我们只需要想清楚单独抽出一个二叉树节点,它需要做什么事情?
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
return build(nums, 0, nums.length-1);
}
public static TreeNode build(int[] nums, int l, int r){//单独构建的方法,来进行二叉树的构建
if(l > r){//返回null,终止条件
return null;
}
int index = -1, maxValue = Integer.MIN_VALUE;//找到最大值和对应的索引
for(int i = l; i <= r; i++){
if(nums[i] > maxValue){
index = i;
maxValue = nums[i];
}
}
TreeNode root = new TreeNode(maxValue);//构建根节点
root.left = build(nums, l, index-1);//构建左子树
root.right = build(nums, index+1, r);//构建右子树
return root;
}
}
9.从前序和中序遍历序列构造二叉树
思路:先构建根节点,在递归构建左子树和右子树;
主要就是前序数组和中序数组的下标的计算之类的复杂一点,逻辑上不太复杂。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
HashMap<Integer,Integer> map = new HashMap<>();//记录 inorder 数组中元素对应索引下标
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
}
//单独定义的构建二叉树的方法
public TreeNode build(int[] preorder, int preStart, int preEnd, int[] inorder, int inStart, int inEnd){
if(preStart > preEnd){
return null;//终止条件
}
int index = map.get(preorder[preStart]);//得到根节点在中序数组中的下标位置
int leftSize = index - inStart;//计算出左子树的长度,方便递归时计算数组的下标
TreeNode root = new TreeNode(preorder[preStart]);//创建根节点
root.left = build(preorder, preStart+1, preStart+leftSize, inorder, inStart, index-1);//递归的创建左子树
root.right = build(preorder, preStart+leftSize+1, preEnd, inorder, index+1, inEnd);//递归的创建右子树
return root;//返回根节点
}
}
10.从中序和后续遍历序列构造二叉树
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
HashMap<Integer, Integer> map = new HashMap<>();//用来记录中序数组中元素和其下标
public TreeNode buildTree(int[] inorder, int[] postorder) {
for(int i = 0; i < inorder.length; i++){
map.put(inorder[i], i);
}
return build(inorder, 0, inorder.length-1, postorder, 0, postorder.length-1);
}
//单独定义的方法用来构建二叉树
public TreeNode build(int[] inorder, int inStart, int inEnd, int[] postorder, int posStart, int posEnd){
if(posStart > posEnd){
return null;//终止条件
}
int index = map.get(postorder[posEnd]);//用来记录根节点在中序数组中的下标
int leftSize = index-inStart;//用来记录左子树的元素个数,方便递归时计算出合理的下标
TreeNode root = new TreeNode(postorder[posEnd]);//根节点
root.left = build(inorder, inStart, index-1, postorder, posStart, posStart+leftSize-1);//递归构建左子树
root.right = build(inorder, index+1, inEnd, postorder, posStart+leftSize, posEnd-1);//递归构建右子树
return root;//返回根节点
}
}
11.对称二叉树
思路:采用递归
递归的去判断 左子树和右子树是否对称;
最终在去递归的比较左子树的左节点和右子树的右节点是否一样,左子树的右节点和右子树的左节点是否一样;这个和之前的不一样,之前是递归左右节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null){
return true;
}
return dfs(root.left, root.right);
}
public boolean dfs(TreeNode left, TreeNode right){//单独定义的函数,用来比较左右两颗子树是否对称
if(left == null && right == null){
return true;//两个子树都为 null
}
if(left == null || right == null){
return false;//只有一颗子树为 null
}
if(left.val != right.val){
return false;//两个节点的值不相同
}
return dfs(left.left, right.right) && dfs(left.right, right.left);//在去递归的比较左子树的左节点和右子树的右节点是否一样,左子树的右节点和右子树的左节点是否一样
}
}
12.将有序数组转换为二叉搜索树
思路:采用递归的方法,数组的中间那个数作为根节点,他的左子树根据左半边的数组构建,右子树根据右半边的数组构建
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public TreeNode sortedArrayToBST(int[] nums) {
return build(nums, 0, nums.length-1);
}
//单独定义的构造搜索二叉树的函数
public TreeNode build(int[] nums, int l, int r){
if(l > r){//终止条件
return null;
}
int mid = (l+r) / 2;//找到中间值的索引,中间值作为根节点
TreeNode root = new TreeNode(nums[mid]);//根节点
root.left = build(nums, l, mid-1);//构建左子树
root.right = build(nums, mid+1, r);//构建右子树
return root;//返回根节点
}
}
13.二叉搜索树中第K小的元素
思路:BST 的中序遍历就是其结点值的升序排列,所以只需要中序遍历一下即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public int kthSmallest(TreeNode root, int k) {
ArrayList<Integer> result = new ArrayList<>();
midLoop(root, result);
return result.get(k-1);
}
public void midLoop(TreeNode root, List list){//二叉搜索树的中序遍历就是结点值的升序排列
if(root == null){
return;
}
midLoop(root.left, list);
list.add(root.val);
midLoop(root.right, list);
}
}
13.把二叉搜索树转换为累加树
思路:BST 的题要么利用 BST 左小右大的特性提升算法效率,要么利用中序遍历的特性满足题目的要求
BST的中序遍历是元素的升序,但是本题由于需要求 大于等于 当前节点的和,所以我们可以改变中序遍历的顺序,可以先右 在中 最后左,并且用一个额外的变量来记录累计和
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
int sum = 0;
public TreeNode convertBST(TreeNode root) {
midLoop(root);
return root;
}
public void midLoop(TreeNode root){
if(root == null){//终止条件
return;
}
//平时中序遍历我们是左 中 右,那样 BST 是升序的,但是本题中我们需要的是降序
midLoop(root.right);//先右
sum += root.val;//记录大于等于当前节点的所有值的和
root.val = sum;
midLoop(root.left);//最后左
}
}
14.验证二叉搜索树
思路:确实需要采用递归,但是有个坑,我们如果单纯的判断左节点的值小于右节点的值的话,是跑不通案例的。
因为我们没有满足整体上右子树是大于根节点的,比如
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
public boolean isValidBST(TreeNode root) {
return isBST(root, null, null);
}
//min 代表结点所能设置的最小边界 ,max 代表结点所能设置的最大边界
public boolean isBST(TreeNode root, TreeNode min, TreeNode max){
if(root == null){//结束条件
return true;
}
if(min != null && root.val <= min.val){
return false;
}
if(max != null && root.val >= max.val){
return false;
}
return isBST(root.left, min, root) && isBST(root.right, root, max);//判断左右子树是否满足 BST 特性
}
}
15.二叉搜索树中的搜索
思路:根据 BST 的特性来,左子树都小于根节点,右子树都大于根节点
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//根据 BST 的特性来做这道题
public TreeNode searchBST(TreeNode root, int val) {
if(root == null){//终止条件
return null;
}
if(root.val == val){
return root;
}
TreeNode node = root.val < val ? searchBST(root.right, val) : searchBST(root.left, val);//二分查找
return node;
}
}
17.二叉树的右视图
思路:可以采用层序遍历,记录每一层的最后一个节点即可
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//采用层序遍历,每次记录每一层的最后一个节点即可
public List<Integer> rightSideView(TreeNode root) {
List<Integer> result = new ArrayList<>();//记录结果
if(root == null){
return result;
}
Queue<TreeNode> queue = new LinkedList<>();//队列
queue.offer(root);//先存放首节点
while(!queue.isEmpty()){
int size = queue.size();//记录每一层的结点个数
while(size-- > 0){
TreeNode node = queue.poll();
if(node.left != null){
queue.offer(node.left);
}
if(node.right != null){
queue.offer(node.right);
}
if(size == 0){//此层的最后一个节点
result.add(node.val);
}
}
}
return result;
}
}
18.路径总和 III
思路📢:使用前缀和来做,但是我第一次接触前缀和这种玩意,有点难理解;
当前节点使用完记得撤销选择,因为路径是从父节点到子节点的,不可逆!
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//采用前缀和
int result = 0;
public int pathSum(TreeNode root, int targetSum) {
// 记录路径中某个前缀和出现的次数
Map<Long, Integer> map = new HashMap<>();
// 初始化,避免当前节点的值就是targetSum的情况
map.put(0L, 1);
pathSum(root, targetSum, 0L, map);
return result;
}
public void pathSum(TreeNode root, int targetSum, long curSum, Map<Long, Integer> map) {
if(root == null){//终止条件
return;
}
curSum += root.val;//记录当前结点前缀和
result += map.getOrDefault(curSum - targetSum, 0);//记录当前路径上合格的前缀和
map.put(curSum, map.getOrDefault(curSum, 0)+1);
//递归
pathSum(root.left, targetSum, curSum, map);
pathSum(root.right, targetSum, curSum, map);
//撤销状态
map.put(curSum, map.getOrDefault(curSum, 0)-1);
}
}
11.9回溯算法
回溯算法的框架
1.全排列(元素无重不可复选)
思路📢:用回溯算法的框架
做选择–进递归-撤销选择
class Solution {
//回溯,做选择-- 递归 --撤销选择
List<List<Integer>> result = new ArrayList<>();//结果数组
public List<List<Integer>> permute(int[] nums) {
//记录路径
LinkedList<Integer> track = new LinkedList<>();
//「路径」中的元素会被标记为 true,避免重复使用
boolean[] used = new boolean[nums.length];
backtrack(nums, track, used);
return result;//返回结果
}
public void backtrack(int[] nums, LinkedList track, boolean[] used){//回溯算法的函数
if(track.size() == nums.length){//nums 中的元素全都在 track 中出现,触发结束条件
result.add(new ArrayList(track));
return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]){//一个元素只能用一次
continue;
}
//做出选择
track.add(nums[i]);
used[i] = true;//标记,此元素已经使用过
//进入递归
backtrack(nums, track, used);
//撤销选择
used[i] = false;
track.removeLast();
}
}
}
2.N皇后
思路📢:还是采用的东哥的框架代码
- 触发结束
- 做出选择-递归-撤销选择
class Solution {
List<List<String>> result = new ArrayList<>();//结果数组
public List<List<String>> solveNQueens(int n) {
List<String> board = new ArrayList<>();//路径
for(int i = 0; i < n; i++){//初始化棋盘
char[] chars = new char[n];
Arrays.fill(chars, '.');
board.add(new String(chars));
}
backtrack(board,0);
return result;
}
// 路径:board 中小于 row 的那些行都已经成功放置了皇后
// 选择列表:第 row 行的所有列都是放置皇后的选择
// 结束条件:row 超过 board 的最后一行
public void backtrack(List<String> board, int row){
if(board.size() == row){//触发结束条件
result.add(new ArrayList(board));//注意这里需要重新 new 一个 board,为什么这里是new ArrayList(track),是因为后面取消选择的时候会removeLast,最终会把当前track元素都移除掉,导致最后添加进去的数组都是空的
return;
}
int n = board.get(row).length();//列的长度
for(int j = 0; j < n; j++){
//排除不合法选择
if(!isValid(board, row, j)){
continue;
}
//做出选择
StringBuilder sb = new StringBuilder(board.get(row));
sb.setCharAt(j, 'Q');
board.set(row, sb.toString());
//递归
backtrack(board, row+1);
//撤销选择
sb.setCharAt(j, '.');
board.set(row, sb.toString());
}
}
public boolean isValid(List<String> board, int row, int column){
int n = board.size();
//上
for(int i = 0; i < row; i++){
if(board.get(i).charAt(column) == 'Q'){
return false;
}
}
//左上
for(int i = row-1, j = column-1; i >= 0 && j >= 0; i--,j--){
if(board.get(i).charAt(j) == 'Q'){
return false;
}
}
//右上
for(int i = row-1, j = column+1; i >= 0 && j < n; i--,j++){
if(board.get(i).charAt(j) == 'Q'){
return false;
}
}
return true;
}
}
3.子集(元素无重不可复选)
思路📢:东哥的回溯框架
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
public List<List<Integer>> subsets(int[] nums) {
backtrack(nums, 0);
return result;
}
//回溯框架
public void backtrack(int[] nums, int start){
result.add(new ArrayList(track));//添加到结果集中
for(int i = start; i < nums.length; i++){
//做出选择
track.add(nums[i]);
//递归
backtrack(nums, i+1);
//撤销选择
track.removeLast();
}
}
}
4.组合(元素无重不可复选)
思路:和上一题求子集思路类似,这次是只取第 k 层的数据
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
public List<List<Integer>> combine(int n, int k) {
backtrack(1, n, k);
return result;
}
//回溯框架
public void backtrack(int start, int n, int k){
if(track.size() == k){//路径中有 k 个数据时,符合条件
result.add(new ArrayList(track));
}
for(int i = start; i <= n; i++){
//做出选择
track.add(i);
//递归,通过 start 参数控制树枝的遍历,避免产生重复的子集
backtrack(i+1, n, k);
//撤销选择
track.removeLast();
}
}
}
5.子集/组合(元素可重不可复选)
思路📢:东哥的回溯框架
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
public List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
backtrack(nums, 0);
return result;
}
//回溯框架
public void backtrack(int[] nums, int start){
result.add(new ArrayList(track));//添加结果
for(int i = start; i < nums.length; i++){
if(i > start && nums[i] == nums[i-1]){//剪枝,在同一层才有必要剪枝
continue;//剪枝
}
//做出选择
track.add(nums[i]);
//递归
backtrack(nums, i+1);
//撤销选择
track.removeLast();
}
}
}
6.组合总和II(元素可重不可复选)
思路:东哥回溯框架
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
int sum = 0;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);//排序
backtrack(candidates, target, 0);
return result;
}
public void backtrack(int[] candidates, int target, int start){//start表示哪一层
if(sum == target){
result.add(new ArrayList(track));
}
if(sum > target){//如果 sum 已经大于 target 了,那么就没必要继续回溯下一层了
return;
}
for(int i = start; i < candidates.length; i++){
if(i > start && candidates[i] == candidates[i-1]){//剪枝,同一层剪枝
continue;
}
//做出选择
track.add(candidates[i]);
sum += candidates[i];//记录累加和
//递归
backtrack(candidates,target,i+1);
//撤销选择
track.removeLast();
sum -= candidates[i];
}
}
}
8.全排列II(元素可重不可复选)
思路:有重复的元素了,需要剪枝了,
- 数组排序
- 保证重复元素的相对位置,!used[i-1] 比如 2-2`-2``
- 注意📢📢📢剪枝不能单纯的
i > 0 && nums[i] == nums[i-1]这样会集合中就没有重复元素,比如[1,2,2·],每次回溯到 2·的时候就会跳过
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);//先排序
boolean[] used = new boolean[nums.length];
backtrack(nums, used);
return result;
}
public void backtrack(int[] nums, boolean[] used){
if(track.size() == nums.length){//满足条件
result.add(new ArrayList(track));
// return;
}
for(int i = 0; i < nums.length; i++){
if(used[i]){//已经被使用过
continue;
}
if(i > 0 && nums[i] == nums[i-1] && !used[i - 1]){//剪枝,注意最后一个!used[i-1] 保证了重复元素的相对顺序 2-2`-2`` 直接减掉了很多枝;对于used[i-1] 是保证2``-2`-2的相对顺序,也会剪枝,但是会做很多无用功,才能回溯到2``-2`-2这个顺序
continue;
}
//做出选择
track.add(nums[i]);
used[i] = true;
//递归
backtrack(nums, used);
//撤销选择
track.removeLast();
used[i] = false;
}
}
}
9.组合总和(元素无重可复选)
思路:之前我们是根据元素的相对位置来保证不让元素重复,每次递归的时候让 start+1,但是此题需要元素重复,所以我们每次可以直接传 i 就行,但是需要补上 base case ,也就是 currSum > target 的时候 要return
class Solution {
List<List<Integer>> result = new ArrayList<>();//结果
LinkedList<Integer> track = new LinkedList<>();//路径
int currSum = 0;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
backtrack(candidates, target, 0);
return result;
}
public void backtrack(int[] candidates, int target, int start){//回溯函数
if(currSum == target){
result.add(new ArrayList(track));
return;
}
if(currSum > target){//当前和已经大于目标和,没必要继续回溯了
return;
}
for(int i = start; i < candidates.length; i++){
//做出选择
currSum += candidates[i];
track.add(candidates[i]);
//递归
backtrack(candidates, target, i);//之前为了不让元素重复,我们每次都传的是i+1,保证元素的相对顺序,但是现在元素可重复了
//撤销选择
currSum -= candidates[i];
track.removeLast();
}
}
}
10.电话号码的字母组合
思路📢📢:组合问题,每个字母数组中选一个字母
- 之前做的题都是求一个数组中的组合数,这道题是求多个数组中的组合数;
- 所以我们需要递归多个数组,start 表示的不再是一个数组中的下标了,而是代表哪个数组
注意📢
deleteChatAt(int index) 删除对应下标的字符
char类型的元素怎么转成 int 型,比如
int i = ‘2’ - ‘0’;
class Solution {
List<String> result = new ArrayList<>();//结果
StringBuilder track = new StringBuilder();//路径
private String letterMap[] = {
" ", //0
"", //1
"abc", //2
"def", //3
"ghi", //4
"jkl", //5
"mno", //6
"pqrs", //7
"tuv", //8
"wxyz" //9
};
public List<String> letterCombinations(String digits) {
if(digits.length() == 0){//
return result;
}
backtrack(digits, 0);
return result;
}
public void backtrack(String digits, int start){
if(track.length() == digits.length()){//满足条件
result.add(track.toString());
}
//2 3
for(int i = start; i < digits.length(); i++){
int index = digits.charAt(i) - '0';//当前数字是多少
String str = letterMap[index];//当前数字对应的字母
for(int j = 0; j < str.length(); j++){
//做出选择
track.append(str.charAt(j));
//递归
backtrack(digits, i+1);//下一个数字代表的字母
//撤销选择
track.deleteCharAt(track.length()-1);
}
}
}
}
注意📢:上面的方法用了两层 for 循环,其实没有必要,两层 for 循环求的是子集,本题只需要每个字母数组中都取出一个元素就可以了
注意📢📢📢:合法情况的判断写到前面,不合法情况的判断写到后面
class Solution {
String[] mapping = {
"",
"",
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
List<String> result = new ArrayList<>();//结果
StringBuilder track = new StringBuilder();//路径
public List<String> letterCombinations(String digits) {
if(digits.length() == 0){
return result;
}
backtrack(digits, 0);
return result;
}
public void backtrack(String digits, int start){
if(track.length() == digits.length()){
result.add(track.toString());//满足结果
}
if(start >= digits.length()){//数字用完了,没有数字可用
return;
}
int index = digits.charAt(start) - '0';
String str = mapping[index];//当前数字对应的字符串
for(int i = 0; i < str.length(); i++){
//做出选择
track.append(str.charAt(i));
//递归
backtrack(digits, start+1);
//撤销选择
track.deleteCharAt(track.length()-1);
}
}
}
11.括号生成
思路📢:采用回溯的方式,其实就是 2n 个位置用来放左右括号的组合,剪枝掉那些不合法的组合即可
- 左括号一定大于等于右括号,因为合法的括号都是左括号开头的
- 最后时,左括号等于右括号等于n
class Solution {
List<String> result = new ArrayList<>();//结果
StringBuilder track = new StringBuilder();//路径
// int left = 0, right = 0;//记录左括号和右括号
public List<String> generateParenthesis(int n) {
char[] chars = {'(',')'};//括号
backtrack(n, 0, chars ,0 ,0);
return result;
}
//0 1 2 3 4 5 6
public void backtrack(int n, int start, char[] chars,int left, int right){
if(left == right && left == n){//符合要求,写到前面
result.add(track.toString());
return;
}
if(start >= 2*n){//达到结束条件,2n个位置放括号 ??为啥这里没有等号呢?答:符合条件的判断写到前面就可以添加上等号了
return;
}
if(right > left){//右括号比左括号多,不合法;任何时候都是左括号的大于等于右括号的
return;
}
for(int i = 0; i < 2; i++){
//做出选择
track.append(chars[i]);
//递归
left = i == 0 ? left+1 : left;//看看添加的是左括号还是
right = i == 1 ? right+1 : right;//看看添加的是左括号还是
backtrack(n, start+1, chars, left, right);
//撤销选择
track.deleteCharAt(track.length()-1);
left = i == 0 ? left-1 : left;//看看添加的是左括号还是
right = i == 1 ? right-1 : right;//看看添加的是左括号还是
}
}
}
12.
11.10 二分查找
1.在排序数组中查找元素的第一个和最后一个位置
思路:采用两次二分查找即可;也比较好记。
- 找左边界时,
nums[mid] == target时,接着往左边找,直到退出循环;退出循环后 left 位置有可能是目标位置,但是还得判断一下数组中是否有 target 目标值 - 找右边界时,
nums[mid] == target时,接着往右边找,直到退出循环;退出循环后 right 位置有可能是目标位置,但是还得判断一下数组中是否有 target 目标值 - 在判断
nums[left或者right] == target时,需要确保 left 和 right 的下标合法
class Solution {
public int[] searchRange(int[] nums, int target) {
int[] result = {-1, -1};//结果
//左
int left = 0, right = nums.length - 1;//左闭右闭
while(left <= right){//因为是左闭右闭
int mid = left + (right - left) / 2;//巧妙取得中间下标
if(nums[mid] == target){
right = mid - 1;//继续向左边查找
}else if(nums[mid] > target){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}
}
if(left >= 0 && left < nums.length){//判断left下标是否合法
result[0] = nums[left] == target ? left : -1;
}
//右
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;
}
}
if(right >= 0 && right < nums.length){//判断right下标是否合法
result[1] = nums[right] == target ? right : -1;
}
return result;
}
}
2.搜索插入位置
思路📢:当目标元素 target 不存在数组 nums 中时,搜索左侧边界的二分搜索的返回值可以做以下几种解读:
1、返回的这个值是 nums 中大于等于 target 的最小元素索引。
2、返回的这个值是 target 应该插入在 nums 中的索引位置。
3、返回的这个值是 nums 中小于 target 的元素个数。
class Solution {
public int searchInsert(int[] nums, int target) {//二分搜索找最左边界
int left = 0, right = nums.length-1;//左闭右闭
while(left <= right){
int mid = left + (right - left) / 2;//巧妙求中间下标
if(nums[mid] == target){
right = mid - 1;//接着往左边找
}else if(nums[mid] > target){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}
}
return left;
}
}
3.搜索旋转排序数组
思路📢:第一次二分找到旋转的位置,然后确定 target 在左半段还是右半段,第二次二分找 target
注意📢在找旋转位置结束时
- left 指向第一个小于nums[0]的值;
- right 指向最后一个大于nums[0]的值;
- 因为结束时 left 肯定为 right+1,如果最后是因为执行了
left=mid+1导致条件终止的,那么 mid 处的元素肯定是大于nums[0]的,所以 left 处就是第一个小于 nums[0] 的元素了,对应的 right 在 left 的左边,所以 right 就是最后一个大于 nums[0] 的元素了; - 如果是因为执行了
right=mid-1导致条件终止的,那么也一样,mid 处的元素肯定小于 nums[0],导致 right 向左收缩,所以 right 是指向了最后一个大于 nums[0] 的元素,对应的 left 肯定就是第一个小于 nums[0] 的元素了
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length-1;//左闭右闭
//执行完这个后 left 指向第一个小于nums[0]的值;right 指向最后一个大于nums[0]的值
while(left <= right){//二分法,通过nums[0]找到旋转点
int mid = left + (right - left) / 2;//巧妙求 mid
if(nums[mid] == nums[0]){
left = mid + 1;//接着往右边找
}else if(nums[mid] > nums[0]){
left = mid + 1;
}else if(nums[mid] < nums[0]){
right = mid - 1;
}
}
if(target == nums[0]){//可以返回结果了,运气相当好
return 0;
}
if(target > nums[0]){//在左半段
left = 0;
}else{//在右半段
right = nums.length - 1;
}
while(left <= right){//在来一次二分法
int mid = left + (right - left) / 2;//巧妙求 mid
if(nums[mid] == target){
return mid;//找到了
}else if(nums[mid] > target){
right = mid - 1;
}else if(nums[mid] < target){
left = mid + 1;
}
}
return -1;
}
}
4.寻找旋转排序数组中的最小值
思路📢:和上一个题一毛一样;
- left 指向第一个小于nums[0]的元素
- right 指向最后一个大于nums[0]的元素
class Solution {
public int findMin(int[] nums) {
int left = 0, right = nums.length - 1;//左闭右闭
while(left <= right){
int mid = left + (right - left) / 2;
if(nums[mid] == nums[0]){
left = mid + 1;
}else if(nums[mid] > nums[0]){
left = mid + 1;
}else if(nums[mid] < nums[0]){
right = mid - 1;
}
}
if(left >= 0 && left < nums.length){//判断下标是否合法
return nums[left];
}
return nums[0];//如果不合法说明 nums 数组是正序的
}
}
5.
11.2 双指针
1.移动零
数组排序链接🔗leetcode.cn/problems/so…
解法1:笨办法,两层for循环,倒序遍历到0,那么就将 0 之后的元素都往前挪一位,将 0 插入到后面
why是倒序,因为只有倒序的话,只会挪动i之后的元素,不会影响i之前的元素,不影响遍历!
class Solution {
public void moveZeroes(int[] nums) {
int left = 0;
int right = nums.length-1;
//注意倒序
for(int i = right; i >= 0; i--){
if(nums[i] == 0){
moveArr(nums, i, right);
nums[right] = 0;
right--;
}
}
Arrays.toString(nums);
}
//挪动元素
private static void moveArr(int[] array, int left, int right){
for(int i = left; i < right; i++){
array[i] = array[i+1];
}
}
}
解法2:类似快排的思想,将 0 作为标准值,不等于 0 的放到 0 的左边,
public void moveZeroes(int[] nums) {
int pointer = 0;
for(int i = 0; i < nums.length; i++){
//不等于 0 的放到 0 的左边!
if(nums[i] != 0){
int temp = nums[pointer];
nums[pointer++] = nums[i];
nums[i] = temp;
}
}
Arrays.toString(nums);
}
2.盛最多水的容器
思路:采用双指针,从两边向中间收窄;每次收短板的那边,看他有没有希望变高呢!
class Solution {
public int maxArea(int[] height) {
int i = 0;
int j = height.length - 1;
int result = 0;
while(i < j){//每次收短板的那一边,短板有希望变高,每次都更新面积值 result
result = height[i] < height[j] ? Math.max(result, (j-i) * height[i++] : Math.max(result, (j-i) * height[j--]);
}
return result;
}
}
3.三数之和-双指针
思路:如果采用暴力的思路的话,需要 3 层 for 循环,一层循环确定一个数
而使用双指针的好处是我们将三层循环简化成了两层,第二层循环采用双指针一下确定两个数,太优雅了!
- 拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。
- 依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。
- 接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。
- 如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。
去重的逻辑
-
对于 a 的去重,第二层循环的时候如果
i > 0 && nums[i] == nums[i-1],那么说明 a 重复,直接continue -
对于 b、c 的去重,可以统一在找到 a+b+c=0 的时候在去重
while(left < right && nums[left] == nums[left+1])让left++while(left < right && nums[right] == nums[right-1])让right--- 最后再让 left++; right--;
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
//先给 nums 排序
Arrays.sort(nums);
for(int i = 0; i < nums.length; i++){
//对 a 去重
if(i > 0 && nums[i] == nums[i-1]){
continue;
}
int left = i+1;
int right = nums.length-1;
while(left < right){
if(nums[i] + nums[left] + nums[right] < 0){
left++;
}else if(nums[i] + nums[left] + nums[right] > 0){
right--;
}else{
result.add(Arrays.asList(nums[i], nums[left], nums[right]));
//对 b,c 去重
while(left < right && nums[left] == nums[left+1]){
left++;
}
while(left < right && nums[right] == nums[right-1]){
right--;
}
left++;
right--;
}
}
}
return result;
}
}
4.接雨水
思路:双指针确实巧妙
class Solution {
public int trap(int[] height) {
int result = 0;//结果
int left = 0, right = height.length-1;//左右指针
int l_max = 0, r_max = 0;//l_max代表[0~left]之间最高的柱子高度,r_max代表[right~length-1]之间最高的柱子高度
while(left < right){
l_max = Math.max(l_max, height[left]);
r_max = Math.max(r_max, height[right]);
if(l_max < r_max){//左边有更低的柱子,可以存水
result += l_max - height[left];//当前位置可以存多少水
left++;
}else{//右边有更低的柱子,可以存水
result += r_max - height[right];//当前位置可以存多少水
right--;
}
}
return result;
}
}